Add group iteration and syncing support to Keystone auth
This commit is contained in:
parent
47278cc559
commit
d7825c6720
6 changed files with 148 additions and 15 deletions
|
@ -422,7 +422,8 @@ def list_team_robots(team):
|
||||||
def set_team_syncing(team, login_service_name, config):
|
def set_team_syncing(team, login_service_name, config):
|
||||||
""" Sets the given team to sync to the given service using the given config. """
|
""" Sets the given team to sync to the given service using the given config. """
|
||||||
login_service = LoginService.get(name=login_service_name)
|
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):
|
def remove_team_syncing(orgname, teamname):
|
||||||
|
|
|
@ -5,6 +5,7 @@ from keystoneclient.v2_0 import client as kclient
|
||||||
from keystoneclient.v3 import client as kv3client
|
from keystoneclient.v3 import client as kv3client
|
||||||
from keystoneclient.exceptions import AuthorizationFailure as KeystoneAuthorizationFailure
|
from keystoneclient.exceptions import AuthorizationFailure as KeystoneAuthorizationFailure
|
||||||
from keystoneclient.exceptions import Unauthorized as KeystoneUnauthorized
|
from keystoneclient.exceptions import Unauthorized as KeystoneUnauthorized
|
||||||
|
from keystoneclient.exceptions import NotFound as KeystoneNotFound
|
||||||
from data.users.federated import FederatedUsers, UserInformation
|
from data.users.federated import FederatedUsers, UserInformation
|
||||||
from util.itertoolrecipes import take
|
from util.itertoolrecipes import take
|
||||||
|
|
||||||
|
@ -83,6 +84,11 @@ class KeystoneV3Users(FederatedUsers):
|
||||||
self.debug = os.environ.get('USERS_DEBUG') == '1'
|
self.debug = os.environ.get('USERS_DEBUG') == '1'
|
||||||
self.requires_email = requires_email
|
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):
|
def verify_credentials(self, username_or_email, password):
|
||||||
try:
|
try:
|
||||||
keystone_client = kv3client.Client(username=username_or_email, password=password,
|
keystone_client = kv3client.Client(username=username_or_email, password=password,
|
||||||
|
@ -116,6 +122,46 @@ class KeystoneV3Users(FederatedUsers):
|
||||||
|
|
||||||
return (user, None)
|
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
|
@staticmethod
|
||||||
def _user_info(user):
|
def _user_info(user):
|
||||||
email = user.email if hasattr(user, 'email') else None
|
email = user.email if hasattr(user, 'email') else None
|
||||||
|
@ -126,10 +172,7 @@ class KeystoneV3Users(FederatedUsers):
|
||||||
return ([], self.federated_service, None)
|
return ([], self.federated_service, None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
keystone_client = kv3client.Client(username=self.admin_username, password=self.admin_password,
|
found_users = list(take(limit, self._get_admin_client().users.list(name=query)))
|
||||||
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)))
|
|
||||||
logger.debug('For Keystone query %s found users: %s', query, found_users)
|
logger.debug('For Keystone query %s found users: %s', query, found_users)
|
||||||
if not found_users:
|
if not found_users:
|
||||||
return ([], self.federated_service, None)
|
return ([], self.federated_service, None)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from data.users.federated import FederatedUsers, UserInformation
|
||||||
from data.users.teamsync import sync_team, sync_teams_to_groups
|
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.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
|
||||||
from test.test_ldap import mock_ldap
|
from test.test_ldap import mock_ldap
|
||||||
|
from test.test_keystone_auth import fake_keystone
|
||||||
from util.names import parse_robot_username
|
from util.names import parse_robot_username
|
||||||
|
|
||||||
_FAKE_AUTH = 'fake'
|
_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.last_updated == updated_sync_info.last_updated
|
||||||
assert third_sync_info.transaction_id == updated_sync_info.transaction_id
|
assert third_sync_info.transaction_id == updated_sync_info.transaction_id
|
||||||
|
|
||||||
# Set the stale threshold to -1 seconds, and ensure the team is resynced.
|
# Set the stale threshold to -10 seconds, and ensure the team is resynced.
|
||||||
sync_teams_to_groups(fake_auth, timedelta(seconds=-1))
|
sync_teams_to_groups(fake_auth, timedelta(seconds=-120))
|
||||||
|
|
||||||
fourth_sync_info = model.team.get_team_sync_information('buynlarge', 'synced')
|
fourth_sync_info = model.team.get_team_sync_information('buynlarge', 'synced')
|
||||||
assert fourth_sync_info.transaction_id != updated_sync_info.transaction_id
|
assert fourth_sync_info.transaction_id != updated_sync_info.transaction_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('auth_system_builder', [
|
@pytest.mark.parametrize('auth_system_builder,config', [
|
||||||
mock_ldap,
|
(mock_ldap, {'group_dn': 'cn=AwesomeFolk'}),
|
||||||
|
(fake_keystone, {'group_id': 'somegroupid'}),
|
||||||
])
|
])
|
||||||
def test_teamsync_end_to_end(auth_system_builder, app):
|
def test_teamsync_end_to_end(auth_system_builder, config, 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
|
|
||||||
|
|
||||||
with auth_system_builder() as auth:
|
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)
|
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
|
||||||
|
|
|
@ -709,8 +709,9 @@ def populate_database(minimal=False, with_storage=False):
|
||||||
|
|
||||||
model.team.add_user_to_team(new_user_4, sell_owners)
|
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.')
|
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.')
|
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'})
|
model.team.set_team_syncing(another_synced_team, 'ldap', {'group_dn': 'cn=Test-Group,ou=Users'})
|
||||||
|
|
|
@ -39,6 +39,9 @@
|
||||||
<div ng-if="syncInfo.service == 'ldap'">
|
<div ng-if="syncInfo.service == 'ldap'">
|
||||||
<code>{{ syncInfo.config.group_dn }}</code>
|
<code>{{ syncInfo.config.group_dn }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="syncInfo.service == 'keystone'">
|
||||||
|
<code>{{ syncInfo.config.group_id }}</code>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -174,6 +177,10 @@
|
||||||
Enter the distinguished name of the group, relative to <code>{{ enableSyncingInfo.service_info.base_dn }}</code>:
|
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>
|
<input type="text" class="form-control" placeholder="Group DN" ng-model="enableSyncingInfo.config.group_dn" required>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,6 +47,23 @@ def _create_app(requires_email=True):
|
||||||
{'username': 'some.neat.user', 'name': 'Neat User', 'password': 'foobar'},
|
{'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 = Flask('testks')
|
||||||
ks_app.config['SERVER_HOSTNAME'] = 'localhost:%s' % _PORT_NUMBER
|
ks_app.config['SERVER_HOSTNAME'] = 'localhost:%s' % _PORT_NUMBER
|
||||||
if os.environ.get('DEBUG') == 'true':
|
if os.environ.get('DEBUG') == 'true':
|
||||||
|
@ -66,6 +83,35 @@ def _create_app(requires_email=True):
|
||||||
|
|
||||||
abort(404)
|
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'])
|
@ks_app.route('/v3/identity/users/<userid>', methods=['GET'])
|
||||||
def getv3user(userid):
|
def getv3user(userid):
|
||||||
for user in users:
|
for user in users:
|
||||||
|
@ -321,6 +367,32 @@ class KeystoneV3AuthTests(KeystoneAuthTestsMixin, unittest.TestCase):
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('cool_user', result.username)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Reference in a new issue