-
Notifications
You must be signed in to change notification settings - Fork 14
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
Superadmin Implementation #168
Conversation
…ty to user model for checking superadmin. Added DAO functions to get and set the superadmin propery.
Added a link to the admin console fro dropdown when a user is logged in. Made Super Admin functions only show up for superadmin users.
…min functions of adding new regions and users.
… page now hides adding region/users fields to everyone but superadmins
I still need to implement a unit test or two before we PR this, but the functionality is there. |
…ng serer side if a user has SuperAdmin privileges
Tests written and this PR is ready to be reviewed |
… would ensure that superadmins have access to Anything and any function on the website
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.
just tested it out a bunch on stage -- looks good!
mostly minor things. the biggest thing is whether we actually want to use an Enum for the admin_levels (and if we do, we should be consistent and use the enum constants everywhere instead of string constants).
in addition to these review comments, i'm also having issues as a superadmin editing default rating criteria for a region that's not in my regions list... probably another check is needed there for superadmins?
@@ -348,7 +349,8 @@ class User(orm.Document): | |||
('username', orm.StringField(required=True)), | |||
('salt', orm.StringField(required=True)), | |||
('hashed_password', orm.StringField(required=True)), | |||
('admin_regions', orm.ListField(orm.StringField()))] | |||
('admin_regions', orm.ListField(orm.StringField())), | |||
('admin_level', orm.StringField(required=True, default='REGION'))] |
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.
since these guys should come from an Enum, probably it is a good idea to add a choices validator so the ORM will automatically check for this. (take a look at how tournament source types work on line 129).
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.
So I've gone with the String Only method, but I'm not really sure how to validate on the ORM. can you point me in the right direction?
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.
so, take a look at line 129 to see how it's done for tournament source_types (challonge, tio, smashgg).
basically, you should do something like
('admin_level', orm.StringField(
required=True,
validators=[orm.validate_choices(ADMIN_LEVEL_CHOICES)]))
and define a list/tuple ADMIN_LEVEL_CHOICES somewhere.
@@ -371,3 +373,8 @@ class Session(orm.Document): | |||
collection_name = 'sessions' | |||
fields = [('session_id', orm.StringField(required=True)), | |||
('user_id', orm.StringField(required=True))] | |||
|
|||
|
|||
class AdminLevels(Enum): |
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.
probably makes most sense to move this up to the top, next to the SOURCE_TYPE_CHOICES "enum".
For that matter, I'm still not sure if we gain anything by making this an explicit enum instead of just writing a tuple of choices (as in SOURCE_TYPE_CHOICES) and using the validator. It would make more sense I feel if we were mapping human-readable enum values to non-human readable int values (this is what a lot of enums are for), but since we are mapping them to string values, it feels a bit redundant (also, in server.py, it looks like we actually just use the explicit string constants).
Anyway tl;dr either way (enums/lists of strings) is okay, but if we decide to go the Enum route we should probably also convert SOURCE_TYPE_CHOICES to an Enum for consistency.
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.
Going with the string method so no need for this
@@ -137,6 +144,8 @@ def get_user_from_request(request, dao): | |||
|
|||
|
|||
def is_user_admin_for_region(user, region): | |||
if user.admin_level == 'SUPER': |
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.
if we want to stick with the Enum method, these 'SUPER's and everything should be M.AdminLevels.SUPERs.
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.
Going with the String method. so no need to change this
@@ -1176,8 +1209,8 @@ def put(self): | |||
user = get_user_from_request(request, dao) | |||
if not user: | |||
return 'Permission denied', 403 | |||
#if not is_user_admin_for_region(user, region='*'): | |||
# return 'Permission denied', 403 | |||
if not dao.get_is_superadmin(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.
hmm if we have the user object, can't we just check if user.admin_level=='SUPER'? This should save a mongo lookup.
@@ -559,7 +559,7 @@ def insert_user(self, user): | |||
|
|||
# throws invalidRegionsException, which is okay, as this is only used by a |
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.
let's remove this comment, it's no longer true with the introduction of the admin dashboard.
@@ -645,6 +665,17 @@ def check_creds_and_get_session_id_or_none(self, username, password): | |||
else: | |||
return None | |||
|
|||
def check_creds(self, username, password): |
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 should try not to repeat code (this is basically part of check_creds_and_get_session_id_or_none). maybe let's make check_creds
return a user object, and refactor check_creds_and_get_session_id_or_none
to call check_creds
first?
(this could also be postponed to a later PR, but we should make a note).
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.
OK so. This one is interesting. I tried making its own function via the following:
def get_and_verify_user_by_username(self, username):
result = self.users_col.find({"username": username})
if result.count() == 0:
return None
assert result.count() == 1, "WE HAVE DUPLICATE USERNAMES IN THE DB"
user = M.User.load(result[0], context='db')
assert user, "mongo has stopped being consistent, abort ship"
return user
However, when we run this function, I get problems with this function:
def check_creds_and_get_session_id_or_none(self, username, password):
user = self.get_and_verify_user_by_username(username)
# timing oracle on this... good luck
if verify_password(password, user.salt, user.hashed_password):
session_id = base64.b64encode(os.urandom(128))
self.update_session_id_for_user(user.id, session_id)
return session_id
else:
return None
The error I get is that it can't get user.salt for NoneType
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.
Oh looking a little closer it might be because it finds no user in the db and then returns None...
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.
i'm confused, it looks there are now 3 functions which repeat a lot of code?:
check_creds(username, password)
which returns whether (username, password) are valid credentials.check_creds_and_get_session_id_or_none(username, password)
, which checks if (username, password) are valid credentials, and tries to return the current sessionget_and_verify_user_by_username
, which seems to be a duplicate ofget_user_by_username
.
Right now 1 and 2 duplicate a lot of the same code. I recommend removing get_and_verify_user_by_username
and refactoring 1 and 2 so that check_creds returns a user object (not just a bool), and so that get_session_id calls check_creds to get the user object.
else: | ||
return False | ||
|
||
def set_user_superadmin(self, user_id, admin_level): |
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 can be used for any admin_level, so let's call it something like set_user_admin_level
or update_user_admin_level
? (it might also make more sense to just make a general update_user
function in the same vein as update_player
and update_tournament
).
@@ -1196,9 +1229,14 @@ def put(self): | |||
uperm = args['new_user_permissions'] | |||
uregions = args['new_user_regions'] | |||
|
|||
perm = None | |||
if uperm == 'Super Admin': perm = M.AdminLevels.SUPER |
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.
for consistency, let's make the values of the permissions on the frontend match the string values of the enums on the backends (i.e. 'SUPER' and 'REGION')
|
||
<h4>Permission Level:</h4> | ||
<select ng-model="postParams.new_user_permissions" id="permissionLevelSelect"> | ||
<option>Region Admin</option> |
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.
can we put value='REGION' and value='SUPER' here to pass 'REGION' and 'SUPER' to the backend? (not a big deal, but it's nice when values are consistent between frontend and backend)
return False | ||
|
||
def set_user_admin_level(self, user_id, admin_level): | ||
if type(admin_level) is not M.AdminLevels: |
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.
I think M.AdminLevels does not exist anymore, so this should error (is it being tested)?
In any case, the ORM should take care of this check.
@@ -619,6 +618,25 @@ def get_user_by_session_id_or_none(self, session_id): | |||
def get_user_by_region(self, regions): | |||
pass | |||
|
|||
def get_is_superadmin(self, user_id): | |||
user = None |
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.
I think it is probably better to use get_user_by_id_or_none
here instead of rewriting the Mongo queries (so something like user = get_user_by_id_or_none(user_id)
.
Actually, do we use this function (get_is_super_admin) anywhere else in the code? If not, I think it's fine to remove this function (it is easy enough to just check whether user.admin_level=='SUPER' whenever we have a user object).
else: | ||
return False | ||
|
||
def set_user_admin_level(self, user_id, admin_level): |
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 we actually use this function anywhere else in the code yet? if not, I think it would be fine to just replace it with a simple update_user
function, in the same style as update_player
and update_region
.
def set_user_admin_level(self, user_id, admin_level): | ||
if type(admin_level) is not M.AdminLevels: | ||
raise Exception('Submitted admin level is not of correct type') | ||
if self.users_col.find_one({'_id': 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.
right now this way of updating bypasses the ORM completely and won't trigger any of the ORMs validators. the correct way to do this sort of thing is 1. read the user data into a User object (as is done in get_user_by_id_or_none), 2. update whatever properties of the User object, 3. then write the User object back into the database using self.users_col.update({'_id': user.id}, user.dump(context='db'))
.
@@ -645,6 +665,17 @@ def check_creds_and_get_session_id_or_none(self, username, password): | |||
else: | |||
return None | |||
|
|||
def check_creds(self, username, password): |
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.
i'm confused, it looks there are now 3 functions which repeat a lot of code?:
check_creds(username, password)
which returns whether (username, password) are valid credentials.check_creds_and_get_session_id_or_none(username, password)
, which checks if (username, password) are valid credentials, and tries to return the current sessionget_and_verify_user_by_username
, which seems to be a duplicate ofget_user_by_username
.
Right now 1 and 2 duplicate a lot of the same code. I recommend removing get_and_verify_user_by_username
and refactoring 1 and 2 so that check_creds returns a user object (not just a bool), and so that get_session_id calls check_creds to get the user object.
Rewrote set admin-level. Also commented it out until there's a time we need to use it. Deleted the third creds_check because it was broken initially. I left it in in case we could figure it out but I have no time anymore.
Jon, I made the changes. Notably I deleted the third check_creds thing I had written. I left it in to try to fix whatever bug was in it, but now I have no time. Additionally, the third function for check_creds (that seemingly duplicate code) I wrote to try to get the user and return that data (the first half of both check_creds functions). However, it wasn't working properly. So I left it in there to fix another day. But instead I just deleted it in the last commit. We can do a refactor on it later; right now I'm just lacking free time. |
I fixed a bug with creating new users; I think it works now. Also, while fixing this I noticed some of the frontend code is probably less angular-y than it should be; there's a lot of places where you're manipulating the DOM directly (I think the angular philosophy is that this should basically always be done through angular directives). Fixing this can probably wait for a later PR though; there is a bunch of frontend refactoring that still needs to be done. |
* Added function to sessions service to check super admin. Added property to user model for checking superadmin. Added DAO functions to get and set the superadmin propery. * Made is_superadmin false by default (new users). Added a link to the admin console fro dropdown when a user is logged in. Made Super Admin functions only show up for superadmin users. * Bug fixes for get_is_superadmin. Added a Superadmin check onto the admin functions of adding new regions and users. * Adding additional features to admin portal. Change password. List regions. * Refactored superadmin from T/F booleans into enumerated Strings * Successfully programmed password change form to work. Admin functions page now hides adding region/users fields to everyone but superadmins * Added select field for permissions on inserting a new user form * Bug fix for admin permission levels * Added unit tests for Changing Password via PUT request, and for testing serer side if a user has SuperAdmin privileges * Added tests to try adding a region and user with and without superadmin privileges * Added lines to check SuperAdmin privileges before regular admin. This would ensure that superadmins have access to Anything and any function on the website * Fixed syntax error * Refactoring for PR. 80% done. * PR Fixes. Added admin level validators. Removed Comment not neccessary. * Changes for Jon. Rewrote set admin-level. Also commented it out until there's a time we need to use it. Deleted the third creds_check because it was broken initially. I left it in in case we could figure it out but I have no time anymore. * fix bug with creating users in admin functions
This PR implements: