summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-04-10 11:02:36 +0000
committerGerrit Code Review <review@openstack.org>2020-04-10 11:02:36 +0000
commitea1b2b0a65e26e11b705b8d25bac474ee0ac7ac9 (patch)
tree7ad169cde97e7972748ffa56a51492c84f372642
parentffc235845ad47586a4e4a06b5a50c81eddda40a3 (diff)
parent1627c28282c832f4fb837e2c445ed74d0ad3c68b (diff)
downloadkeystone-ea1b2b0a65e26e11b705b8d25bac474ee0ac7ac9.tar.gz
Merge "Add federated support for creating a user"
-rw-r--r--keystone/identity/core.py53
-rw-r--r--keystone/identity/schema.py24
-rw-r--r--keystone/identity/shadow_backends/base.py8
-rw-r--r--keystone/identity/shadow_backends/sql.py6
-rw-r--r--keystone/tests/unit/test_shadow_users.py61
-rw-r--r--keystone/tests/unit/test_v3_identity.py45
6 files changed, 196 insertions, 1 deletions
diff --git a/keystone/identity/core.py b/keystone/identity/core.py
index 00dffcabf..16ef30cf8 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 cdb66f524..e39c376db 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 3e7f32189..12e7f545f 100644
--- a/keystone/identity/shadow_backends/base.py
+++ b/keystone/identity/shadow_backends/base.py
@@ -51,6 +51,14 @@ 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 213376a5b..513f7c50d 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 79f8ba53c..2cbfe4629 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 2a22ce7fc..8441f49db 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)