diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 22fcb5fff7..82e8864822 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -924,6 +924,57 @@ class Manager(manager.Manager): # backward compatible pass + def _validate_federated_objects(self, fed_obj_list): + # Validate that the ipd and protocols exist + for fed_obj in fed_obj_list: + try: + self.federation_api.get_idp(fed_obj['idp_id']) + except exception.IdentityProviderNotFound: + msg = (_("Could not find Identity Provider: %s") + % fed_obj['idp_id']) + raise exception.ValidationError(msg) + for protocol in fed_obj['protocols']: + try: + self.federation_api.get_protocol(fed_obj['idp_id'], + protocol['protocol_id']) + except exception.FederatedProtocolNotFound: + msg = (_("Could not find federated protocol " + "%(protocol)s for Identity Provider: %(idp)s.") + % {'protocol': protocol['protocol_id'], + 'idp': fed_obj['idp_id']}) + raise exception.ValidationError(msg) + + def _create_federated_objects(self, user_ref, fed_obj_list): + for fed_obj in fed_obj_list: + for protocols in fed_obj['protocols']: + federated_dict = { + 'user_id': user_ref['id'], + 'idp_id': fed_obj['idp_id'], + 'protocol_id': protocols['protocol_id'], + 'unique_id': protocols['unique_id'], + 'display_name': user_ref['name'] + } + self.shadow_users_api.create_federated_object( + federated_dict) + + def _create_user_with_federated_objects(self, user, driver): + # If the user did not pass a federated object along inside the user + # object then we simply create the user as normal. + if not user.get('federated'): + if 'federated' in user: + del user['federated'] + user = driver.create_user(user['id'], user) + return user + # Otherwise, validate the federated object and create the user. + else: + user_ref = user.copy() + del user['federated'] + self._validate_federated_objects(user_ref['federated']) + user = driver.create_user(user['id'], user) + self._create_federated_objects(user_ref, user_ref['federated']) + user['federated'] = user_ref['federated'] + return user + @domains_configured @exception_translated('user') def create_user(self, user_ref, initiator=None): @@ -946,7 +997,7 @@ class Manager(manager.Manager): # the underlying driver so that it could conform to rules set down by # that particular driver type. user['id'] = uuid.uuid4().hex - ref = driver.create_user(user['id'], user) + ref = self._create_user_with_federated_objects(user, driver) notifications.Audit.created(self._USER, user['id'], initiator) return self._set_domain_id_and_mapping( ref, domain_id, driver, mapping.EntityType.USER) diff --git a/keystone/identity/schema.py b/keystone/identity/schema.py index cdb66f5241..e39c376dbe 100644 --- a/keystone/identity/schema.py +++ b/keystone/identity/schema.py @@ -33,6 +33,30 @@ _user_properties = { 'description': validation.nullable(parameter_types.description), 'domain_id': parameter_types.id_string, 'enabled': parameter_types.boolean, + 'federated': { + 'type': 'array', + 'items': + { + 'type': 'object', + 'properties': { + 'idp_id': {'type': 'string'}, + 'protocols': { + 'type': 'array', + 'items': + { + 'type': 'object', + 'properties': { + 'protocol_id': {'type': 'string'}, + 'unique_id': {'type': 'string'} + }, + 'required': ['protocol_id', 'unique_id'] + }, + 'minItems': 1 + } + }, + 'required': ['idp_id', 'protocols'] + }, + }, 'name': _identity_name, 'password': { 'type': ['string', 'null'] diff --git a/keystone/identity/shadow_backends/base.py b/keystone/identity/shadow_backends/base.py index 3e7f321896..12e7f545f1 100644 --- a/keystone/identity/shadow_backends/base.py +++ b/keystone/identity/shadow_backends/base.py @@ -50,6 +50,14 @@ def federated_objects_to_list(fed_ref): class ShadowUsersDriverBase(object, metaclass=abc.ABCMeta): """Interface description for an Shadow Users driver.""" + @abc.abstractmethod + def create_federated_object(self, fed_dict): + """Create a new federated object. + + :param dict federated_dict: Reference to the federated user + """ + raise exception.NotImplemented() + @abc.abstractmethod def create_federated_user(self, domain_id, federated_dict, email=None): """Create a new user with the federated identity. diff --git a/keystone/identity/shadow_backends/sql.py b/keystone/identity/shadow_backends/sql.py index d739f5756a..d45d325a58 100644 --- a/keystone/identity/shadow_backends/sql.py +++ b/keystone/identity/shadow_backends/sql.py @@ -54,6 +54,12 @@ class ShadowUsers(base.ShadowUsersDriverBase): session.add(user_ref) return identity_base.filter_user(user_ref.to_dict()) + @sql.handle_conflicts(conflict_type='federated_user') + def create_federated_object(self, fed_dict): + with sql.session_for_write() as session: + fed_ref = model.FederatedUser.from_dict(fed_dict) + session.add(fed_ref) + def get_federated_objects(self, user_id): with sql.session_for_read() as session: query = session.query(model.FederatedUser) diff --git a/keystone/tests/unit/test_shadow_users.py b/keystone/tests/unit/test_shadow_users.py index 79f8ba53cf..2cbfe4629d 100644 --- a/keystone/tests/unit/test_shadow_users.py +++ b/keystone/tests/unit/test_shadow_users.py @@ -13,6 +13,7 @@ import uuid from keystone.common import provider_api +from keystone import exception from keystone.tests import unit from keystone.tests.unit import default_fixtures from keystone.tests.unit.identity.shadow_users import test_backend @@ -88,3 +89,63 @@ class TestUserWithFederatedUser(ShadowUsersTests): self.assertEqual(1, len(user_ref['federated'])) self.assertFederatedDictsEqual(fed_dict, user_ref['federated'][0]) + + def test_create_user_with_invalid_idp_and_protocol_fails(self): + baduser = unit.new_user_ref(domain_id=self.domain_id) + baduser['federated'] = [ + { + 'idp_id': 'fakeidp', + 'protocols': [ + { + 'protocol_id': 'nonexistent', + 'unique_id': 'unknown' + } + ] + } + ] + # Check validation works by throwing a federated object with + # invalid idp_id, protocol_id inside the user passed to create_user. + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + baduser) + + baduser['federated'][0]['idp_id'] = self.idp['id'] + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + baduser) + + def test_create_user_with_federated_attributes(self): + # Create the schema of a federated attribute being passed in with a + # user. + user = unit.new_user_ref(domain_id=self.domain_id) + unique_id = uuid.uuid4().hex + user['federated'] = [ + { + 'idp_id': self.idp['id'], + 'protocols': [ + { + 'protocol_id': self.protocol['id'], + 'unique_id': unique_id + } + ] + } + ] + + # Test that there are no current federated_users that match our users + # federated object and create the user + self.assertRaises(exception.UserNotFound, + self.shadow_users_api.get_federated_user, + self.idp['id'], + self.protocol['id'], + unique_id) + + ref = self.identity_api.create_user(user) + + # Test that the user and federated object now exists + self.assertEqual(user['name'], ref['name']) + self.assertEqual(user['federated'], ref['federated']) + fed_user = self.shadow_users_api.get_federated_user( + self.idp['id'], + self.protocol['id'], + unique_id) + self.assertIsNotNone(fed_user) diff --git a/keystone/tests/unit/test_v3_identity.py b/keystone/tests/unit/test_v3_identity.py index 2a22ce7fc2..8441f49dbe 100644 --- a/keystone/tests/unit/test_v3_identity.py +++ b/keystone/tests/unit/test_v3_identity.py @@ -1177,3 +1177,48 @@ class UserFederatedAttributesTests(test_v3.RestfulTestCase): self.assertIn('unique_id', user['federated'][0]['protocols'][0]) r = self.get('/users/%(user_id)s' % {'user_id': user['id']}) self.assertValidUserResponse(r, user) + + def test_create_user_with_federated_attributes(self): + """Call ``POST /users``.""" + idp, protocol = self._create_federated_attributes() + ref = unit.new_user_ref(domain_id=self.domain_id) + ref['federated'] = [ + { + 'idp_id': idp['id'], + 'protocols': [ + { + 'protocol_id': protocol['id'], + 'unique_id': uuid.uuid4().hex + } + ] + } + ] + r = self.post( + '/users', + body={'user': ref}) + user = r.result['user'] + self.assertEqual(user['name'], ref['name']) + self.assertEqual(user['federated'], ref['federated']) + self.assertValidUserResponse(r, ref) + + def test_create_user_fails_when_given_invalid_idp_and_protocols(self): + """Call ``POST /users`` with invalid idp and protocol to fail.""" + idp, protocol = self._create_federated_attributes() + ref = unit.new_user_ref(domain_id=self.domain_id) + ref['federated'] = [ + { + 'idp_id': 'fakeidp', + 'protocols': [ + { + 'protocol_id': 'fakeprotocol_id', + 'unique_id': uuid.uuid4().hex + } + ] + } + ] + + self.post('/users', body={'user': ref}, token=self.get_admin_token(), + expected_status=http.client.BAD_REQUEST) + ref['federated'][0]['idp_id'] = idp['id'] + self.post('/users', body={'user': ref}, token=self.get_admin_token(), + expected_status=http.client.BAD_REQUEST)