Add federated support for creating a user
This patch adds functionality to allow an operator to pass in a federated attribute when creating a user. When a user is created the federated objects in the federated attribute will be created and associated along with the user. Co-Authored-By: Kristi Nikolla <knikolla@bu.edu> Partial-Bug: 1816076 Change-Id: I6db03af81099a7509635881f05adf5a7257466a7
This commit is contained in:
parent
652f02c8b5
commit
1627c28282
|
@ -924,6 +924,57 @@ class Manager(manager.Manager):
|
||||||
# backward compatible
|
# backward compatible
|
||||||
pass
|
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
|
@domains_configured
|
||||||
@exception_translated('user')
|
@exception_translated('user')
|
||||||
def create_user(self, user_ref, initiator=None):
|
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
|
# the underlying driver so that it could conform to rules set down by
|
||||||
# that particular driver type.
|
# that particular driver type.
|
||||||
user['id'] = uuid.uuid4().hex
|
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)
|
notifications.Audit.created(self._USER, user['id'], initiator)
|
||||||
return self._set_domain_id_and_mapping(
|
return self._set_domain_id_and_mapping(
|
||||||
ref, domain_id, driver, mapping.EntityType.USER)
|
ref, domain_id, driver, mapping.EntityType.USER)
|
||||||
|
|
|
@ -33,6 +33,30 @@ _user_properties = {
|
||||||
'description': validation.nullable(parameter_types.description),
|
'description': validation.nullable(parameter_types.description),
|
||||||
'domain_id': parameter_types.id_string,
|
'domain_id': parameter_types.id_string,
|
||||||
'enabled': parameter_types.boolean,
|
'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,
|
'name': _identity_name,
|
||||||
'password': {
|
'password': {
|
||||||
'type': ['string', 'null']
|
'type': ['string', 'null']
|
||||||
|
|
|
@ -50,6 +50,14 @@ def federated_objects_to_list(fed_ref):
|
||||||
class ShadowUsersDriverBase(object, metaclass=abc.ABCMeta):
|
class ShadowUsersDriverBase(object, metaclass=abc.ABCMeta):
|
||||||
"""Interface description for an Shadow Users driver."""
|
"""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
|
@abc.abstractmethod
|
||||||
def create_federated_user(self, domain_id, federated_dict, email=None):
|
def create_federated_user(self, domain_id, federated_dict, email=None):
|
||||||
"""Create a new user with the federated identity.
|
"""Create a new user with the federated identity.
|
||||||
|
|
|
@ -54,6 +54,12 @@ class ShadowUsers(base.ShadowUsersDriverBase):
|
||||||
session.add(user_ref)
|
session.add(user_ref)
|
||||||
return identity_base.filter_user(user_ref.to_dict())
|
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):
|
def get_federated_objects(self, user_id):
|
||||||
with sql.session_for_read() as session:
|
with sql.session_for_read() as session:
|
||||||
query = session.query(model.FederatedUser)
|
query = session.query(model.FederatedUser)
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from keystone.common import provider_api
|
from keystone.common import provider_api
|
||||||
|
from keystone import exception
|
||||||
from keystone.tests import unit
|
from keystone.tests import unit
|
||||||
from keystone.tests.unit import default_fixtures
|
from keystone.tests.unit import default_fixtures
|
||||||
from keystone.tests.unit.identity.shadow_users import test_backend
|
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.assertEqual(1, len(user_ref['federated']))
|
||||||
|
|
||||||
self.assertFederatedDictsEqual(fed_dict, user_ref['federated'][0])
|
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)
|
||||||
|
|
|
@ -1177,3 +1177,48 @@ class UserFederatedAttributesTests(test_v3.RestfulTestCase):
|
||||||
self.assertIn('unique_id', user['federated'][0]['protocols'][0])
|
self.assertIn('unique_id', user['federated'][0]['protocols'][0])
|
||||||
r = self.get('/users/%(user_id)s' % {'user_id': user['id']})
|
r = self.get('/users/%(user_id)s' % {'user_id': user['id']})
|
||||||
self.assertValidUserResponse(r, user)
|
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)
|
||||||
|
|
Loading…
Reference in New Issue