This repository has been archived by the owner on Dec 15, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Grouper integration with user admin status
Summary: Historically, making/revoking an admin requires going through the Changes UI. We want to do this on Grouper so that we can say things like, "All members of Team A should be an admin". This diff creates a recurring task every 1 minute that connects to Grouper, gets the list of admin users, and makes sure that those users and only those users have admin access. This also updates the web UI to remove the "Make Admin" button from the users page. The API endpoint that can manipulate the admin status of the user is purposely kept for compatability reasons. It is not a serious risk, as the Grouper sync will override any changes every 1 minute. Test Plan: unit test, manual testing on the VM screenshot of UI change: {F492038} Reviewers: anupc Reviewed By: anupc Subscribers: changesbot, herb, kylec Tags: #changes_ui Differential Revision: https://tails.corp.dropbox.com/D219947
- Loading branch information
Naphat Sanguansin
committed
Aug 18, 2016
1 parent
43a64c9
commit 206b30b
Showing
7 changed files
with
214 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import logging | ||
|
||
from flask import current_app | ||
from groupy.client import Groupy | ||
from typing import Iterable, Set # NOQA | ||
|
||
from changes.config import db, statsreporter | ||
from changes.models.user import User | ||
|
||
|
||
logger = logging.getLogger('grouper.sync') | ||
|
||
|
||
def sync_grouper(): | ||
# type: () -> None | ||
"""This function is meant as a Celery task. It connects to Grouper, gets | ||
all users who should be admin, and makes sure that those users and only | ||
those users are admin. | ||
""" | ||
try: | ||
admin_emails = _get_admin_emails_from_grouper() | ||
_sync_admin_users(admin_emails) | ||
except Exception: | ||
logger.exception("An error occurred during Grouper sync.") | ||
statsreporter.stats().set_gauge('grouper_sync_error', 1) | ||
raise | ||
else: | ||
statsreporter.stats().set_gauge('grouper_sync_error', 0) | ||
|
||
|
||
def _get_admin_emails_from_grouper(): | ||
# type: () -> Set[str] | ||
"""This function connects to Grouper and retrieves the list of emails of | ||
users with admin permission. | ||
Returns: | ||
set[basestring]: a set of emails of admin users | ||
""" | ||
grouper_api_url = current_app.config['GROUPER_API_URL'] | ||
grouper_permissions_admin = current_app.config['GROUPER_PERMISSIONS_ADMIN'] | ||
grclient = Groupy(grouper_api_url) | ||
groups = grclient.permissions.get(grouper_permissions_admin).groups | ||
|
||
admin_users = set() | ||
for _, group in groups.iteritems(): | ||
for email, user in group.users.iteritems(): | ||
if user['rolename'] not in current_app.config['GROUPER_EXCLUDED_ROLES']: | ||
admin_users.add(email) | ||
return admin_users | ||
|
||
|
||
def _sync_admin_users(admin_emails): | ||
# type: (Iterable[str]) -> None | ||
"""Take a look at the Changes user database. Every user with email in | ||
`admin_emails` should become a Changes admin, and every user already | ||
an admin whose email is not in `admin_emails` will have their | ||
admin privileges revoked. | ||
Args: | ||
admin_emails (iterable[basestring]): an iterable of usernames of | ||
people who should be admin. | ||
""" | ||
# revoke access for people who should not have admin access | ||
User.query.filter( | ||
~User.email.in_(admin_emails), | ||
User.is_admin.is_(True), | ||
).update({ | ||
'is_admin': False, | ||
}, synchronize_session=False) | ||
|
||
# give access for people who should have access | ||
User.query.filter( | ||
User.email.in_(admin_emails), | ||
User.is_admin.is_(False), | ||
).update({ | ||
'is_admin': True, | ||
}, synchronize_session=False) | ||
db.session.commit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import mock | ||
import pytest | ||
|
||
from flask import current_app | ||
|
||
from changes.config import db | ||
from changes.jobs.sync_grouper import ( | ||
_get_admin_emails_from_grouper, _sync_admin_users, sync_grouper | ||
) | ||
from changes.testutils import TestCase | ||
|
||
|
||
class SyncGrouperAdminTestCase(TestCase): | ||
|
||
def test_get_admin_emails_from_grouper_correct(self): | ||
mock_groupy = mock.MagicMock() | ||
mock_groupy.permissions.get().groups = { | ||
'group1': mock.MagicMock(), | ||
'group2': mock.MagicMock(), | ||
} | ||
mock_groupy.permissions.get().groups['group1'].users = { | ||
'user1@dropbox.com': {'rolename': 'owner'}, | ||
'user2@dropbox.com': {'rolename': 'member'}, | ||
'user3@dropbox.com': {'rolename': 'member'}, | ||
} | ||
mock_groupy.permissions.get().groups['group2'].users = { | ||
'user3@dropbox.com': {'rolename': 'member'}, | ||
'user4@dropbox.com': {'rolename': 'member'}, | ||
} | ||
with mock.patch('changes.jobs.sync_grouper.Groupy') as mock_groupy_init: | ||
mock_groupy_init.return_value = mock_groupy | ||
admin_usernames = _get_admin_emails_from_grouper() | ||
mock_groupy_init.assert_called_once_with( | ||
current_app.config['GROUPER_API_URL']) | ||
|
||
# can't use assert_called_once_with because we called it 3 times when | ||
# setting up the mock object itself | ||
mock_groupy.permissions.get.assert_called_with( | ||
current_app.config['GROUPER_PERMISSIONS_ADMIN']) | ||
assert admin_usernames == set([ | ||
'user1@dropbox.com', | ||
'user2@dropbox.com', | ||
'user3@dropbox.com', | ||
'user4@dropbox.com', | ||
]) | ||
|
||
def test_get_admin_emails_from_grouper_np_owner(self): | ||
mock_groupy = mock.MagicMock() | ||
mock_groupy.permissions.get().groups = { | ||
'group1': mock.MagicMock(), | ||
'group2': mock.MagicMock(), | ||
} | ||
mock_groupy.permissions.get().groups['group1'].users = { | ||
'user1@dropbox.com': {'rolename': 'np-owner'}, | ||
'user2@dropbox.com': {'rolename': 'member'}, | ||
'user3@dropbox.com': {'rolename': 'member'}, | ||
} | ||
mock_groupy.permissions.get().groups['group2'].users = { | ||
'user3@dropbox.com': {'rolename': 'member'}, | ||
'user4@dropbox.com': {'rolename': 'member'}, | ||
} | ||
with mock.patch('changes.jobs.sync_grouper.Groupy') as mock_groupy_init: | ||
mock_groupy_init.return_value = mock_groupy | ||
admin_usernames = _get_admin_emails_from_grouper() | ||
mock_groupy_init.assert_called_once_with( | ||
current_app.config['GROUPER_API_URL']) | ||
mock_groupy.permissions.get.assert_called_with( | ||
current_app.config['GROUPER_PERMISSIONS_ADMIN']) | ||
assert admin_usernames == set([ | ||
'user2@dropbox.com', | ||
'user3@dropbox.com', | ||
'user4@dropbox.com', | ||
]) | ||
|
||
def test_sync_admin_users_correct(self): | ||
admin_user1 = self.create_user( | ||
email='user1@dropbox.com', is_admin=True) | ||
admin_user2 = self.create_user( | ||
email='user2@dropbox.com', is_admin=True) | ||
admin_user3 = self.create_user( | ||
email='user3@dropbox.com', is_admin=True) | ||
|
||
user4 = self.create_user(email='user4@dropbox.com', is_admin=False) | ||
user5 = self.create_user(email='user5@dropbox.com', is_admin=False) | ||
|
||
_sync_admin_users( | ||
set([u'user2@dropbox.com', u'user3@dropbox.com', u'user5@dropbox.com'])) | ||
db.session.expire_all() | ||
|
||
assert admin_user1.is_admin is False | ||
|
||
assert admin_user2.is_admin is True | ||
|
||
assert admin_user3.is_admin is True | ||
|
||
assert user4.is_admin is False | ||
|
||
assert user5.is_admin is True | ||
|
||
def test_sync_grouper_stats_succeeded(self): | ||
with mock.patch('changes.jobs.sync_grouper._get_admin_emails_from_grouper'): | ||
with mock.patch('changes.jobs.sync_grouper._sync_admin_users'): | ||
with mock.patch('changes.jobs.sync_grouper.statsreporter') as mock_statsreporter: | ||
sync_grouper() | ||
mock_statsreporter.stats().set_gauge.assert_called_once_with('grouper_sync_error', 0) | ||
|
||
def test_sync_grouper_stats_failure(self): | ||
with mock.patch('changes.jobs.sync_grouper._get_admin_emails_from_grouper') as mock_call: | ||
with mock.patch('changes.jobs.sync_grouper._sync_admin_users'): | ||
with mock.patch('changes.jobs.sync_grouper.statsreporter') as mock_statsreporter: | ||
mock_call.side_effect = Exception | ||
with pytest.raises(Exception): | ||
sync_grouper() | ||
mock_statsreporter.stats().set_gauge.assert_called_once_with('grouper_sync_error', 1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters