From d7825c6720871be178126b9f500d2c10087250d9 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 23 Feb 2017 14:41:27 -0500 Subject: [PATCH] Add group iteration and syncing support to Keystone auth --- data/model/team.py | 3 +- data/users/keystone.py | 51 ++++++++++++++++++++-- data/users/test/test_teamsync.py | 27 ++++++++---- initdb.py | 3 +- static/partials/team-view.html | 7 ++++ test/test_keystone_auth.py | 72 ++++++++++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 15 deletions(-) diff --git a/data/model/team.py b/data/model/team.py index f314a4c22..2da96df55 100644 --- a/data/model/team.py +++ b/data/model/team.py @@ -422,7 +422,8 @@ def list_team_robots(team): def set_team_syncing(team, login_service_name, config): """ Sets the given team to sync to the given service using the given config. """ login_service = LoginService.get(name=login_service_name) - TeamSync.create(team=team, transaction_id='', service=login_service, config=json.dumps(config)) + return TeamSync.create(team=team, transaction_id='', service=login_service, + config=json.dumps(config)) def remove_team_syncing(orgname, teamname): diff --git a/data/users/keystone.py b/data/users/keystone.py index b3e7ad442..f01b9f4b7 100644 --- a/data/users/keystone.py +++ b/data/users/keystone.py @@ -5,6 +5,7 @@ from keystoneclient.v2_0 import client as kclient from keystoneclient.v3 import client as kv3client from keystoneclient.exceptions import AuthorizationFailure as KeystoneAuthorizationFailure from keystoneclient.exceptions import Unauthorized as KeystoneUnauthorized +from keystoneclient.exceptions import NotFound as KeystoneNotFound from data.users.federated import FederatedUsers, UserInformation from util.itertoolrecipes import take @@ -83,6 +84,11 @@ class KeystoneV3Users(FederatedUsers): self.debug = os.environ.get('USERS_DEBUG') == '1' self.requires_email = requires_email + def _get_admin_client(self): + return kv3client.Client(username=self.admin_username, password=self.admin_password, + tenant_name=self.admin_tenant, auth_url=self.auth_url, + timeout=self.timeout, debug=self.debug) + def verify_credentials(self, username_or_email, password): try: keystone_client = kv3client.Client(username=username_or_email, password=password, @@ -116,6 +122,46 @@ class KeystoneV3Users(FederatedUsers): return (user, None) + def check_group_lookup_args(self, group_lookup_args): + if not group_lookup_args.get('group_id'): + return (False, 'Missing group_id') + + group_id = group_lookup_args['group_id'] + return self._check_group(group_id) + + def _check_group(self, group_id): + try: + return (bool(self._get_admin_client().groups.get(group_id)), None) + except KeystoneNotFound: + return (False, 'Group not found') + except KeystoneAuthorizationFailure as kaf: + logger.exception('Keystone auth failure for admin user for group lookup %s', group_id) + return (False, kaf.message or 'Invalid admin username or password') + except KeystoneUnauthorized as kut: + logger.exception('Keystone unauthorized for admin user for group lookup %s', group_id) + return (False, kut.message or 'Invalid admin username or password') + + def iterate_group_members(self, group_lookup_args, page_size=None, disable_pagination=False): + group_id = group_lookup_args['group_id'] + + (status, err) = self._check_group(group_id) + if not status: + return (None, err) + + try: + group_member_iterator = self._get_admin_client().users.list(group=group_id) + def iterator(): + for user in group_member_iterator: + yield (self._user_info(user), None) + + return (iterator(), None) + except KeystoneAuthorizationFailure as kaf: + logger.exception('Keystone auth failure for admin user for group lookup %s', group_id) + return (False, kaf.message or 'Invalid admin username or password') + except KeystoneUnauthorized as kut: + logger.exception('Keystone unauthorized for admin user for group lookup %s', group_id) + return (False, kut.message or 'Invalid admin username or password') + @staticmethod def _user_info(user): email = user.email if hasattr(user, 'email') else None @@ -126,10 +172,7 @@ class KeystoneV3Users(FederatedUsers): return ([], self.federated_service, None) try: - keystone_client = kv3client.Client(username=self.admin_username, password=self.admin_password, - tenant_name=self.admin_tenant, auth_url=self.auth_url, - timeout=self.timeout, debug=self.debug) - found_users = list(take(limit, keystone_client.users.list(name=query))) + found_users = list(take(limit, self._get_admin_client().users.list(name=query))) logger.debug('For Keystone query %s found users: %s', query, found_users) if not found_users: return ([], self.federated_service, None) diff --git a/data/users/test/test_teamsync.py b/data/users/test/test_teamsync.py index a3172f010..5cb4fcda8 100644 --- a/data/users/test/test_teamsync.py +++ b/data/users/test/test_teamsync.py @@ -7,6 +7,7 @@ from data.users.federated import FederatedUsers, UserInformation from data.users.teamsync import sync_team, sync_teams_to_groups from test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file from test.test_ldap import mock_ldap +from test.test_keystone_auth import fake_keystone from util.names import parse_robot_username _FAKE_AUTH = 'fake' @@ -213,20 +214,28 @@ def test_sync_teams_to_groups(app): assert third_sync_info.last_updated == updated_sync_info.last_updated assert third_sync_info.transaction_id == updated_sync_info.transaction_id - # Set the stale threshold to -1 seconds, and ensure the team is resynced. - sync_teams_to_groups(fake_auth, timedelta(seconds=-1)) + # Set the stale threshold to -10 seconds, and ensure the team is resynced. + sync_teams_to_groups(fake_auth, timedelta(seconds=-120)) fourth_sync_info = model.team.get_team_sync_information('buynlarge', 'synced') assert fourth_sync_info.transaction_id != updated_sync_info.transaction_id -@pytest.mark.parametrize('auth_system_builder', [ - mock_ldap, +@pytest.mark.parametrize('auth_system_builder,config', [ + (mock_ldap, {'group_dn': 'cn=AwesomeFolk'}), + (fake_keystone, {'group_id': 'somegroupid'}), ]) -def test_teamsync_end_to_end(auth_system_builder, app): - # Assert the team has not yet been updated. - sync_team_info = model.team.get_team_sync_information('buynlarge', 'synced') - assert sync_team_info.last_updated is None - +def test_teamsync_end_to_end(auth_system_builder, config, app): with auth_system_builder() as auth: + # Create an new team to sync. + org = model.organization.get_organization('buynlarge') + new_synced_team = model.team.create_team('synced2', org, 'member', 'Some synced team.') + sync_team_info = model.team.set_team_syncing(new_synced_team, auth.federated_service, config) + + # Sync the team. assert sync_team(auth, sync_team_info) + + # Ensure we now have members. + msg = 'Auth system: %s' % auth.federated_service + sync_team_info = model.team.get_team_sync_information('buynlarge', 'synced2') + assert len(list(model.team.list_team_users(sync_team_info.team))) > 0, msg diff --git a/initdb.py b/initdb.py index d13b0a058..a7e278823 100644 --- a/initdb.py +++ b/initdb.py @@ -709,8 +709,9 @@ def populate_database(minimal=False, with_storage=False): model.team.add_user_to_team(new_user_4, sell_owners) + sync_config = {'group_dn': 'cn=Test-Group,ou=Users', 'group_id': 'somegroupid'} synced_team = model.team.create_team('synced', org, 'member', 'Some synced team.') - model.team.set_team_syncing(synced_team, 'ldap', {'group_dn': 'cn=Test-Group,ou=Users'}) + model.team.set_team_syncing(synced_team, 'ldap', sync_config) another_synced_team = model.team.create_team('synced', thirdorg, 'member', 'Some synced team.') model.team.set_team_syncing(another_synced_team, 'ldap', {'group_dn': 'cn=Test-Group,ou=Users'}) diff --git a/static/partials/team-view.html b/static/partials/team-view.html index 6da8e07c3..b046f8d2b 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -39,6 +39,9 @@
{{ syncInfo.config.group_dn }}
+
+ {{ syncInfo.config.group_id }} +
@@ -174,6 +177,10 @@ Enter the distinguished name of the group, relative to {{ enableSyncingInfo.service_info.base_dn }}: +
+ Enter the Keystone group ID: + +
diff --git a/test/test_keystone_auth.py b/test/test_keystone_auth.py index a7ec985f0..4d70a4c1b 100644 --- a/test/test_keystone_auth.py +++ b/test/test_keystone_auth.py @@ -47,6 +47,23 @@ def _create_app(requires_email=True): {'username': 'some.neat.user', 'name': 'Neat User', 'password': 'foobar'}, ] + groups = [ + {'id': 'somegroupid', 'name': 'somegroup', 'description': 'Hi there!', + 'members': ['adminuser', 'cool.user']}, + ] + + def _get_user(username): + for user in users: + if user['username'] == username: + user_data = {} + user_data['id'] = username + user_data['name'] = username + if requires_email: + user_data['email'] = username + '@example.com' + return user_data + + return None + ks_app = Flask('testks') ks_app.config['SERVER_HOSTNAME'] = 'localhost:%s' % _PORT_NUMBER if os.environ.get('DEBUG') == 'true': @@ -66,6 +83,35 @@ def _create_app(requires_email=True): abort(404) + @ks_app.route('/v3/identity/groups//users', methods=['GET']) + def getv3groupmembers(groupid): + for group in groups: + if group['id'] == groupid: + group_data = { + "links": {}, + "users": [_get_user(username) for username in group['members']], + } + + return json.dumps(group_data) + + abort(404) + + @ks_app.route('/v3/identity/groups/', methods=['GET']) + def getv3group(groupid): + for group in groups: + if group['id'] == groupid: + group_data = { + "description": group['description'], + "domain_id": "default", + "id": groupid, + "links": {}, + "name": group['name'], + } + + return json.dumps({'group': group_data}) + + abort(404) + @ks_app.route('/v3/identity/users/', methods=['GET']) def getv3user(userid): for user in users: @@ -321,6 +367,32 @@ class KeystoneV3AuthTests(KeystoneAuthTestsMixin, unittest.TestCase): self.assertIsNotNone(result) self.assertEquals('cool_user', result.username) + def test_check_group_lookup_args(self): + with self.fake_keystone() as keystone: + (status, err) = keystone.check_group_lookup_args({}) + self.assertFalse(status) + self.assertEquals('Missing group_id', err) + + (status, err) = keystone.check_group_lookup_args({'group_id': 'unknownid'}) + self.assertFalse(status) + self.assertEquals('Group not found', err) + + (status, err) = keystone.check_group_lookup_args({'group_id': 'somegroupid'}) + self.assertTrue(status) + self.assertIsNone(err) + + def test_iterate_group_members(self): + with self.fake_keystone() as keystone: + (itt, err) = keystone.iterate_group_members({'group_id': 'somegroupid'}) + self.assertIsNone(err) + + results = list(itt) + results.sort() + + self.assertEquals(2, len(results)) + self.assertEquals('adminuser', results[0][0].id) + self.assertEquals('cool.user', results[1][0].id) + if __name__ == '__main__': unittest.main()