Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 46 additions & 25 deletions learning_observer/learning_observer/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
We haven't figured out all of the data models here.
'''

import pmss
import sys

# Decorators to confirm requests are authenticated
Expand Down Expand Up @@ -78,37 +79,57 @@
import learning_observer.prestartup
import learning_observer.settings as settings

# HACK These enabled items ought to be implicitly defined by pmss if
# the child settings are available. For now, we define these manually
# in the creds.pmss file.
pmss.register_field(
name='google_oauth_enabled',
type=pmss.pmsstypes.TYPES.boolean,
description='Flag for determining if we should use Google OAuth or not.',
default=False
)
pmss.register_field(
name='password_file_enabled',
type=pmss.pmsstypes.TYPES.boolean,
description='Flag for determining if we should use a Password File or not.',
default=False
)
pmss.register_field(
name='http_basic_auth_enabled',
type=pmss.pmsstypes.TYPES.boolean,
description='Flag for determining if we should use a HTTP Basic or not.',
default=False
)

pmss.register_field(
name='password_file',
type=pmss.pmsstypes.TYPES.string,
description='Path to the password file on the system.'
)


@learning_observer.prestartup.register_startup_check
def verify_auth_precheck():
'''
This is a pre-startup check to make sure that the auth system is configured
correctly.
'''
# We need some auth
if 'auth' not in settings.settings:
raise learning_observer.prestartup.StartupCheck(
"Please configure auth")

# If we have Google oauth, we need it properly configured.
# TODO: Confirm everything works with Google Oauth missing
if 'google_oauth' in settings.settings['auth']:
if 'web' not in settings.settings['auth']['google_oauth'] or \
'client_secret' not in settings.settings['auth']['google_oauth']['web'] or \
'project_id' not in settings.settings['auth']['google_oauth']['web'] or \
'client_id' not in settings.settings['auth']['google_oauth']['web'] or \
isinstance(settings.settings['auth']['google_oauth']['web']['client_secret'], dict) or \
isinstance(settings.settings['auth']['google_oauth']['web']['project_id'], dict) or \
isinstance(settings.settings['auth']['google_oauth']['web']['client_id'], dict):
error = \
"Please configure (or disable) Google oauth\n" + \
"\n" + \
"Go to:\n" + \
" https://console.developers.google.com/ \n" + \
"And set up an OAuth client for a web application. Make sure that configuration\n" + \
"mirrors the one here.\n" + \
"\n" + \
"If you are not planning to use Google auth (which is the case for most dev\n" + \
"settings), please disable Google authentication in creds.yaml by\n" + \
"removing the google_auth section under auth."
raise learning_observer.prestartup.StartupCheck("Auth: " + error)
try:
settings.pmss_settings.client_secret(types=['auth', 'google_oauth', 'web'])
settings.pmss_settings.project_id(types=['auth', 'google_oauth', 'web'])
settings.pmss_settings.client_id(types=['auth', 'google_oauth', 'web'])
except Exception as e:
error = \
"Please configure (or disable) Google oauth\n" + \
"\n" + \
"Go to:\n" + \
" https://console.developers.google.com/ \n" + \
"And set up an OAuth client for a web application. Make sure that configuration\n" + \
"mirrors the one here.\n" + \
"\n" + \
"If you are not planning to use Google auth (which is the case for most dev\n" + \
"settings), please disable Google authentication in creds.yaml by\n" + \
"removing the google_auth section under auth."
raise learning_observer.prestartup.StartupCheck("Auth: " + error)
31 changes: 21 additions & 10 deletions learning_observer/learning_observer/auth/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@
Some of these code paths are untested. Please test and debug before using.
'''

import asyncio
import pmss
import urllib.parse
import secrets
import sys

import aiohttp_session
import aiohttp.web
Expand All @@ -37,9 +36,22 @@

from learning_observer.log_event import debug_log

AVAILABLE_AUTH_METHODS = ['local_storage', 'chromebook', 'testcase_auth', 'guest']
AUTH_METHODS = {}


pmss.register_field(
name='userfile',
type=pmss.TYPES.string,
description='Filename for determining which users to authenticate.'
)
pmss.register_field(
name='allow_guest',
type=pmss.TYPES.boolean,
description='Flag for if guests are allowed for a given auth method.'
)


def register_event_auth(name):
'''
Decorator to register a method to authenticate events
Expand Down Expand Up @@ -105,13 +117,13 @@ def token_authorize_user(auth_method, user_id_token):
'''
Authorize a user based on a list of allowed user ID tokens
'''
am_settings = learning_observer.settings.settings['event_auth'][auth_method]
if 'userfile' in am_settings:
userfile = am_settings['userfile']
userfile = learning_observer.settings.pmss_settings.userfile(types=['event_auth', auth_method])
if userfile:
users = [u.strip() for u in open(learning_observer.paths.data(userfile)).readlines()]
if user_id_token in users:
return "authenticated"
if am_settings.get("allow_guest", False):
allow_guest = learning_observer.settings.pmss_settings.allow_guest(types=['event_auth', auth_method])
if allow_guest:
return "unauthenticated"
raise aiohttp.web.HTTPUnauthorized()

Expand Down Expand Up @@ -311,7 +323,7 @@ async def authenticate(request, headers, first_event, source):
2. Providence: How they were authenticated (if at all), or how we believe they are who they are.
3. `user_id` -- a unique user identifier
'''
for auth_method in learning_observer.settings.settings['event_auth']:
for auth_method in AVAILABLE_AUTH_METHODS:
auth_metadata = await AUTH_METHODS[auth_method](request, headers, first_event, source)
if auth_metadata:
if "safe_user_id" not in auth_metadata:
Expand All @@ -330,9 +342,7 @@ def check_event_auth_config():
Check that all event auth methods are correctly configured,
before events come in.
'''
if 'event_auth' not in learning_observer.settings.settings:
raise learning_observer.prestartup.StartupCheck("Please configure event authentication")
for auth_method in learning_observer.settings.settings['event_auth']:
for auth_method in AVAILABLE_AUTH_METHODS:
if auth_method not in AUTH_METHODS:
raise learning_observer.prestartup.StartupCheck(
"Please configure event authentication for {}\n(Methods: {})".format(
Expand All @@ -342,6 +352,7 @@ def check_event_auth_config():


if __name__ == "__main__":
import asyncio
import doctest
print("Running tests")

Expand Down
27 changes: 22 additions & 5 deletions learning_observer/learning_observer/auth/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import base64
import json
import pmss
import random

import aiohttp
Expand All @@ -21,6 +22,22 @@

import names

# TODO we ought to define all the auth items for pmss
# in one spot. Currently they are split up across
# multiple files.
pmss.register_field(
name='test_case_insecure',
type=pmss.TYPES.boolean,
description='For testing to allow no-login required.',
default=False
)
pmss.register_field(
name='demo_insecure',
type=pmss.TYPES.string,
description='Similar to `test_case_insecure`, but provides a name.',
default=None
)


async def logout_handler(request):
"""
Expand Down Expand Up @@ -67,7 +84,7 @@ async def test_case_user(request):
This is a short circuit for test cases without logging in.
THIS SHOULD NEVER BE ENABLED ON A LIVE SERVER
'''
tci = learning_observer.settings.settings['auth'].get("test_case_insecure", False)
tci = learning_observer.settings.pmss_settings.test_case_insecure(types=['auth'])
if not tci:
return None
if not isinstance(tci, dict):
Expand Down Expand Up @@ -96,7 +113,7 @@ async def demo_user(request):
In contrast to the test case user, this assigns a dummy name and similar. That's
bad for testing, where we want determinism, but it's good for demos.
'''
if not learning_observer.settings.settings['auth'].get("demo_insecure", False):
if not learning_observer.settings.pmss_settings.demo_insecure(types=['auth']):
return None

def name_to_email(name):
Expand All @@ -115,9 +132,9 @@ def name_to_email(name):
name = name.split()
return name[0][0].lower() + name[-1].lower() + "@localhost"

demo_auth_setting = learning_observer.settings.settings['auth']["demo_insecure"]
if isinstance(demo_auth_setting, dict) and 'name' in demo_auth_setting:
name = demo_auth_setting['name']
demo_auth_setting = learning_observer.settings.pmss_settings.demo_insecure(types=['auth'])
if demo_auth_setting:
name = demo_auth_setting
else:
name = names.get_full_name()

Expand Down
40 changes: 31 additions & 9 deletions learning_observer/learning_observer/auth/http_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'''
import base64
import json
import pmss
import yaml
import sys

Expand All @@ -30,6 +31,29 @@

from learning_observer.log_event import debug_log

# TODO I noticed that this code consistently referred to
# `http_basic` whereas the creds example that I've been using
# refers to `http_basic_auth`. We ought make sure we are
# using a consistent reference.
pmss.register_field(
name='full_site_auth',
type=pmss.TYPES.boolean,
description='', # TODO
default=False
)
pmss.register_field(
name='login_page_enabled',
type=pmss.TYPES.boolean,
description='', # TODO
default=False
)
pmss.register_field(
name='delegate_nginx_auth',
type=pmss.TYPES.boolean,
description='', # TODO,
default=False
)


def http_basic_extract_username_password(request):
'''
Expand Down Expand Up @@ -73,10 +97,9 @@ def http_auth_middleware_enabled():
to accidentally receive requests with auth headers on pages which
nginx has not secured.
'''
if 'http_basic' not in learning_observer.settings.settings['auth']:
if not learning_observer.settings.pmss_settings.http_basic_auth_enabled(types=['auth']):
return False
auth_basic_settings = learning_observer.settings.settings['auth']['http_basic']
return auth_basic_settings.get("full_site_auth", False)
return learning_observer.settings.pmss_settings.full_site_auth(types=['auth', 'http_basic_auth'])


def http_auth_page_enabled():
Expand All @@ -88,11 +111,10 @@ def http_auth_page_enabled():
this.
'''
# Is http basic auth enabled?
if 'http_basic' not in learning_observer.settings.settings['auth']:
if not learning_observer.settings.pmss_settings.http_basic_auth_enabled(types=['auth']):
return False
auth_basic_settings = learning_observer.settings.settings['auth']['http_basic']
# And is it configured with a dedicated login page?
if not auth_basic_settings.get("login_page_enabled", False):
if not learning_observer.settings.pmss_settings.login_page_enabled(types=['auth', 'http_basic_auth']):
return False
return True

Expand Down Expand Up @@ -185,9 +207,9 @@ def http_basic_startup_check():
)

if (
'http_basic' in learning_observer.settings.settings['auth']
and learning_observer.settings.settings['auth']['http_basic'].get("delegate_nginx_auth", False)
and learning_observer.settings.settings['auth']['http_basic'].get("password_file", False)
learning_observer.settings.pmss_settings.http_basic_auth_enabled(types=['auth'])
and learning_observer.settings.pmss_settings.password_file(types=['auth', 'http_basic_auth'])
and learning_observer.settings.pmss_settings.delegate_nginx_auth(types=['auth', 'http_basic_auth'])
):
raise learning_observer.prestartup.StartupCheck(
"Your HTTP Basic authentication is misconfigured.\n"
Expand Down
9 changes: 9 additions & 0 deletions learning_observer/learning_observer/auth/social_sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
description="The Google OAuth client secret",
required=True
)
pmss.register_field(
name="project_id",
type=pmss.pmsstypes.TYPES.string,
description="The Google OAuth project id",
required=True
)
pmss.register_field(
name='fetch_additional_info_from_teacher_on_login',
type=pmss.pmsstypes.TYPES.boolean,
Expand Down Expand Up @@ -106,6 +112,9 @@
]

# TODO Type list is not yet supported by PMSS 4/24/24
# We did another pass of PMSS conversion and lists are still
# not supported. Our current strategy is to try and make things
# work without lists until they are supported 2/10/24
# pmss.register_field(
# name='base_scopes',
# type='list',
Expand Down
2 changes: 1 addition & 1 deletion learning_observer/learning_observer/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def _role_required(role):
def decorator(func):
@functools.wraps(func)
async def wrapper(request):
if learning_observer.settings.settings['auth'].get("test_case_insecure", False):
if learning_observer.settings.pmss_settings.test_case_insecure(types=['auth']):
return await func(request)
'''TODO evaluate how we should be using `role` with the
`authorized` key.
Expand Down
37 changes: 34 additions & 3 deletions learning_observer/learning_observer/client_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,37 @@
'''

import aiohttp
import pmss

import learning_observer.settings
import learning_observer.auth.http_basic

# TODO where should we define the theme items?
pmss.register_field(
name='server_name',
type=pmss.pmsstypes.TYPES.string,
description='Name of the server to include on the login page.',
default='Learning Observer'
)
pmss.register_field(
name='front_page_pitch',
type=pmss.pmsstypes.TYPES.string,
description='Short server description to include on the login page.'
)
pmss.register_field(
name='logo_big',
type=pmss.pmsstypes.TYPES.string,
description='Path to logo to display on the login page.'
)
# This item is used in routes, but we are leaving it here
# along with the rest of the `theme` items.
pmss.register_field(
name='root_file',
type=pmss.pmsstypes.TYPES.string,
description="We'd like to be able to have the root page themeable, for non-ETS deployments. This is a quick-and-dirty way to override the main page.",
default='webapp.html'
)


async def client_config_handler(request):
'''
Expand All @@ -34,10 +61,14 @@ async def client_config_handler(request):
'hide_labels': False # TODO: Should be loaded from config file
}
},
"google_oauth": "google_oauth" in learning_observer.settings.settings['auth'],
"password_auth": "password_file" in learning_observer.settings.settings['auth'],
"google_oauth": learning_observer.settings.pmss_settings.google_oauth_enabled(types=['auth']),
"password_auth": learning_observer.settings.pmss_settings.password_file_enabled(types=['auth']),
"http_basic_auth": learning_observer.auth.http_basic.http_auth_page_enabled(),
"theme": learning_observer.settings.settings['theme']
"theme": {
'server_name': learning_observer.settings.pmss_settings.server_name(types=['theme']),
'front_page_pitch': learning_observer.settings.pmss_settings.front_page_pitch(types=['theme']),
'logo_big': learning_observer.settings.pmss_settings.logo_big(types=['theme']),
}
}

return aiohttp.web.json_response(client_config)
Loading