Add group iteration and syncing support to Keystone auth

This commit is contained in:
Joseph Schorr 2017-02-23 14:41:27 -05:00
parent 47278cc559
commit d7825c6720
6 changed files with 148 additions and 15 deletions

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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'})

View file

@ -39,6 +39,9 @@
<div ng-if="syncInfo.service == 'ldap'">
<code>{{ syncInfo.config.group_dn }}</code>
</div>
<div ng-if="syncInfo.service == 'keystone'">
<code>{{ syncInfo.config.group_id }}</code>
</div>
</td>
</tr>
<tr>
@ -174,6 +177,10 @@
Enter the distinguished name of the group, relative to <code>{{ enableSyncingInfo.service_info.base_dn }}</code>:
<input type="text" class="form-control" placeholder="Group DN" ng-model="enableSyncingInfo.config.group_dn" required>
</div>
<div ng-switch-when="keystone">
Enter the Keystone group ID:
<input type="text" class="form-control" placeholder="Group ID" ng-model="enableSyncingInfo.config.group_id" required>
</div>
</div>
</form>
</div>

View file

@ -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/<groupid>/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/<groupid>', 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/<userid>', 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()