Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added more distinct admin and teacher roles and ability to impersonate #111

Merged
merged 8 commits into from
Jan 2, 2024

Conversation

bradley-erickson
Copy link
Collaborator

No description provided.

@@ -79,7 +80,8 @@ async def password_auth_handler(request):
'name': user_data.get('name', ''),
'family_name': user_data.get('family_name', ''),
'picture': user_data.get('picture', '/auth/default-avatar.svg'),
'authorized': True
'authorized': True,
'role': learning_observer.auth.roles.ROLES.ADMIN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth.ROLES.ADMIN or auth.roles.ADMIN <-- remove one level of indirection

fine just as from roles import ROLES or setattr on the module with a loop over ROLES

@@ -151,7 +152,9 @@ async def _google(request):
'back_to': request.query.get('state'),
'picture': profile['picture'],
# TODO: Should this be immediate?
'authorized': await learning_observer.auth.utils.verify_teacher_account(profile['id'], profile['email'])
# TODO: Should this still just verify the teacher account?
'authorized': await learning_observer.auth.utils.verify_teacher_account(profile['id'], profile['email']),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verify_account(TEACHER, profiles...) or similar.

Same everywhere. Have a dict mapping to files if needed: {TEACHER: {'roster_file': '/foo/bar/teacher.json', ...}}

The key here is DRY.

@@ -50,6 +51,28 @@ def fernet_key(secret_string):
return md5_hash.hexdigest().encode('utf-8')


async def verify_role(user_id, email):
if await verify_admin_account(user_id, email):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loop

@@ -160,7 +162,8 @@ async def password_auth_handler(request):
'name': "",
'family_name': "",
'picture': "",
'authorized': True
'authorized': True,
'role': learning_observer.auth.roles.ROLES.ADMIN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not want to default to ADMIN ever, anywhere, since that will be brittle to security issues

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Default to student
  • Permit a different role in settings (e.g. settings.auth.http_basic_auth.role)
  • Permit escalation through files enumerating admin and teacher users

The use cases:

  • Students log in through http basic, and a select number of teacher have dashboards
  • Teachers log in for demoing dashboards, same as above for admin

Extend the password file to allow an optional role field for escalation beyond default (note the script in util needs to be updated to allow this, and to allow this to be left blank for default behavior).

#
# For now, we don't have seperate teacher and admin accounts.
teacher = admin
def role_required(role):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_role_required

session = await aiohttp_session.get_session(request)
user = session['user']

cache_key = "raw_google/" + user['user_id'] + '/' + learning_observer.util.url_pathname(url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there ought to be (already, and if not make one) a helper to make keys. If I submit a user_id of /etc/passwd or something, I don't want the universe imploding. This was of constructing the key looks very concerning to me.

In the whole system, we want one way of encoding strings, and one way of making safe keys and similar (or perhaps two if really, really needed)

See also, safe versus unsafe strings.

@@ -190,7 +194,8 @@ def initialize_and_register_routes(app):
for key in ['save_google_ajax', 'use_google_ajax', 'save_clean_ajax', 'use_clean_ajax']:
if key in settings.settings['feature_flags']:
global cache
cache = learning_observer.kvs.FilesystemKVS(path=learning_observer.paths.data('google'), subdirs=True)
cache = learning_observer.kvs.KVS()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be some kind of specific KVS for this functionality, bubbling up to default. I want to be able to use the filesystem method too, for example.

@pmitros
Copy link
Contributor

pmitros commented Dec 21, 2023

Before merge (not now): Let's make sure everything has good docstrings.


session['original_user_id'] = session['user']['user_id']
session['user']['user_id'] = requested_user_id
return aiohttp.web.Response(text=f'Masking as a new user: {requested_user_id}')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Two impersonations later, I've lost my original user id.
  2. We're overloading the meaning of user_id in ways I don't like

I'd much rather have a helper to get at the user (e.g. not call into session directly anymore) which gives the appropriate user, and have fields in the session:

user_id and masquerading_as

which the helper uses. That's a better data representation, and we want a helper in either case. As much as possible, we want to use decorators and helpers for specific purposes, rather than diving into session directly.

if 'original_user_id' in session:
session['user']['user_id'] = session['original_user_id']
del session['original_user_id']
return aiohttp.web.Response(text='Done masking user')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use same consistent terminology :)

@@ -278,6 +278,7 @@ async def keys(self):
'stub': InMemoryKVS,
'redis_ephemeral': EphemeralRedisKVS,
'redis': PersistentRedisKVS
# TODO add filesystem KVS to this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this TODO :)

@@ -105,6 +105,23 @@ def validate_teacher_list():
)


@register_startup_check
def validate_admins_list():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loop. One call. DRY

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TEACHER, ADMIN

'/stop-impersonation',
learning_observer.impersonate.stop_impersonation
)
])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also want a view, UX, or similar at some point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header on every page, warning "viewing as" as well as UX for changing who / where

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine do do UX just for react pages and assume we'll add it in as we migrate the rest of the system.

@@ -88,6 +88,16 @@ async def update_session_user_info(request, user):
session["user"] = user


async def get_active_user(request):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. FYI: Maybe get_effective_user and get_true_user? Not sure.

session['original_user_id'] = session['user']['user_id']
session['user']['user_id'] = requested_user_id
# TODO we should pull more of the users information from somewhere
session[IMPERSONATING_AS] = {'user_id': requested_user_id}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Come to think of it, we should have similar variables for user_id and more globally as well, if you are making further passes. :)

auth in our code (e.g. password, http_basic, google, etc.)
'''
if 'user' in request and \
request['user'] is not None and \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: Formatting here is wonky. Line continuations should be indented.

This is also a train wreck (MY train wreck, looking at original code). Should be lines more like: authorized = request.get(USER, {}).get(AUTHORIZED, False), and ditto for role.

And then: if session_authorized and session_role in [role, role.ROLES.ADMIN]

That's a lot more readable to break it down like that. A 5 line continuation is a flag that something is wrong.

This is actually pretty good code circa-eighties/nineties coding standards :) I sometimes revert to my childhood, apparently.

@learning_observer.prestartup.register_startup_check
def connect_to_google_cache():
'''Setup cache for request to the Google API if.
The cache is currenlty only used with the `use_google_ajax`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typos: "if." "currenlty"

session = await aiohttp_session.get_session(request)
if IMPERSONATING_AS in session:
del session[IMPERSONATING_AS]
return aiohttp.web.Response(text='Done impersonating user.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps json_response so it's expandable?

@bradley-erickson bradley-erickson merged commit 8735c98 into master Jan 2, 2024
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants