-
Notifications
You must be signed in to change notification settings - Fork 9
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
Conversation
@@ -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 |
There was a problem hiding this comment.
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']), |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
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}') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Two impersonations later, I've lost my original user id.
- 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') |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loop. One call. DRY
There was a problem hiding this comment.
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 | ||
) | ||
]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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 \ |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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.') |
There was a problem hiding this comment.
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?
No description provided.