From a8f5267f0363901809b07c2d293d6bb21d025daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 8 Jul 2016 11:41:06 +0200 Subject: [PATCH 1/3] Made a management.py command 'badger ' The action can be 'grant' or 'revoke'. --- pillar/application/modules/service.py | 47 ++++++++++++++++----------- pillar/manage.py | 14 ++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/pillar/application/modules/service.py b/pillar/application/modules/service.py index 0d477f5e..c22b2707 100644 --- a/pillar/application/modules/service.py +++ b/pillar/application/modules/service.py @@ -7,7 +7,7 @@ from bson import ObjectId from flask import Blueprint, current_app, g, request from werkzeug import exceptions as wz_exceptions -from application.utils import authorization +from application.utils import authorization, authentication from application.modules import local_auth, users blueprint = Blueprint('service', __name__) @@ -51,6 +51,32 @@ def badger(): user_email = args.get('user_email', '') role = args.get('role', '') + current_user_id = authentication.current_user_id() + log.info('Service account %s %ss role %r to/from user %s', + current_user_id, action, role, user_email) + + users_coll = current_app.data.driver.db['users'] + + # Check that the user is allowed to grant this role. + srv_user = users_coll.find_one(current_user_id, + projection={'service.badger': 1}) + if srv_user is None: + log.error('badger(%s, %s, %s): current user %s not found -- how did they log in?', + action, user_email, role, current_user_id) + return 'User not found', 403 + + allowed_roles = set(srv_user.get('service', {}).get('badger', [])) + if role not in allowed_roles: + log.warning('badger(%s, %s, %s): service account not authorized to %s role %s', + action, user_email, role, action, role) + return 'Role not allowed', 403 + + return do_badger(action, user_email, role) + + +def do_badger(action, user_email, role): + """Performs a badger action, returning a HTTP response.""" + if action not in {'grant', 'revoke'}: raise wz_exceptions.BadRequest('Action %r not supported' % action) @@ -60,25 +86,8 @@ def badger(): if not role: raise wz_exceptions.BadRequest('Role not given') - log.info('Service account %s %ss role %r to/from user %s', - g.current_user['user_id'], action, role, user_email) - users_coll = current_app.data.driver.db['users'] - # Check that the user is allowed to grant this role. - srv_user = users_coll.find_one(g.current_user['user_id'], - projection={'service.badger': 1}) - if srv_user is None: - log.error('badger(%s, %s, %s): current user %s not found -- how did they log in?', - action, user_email, role, g.current_user['user_id']) - return 'User not found', 403 - - allowed_roles = set(srv_user.get('service', {}).get('badger', [])) - if role not in allowed_roles: - log.warning('badger(%s, %s, %s): service account not authorized to %s role %s', - action, user_email, role, action, role) - return 'Role not allowed', 403 - # Fetch the user db_user = users_coll.find_one({'email': user_email}, projection={'roles': 1, 'groups': 1}) if db_user is None: @@ -128,7 +137,7 @@ def manage_user_group_membership(db_user, role, action): try: group_id = _role_to_group_id[role] except KeyError: - log.warning('Group for role %r cannot be found, unable to %s members for user %s', + log.warning('Group for role %r cannot be found, unable to %s membership for user %s', role, action, db_user['_id']) return diff --git a/pillar/manage.py b/pillar/manage.py index 794081c1..77387da5 100755 --- a/pillar/manage.py +++ b/pillar/manage.py @@ -1028,5 +1028,19 @@ def sync_project_groups(user_email, fix): log.info('Updated %i user.', result.modified_count) +@manager.command +def badger(action, user_email, role): + from application.modules import service + + with app.app_context(): + service.fetch_role_to_group_id_map() + response, status = service.do_badger(action, user_email, role) + + if status == 204: + log.info('Done.') + else: + log.info('Response: %s', response) + log.info('Status : %i', status) + if __name__ == '__main__': manager.run() From 3711678fba9518f501f5ba2310a3ce3999aec926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 8 Jul 2016 12:50:07 +0200 Subject: [PATCH 2/3] Changed confusing log message --- pillar/manage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pillar/manage.py b/pillar/manage.py index 77387da5..ea033668 100755 --- a/pillar/manage.py +++ b/pillar/manage.py @@ -961,7 +961,7 @@ def sync_role_groups(do_revoke_groups): users_coll.update_one({'_id': user['_id']}, {'$set': {'groups': list(final_groups)}}) - print('%i bad and %i ok users seen.' % (bad_users, ok_users)) + print('%i bad and %i ok user/role combos seen.' % (bad_users, ok_users)) @manager.command From 660a13cab6956f20725598782ad964ba32afb7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 8 Jul 2016 13:03:43 +0200 Subject: [PATCH 3/3] sync_role_groups: Iterating over users, instead of user/role combos. This fixes all roles & group memberships at once, and fixes a bug where the script had to be re-run to apply multiple role changes on a single user. --- pillar/manage.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pillar/manage.py b/pillar/manage.py index ea033668..64faa710 100755 --- a/pillar/manage.py +++ b/pillar/manage.py @@ -922,46 +922,57 @@ def sync_role_groups(do_revoke_groups): ok_users = bad_users = 0 for user in users_coll.find(): + grant_groups = set() + revoke_groups = set() + current_groups = set(user.get('groups', [])) + user_roles = user.get('roles', set()) + for role in service.ROLES_WITH_GROUPS: - action = 'grant' if role in user.get('roles', ()) else 'revoke' + action = 'grant' if role in user_roles else 'revoke' groups = service.manage_user_group_membership(user, role, action) if groups is None: # No changes required - ok_users += 1 continue - current_groups = set(user.get('groups')) if groups == current_groups: - ok_users += 1 continue + grant_groups.update(groups.difference(current_groups)) + revoke_groups.update(current_groups.difference(groups)) + + if grant_groups or revoke_groups: bad_users += 1 - grant_groups = groups.difference(current_groups) - revoke_groups = current_groups.difference(groups) + expected_groups = current_groups.union(grant_groups).difference(revoke_groups) print('Discrepancy for user %s/%s:' % (user['_id'], user['full_name'].encode('utf8'))) print(' - actual groups :', sorted(gname(gid) for gid in user.get('groups'))) - print(' - expected groups:', sorted(gname(gid) for gid in groups)) + print(' - expected groups:', sorted(gname(gid) for gid in expected_groups)) print(' - will grant :', sorted(gname(gid) for gid in grant_groups)) - print(' - might revoke :', sorted(gname(gid) for gid in revoke_groups)) + + if do_revoke_groups: + label = 'WILL REVOKE ' + else: + label = 'could revoke' + print(' - %s :' % label, sorted(gname(gid) for gid in revoke_groups)) if grant_groups and revoke_groups: print(' ------ CAREFUL this one has BOTH grant AND revoke -----') # Determine which changes we'll apply + final_groups = current_groups.union(grant_groups) if do_revoke_groups: - final_groups = groups - else: - final_groups = current_groups.union(grant_groups) + final_groups.difference_update(revoke_groups) print(' - final groups :', sorted(gname(gid) for gid in final_groups)) # Perform the actual update users_coll.update_one({'_id': user['_id']}, {'$set': {'groups': list(final_groups)}}) + else: + ok_users += 1 - print('%i bad and %i ok user/role combos seen.' % (bad_users, ok_users)) + print('%i bad and %i ok users seen.' % (bad_users, ok_users)) @manager.command