diff --git a/keystone/auth/plugins/mapped.py b/keystone/auth/plugins/mapped.py index 98a2251de2..582cc4fe1f 100644 --- a/keystone/auth/plugins/mapped.py +++ b/keystone/auth/plugins/mapped.py @@ -123,7 +123,8 @@ def handle_unscoped_token(context, auth_payload, auth_context, mapped_properties = apply_mapping_filter(identity_provider, protocol, assertion, assignment_api, federation_api, identity_api) - user_id = setup_username(context, mapped_properties) + user = setup_username(context, mapped_properties) + user_id = user.get('id') group_ids = mapped_properties['group_ids'] except Exception: @@ -189,24 +190,40 @@ def apply_mapping_filter(identity_provider, protocol, assertion, def setup_username(context, mapped_properties): """Setup federated username. - If ``user_name`` is specified in the mapping_properties use this - value.Otherwise try fetching value from an environment variable - ``REMOTE_USER``. - This method also url encodes user_name and saves this value in user_id. - If user_name cannot be mapped raise exception.Unauthorized. + Function covers all the cases for properly setting user id, a primary + identifier for identity objects. Initial version of the mapping engine + assumed user is identified by ``name`` and his ``id`` is built from the + name. We, however need to be able to accept local rules that identify user + by either id or name/domain. + + The following use-cases are covered: + + 1) If neither user_name nor user_id is set raise exception.Unauthorized + 2) If user_id is set and user_name not, set user_name equal to user_id + 3) If user_id is not set and user_name is, set user_id as url safe version + of user_name. :param context: authentication context :param mapped_properties: Properties issued by a RuleProcessor. :type: dictionary :raises: exception.Unauthorized - :returns: tuple with user_name and user_id values. + :returns: dictionary with user identification + :rtype: dict """ - user_name = mapped_properties['name'] - if user_name is None: - user_name = context['environment'].get('REMOTE_USER') - if user_name is None: - raise exception.Unauthorized(_("Could not map user")) - user_id = parse.quote(user_name) - return user_id + user = mapped_properties['user'] + + user_id = user.get('id') + user_name = user.get('name') or context['environment'].get('REMOTE_USER') + + if not any([user_id, user_name]): + raise exception.Unauthorized(_("Could not map user")) + + elif not user_name: + user['name'] = user_id + + elif not user_id: + user['id'] = parse.quote(user_name) + + return user diff --git a/keystone/contrib/federation/utils.py b/keystone/contrib/federation/utils.py index 491641e2e2..c954257903 100644 --- a/keystone/contrib/federation/utils.py +++ b/keystone/contrib/federation/utils.py @@ -20,6 +20,7 @@ from oslo_log import log from oslo_utils import timeutils import six +from keystone.contrib import federation from keystone import exception from keystone.i18n import _, _LW @@ -291,6 +292,11 @@ class RuleProcessor(object): ANY_ONE_OF = 'any_one_of' NOT_ANY_OF = 'not_any_of' + class _UserType(object): + """User mapping type.""" + EPHEMERAL = 'ephemeral' + LOCAL = 'local' + def __init__(self, rules): """Initialize RuleProcessor. @@ -413,8 +419,27 @@ class RuleProcessor(object): for group in {g['name']: g for g in groups}.values(): yield group + def normalize_user(user): + """Parse and validate user mapping.""" + + user_type = user.get('type') + + if user_type and user_type not in (self._UserType.EPHEMERAL, + self._UserType.LOCAL): + msg = _("User type %s not supported") % user_type + raise exception.ValidationError(msg) + + if user_type is None: + user_type = user['type'] = self._UserType.EPHEMERAL + + if user_type == self._UserType.EPHEMERAL: + user['domain'] = { + 'id': (CONF.federation.federated_domain_name or + federation.FEDERATED_DOMAIN_KEYWORD) + } + # initialize the group_ids as a set to eliminate duplicates - user_name = None + user = {} group_ids = set() group_names = list() groups_by_domain = dict() @@ -422,11 +447,10 @@ class RuleProcessor(object): for identity_value in identity_values: if 'user' in identity_value: # if a mapping outputs more than one user name, log it - if user_name is not None: - LOG.warning(_LW('Ignoring user name %s'), - identity_value['user']['name']) + if user: + LOG.warning(_LW('Ignoring user name')) else: - user_name = identity_value['user']['name'] + user = identity_value.get('user') if 'group' in identity_value: group = identity_value['group'] if 'id' in group: @@ -437,7 +461,9 @@ class RuleProcessor(object): groups_by_domain.setdefault(domain, list()).append(group) group_names.extend(extract_groups(groups_by_domain)) - return {'name': user_name, + normalize_user(user) + + return {'user': user, 'group_ids': list(group_ids), 'group_names': group_names} diff --git a/keystone/tests/unit/mapping_fixtures.py b/keystone/tests/unit/mapping_fixtures.py index 81c39c2a84..0b99a7e3d1 100644 --- a/keystone/tests/unit/mapping_fixtures.py +++ b/keystone/tests/unit/mapping_fixtures.py @@ -20,6 +20,8 @@ DEVELOPER_GROUP_ID = "xyz" DEVELOPER_GROUP_NAME = "developer" DEVELOPER_GROUP_DOMAIN_NAME = "outsourcing" DEVELOPER_GROUP_DOMAIN_ID = "5abc43" +FEDERATED_DOMAIN = "Federated" +LOCAL_DOMAIN = "Local" # Mapping summary: @@ -553,6 +555,187 @@ MAPPING_GROUP_NAMES = { ] } +MAPPING_EPHEMERAL_USER = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": FEDERATED_DOMAIN + }, + "type": "ephemeral" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "tbo" + ] + } + ] + } + ] +} + +MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": LOCAL_DOMAIN + }, + "type": "ephemeral" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "jsmith" + ] + } + ] + } + ] +} + +MAPPING_LOCAL_USER_LOCAL_DOMAIN = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": LOCAL_DOMAIN + }, + "type": "local" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "jsmith" + ] + } + ] + } + ] +} + +# Excercise all possibilities of user identitfication. Values are hardcoded on +# purpose. +MAPPING_USER_IDS = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "jsmith" + ] + } + ] + }, + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": "federated" + } + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "tbo" + ] + } + ] + }, + { + "local": [ + { + "user": { + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "bob" + ] + } + ] + }, + { + "local": [ + { + "user": { + "id": "abc123", + "name": "{0}", + "domain": { + "id": "federated" + } + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "bwilliams" + ] + } + ] + } + ] +} + EMPLOYEE_ASSERTION = { 'Email': 'tim@example.com', 'UserName': 'tbo', diff --git a/keystone/tests/unit/test_v3_federation.py b/keystone/tests/unit/test_v3_federation.py index 484698e7fd..790be63fe1 100644 --- a/keystone/tests/unit/test_v3_federation.py +++ b/keystone/tests/unit/test_v3_federation.py @@ -27,6 +27,8 @@ from saml2 import sigver import xmldsig from keystone.auth import controllers as auth_controllers +from keystone.auth.plugins import mapped +from keystone.contrib import federation from keystone.contrib.federation import controllers as federation_controllers from keystone.contrib.federation import idp as keystone_idp from keystone.contrib.federation import utils as mapping_utils @@ -595,6 +597,26 @@ class MappingCRUDTests(FederationTests): class MappingRuleEngineTests(FederationTests): """A class for testing the mapping rule engine.""" + def assertValidMappedUserObject(self, mapped_properties, + user_type='ephemeral', + domain_id=None): + """Check whether mapped properties object has 'user' within. + + According to today's rules, RuleProcessor does not have to issue user's + id or name. What's actually required is user's type and for ephemeral + users that would be service domain named 'Federated'. + """ + self.assertIn('user', mapped_properties, + message='Missing user object in mapped properties') + user = mapped_properties['user'] + self.assertIn('type', user) + self.assertEqual(user_type, user['type']) + self.assertIn('domain', user) + domain = user['domain'] + domain_name_or_id = domain.get('id') or domain.get('name') + domain_ref = domain_id or federation.FEDERATED_DOMAIN_KEYWORD + self.assertEqual(domain_ref, domain_name_or_id) + def test_rule_engine_any_one_of_and_direct_mapping(self): """Should return user's name and group id EMPLOYEE_GROUP_ID. @@ -612,12 +634,11 @@ class MappingRuleEngineTests(FederationTests): fn = assertion.get('FirstName') ln = assertion.get('LastName') full_name = '%s %s' % (fn, ln) - group_ids = values.get('group_ids') - name = values.get('name') + user_name = values.get('user', {}).get('name') self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) - self.assertEqual(name, full_name) + self.assertEqual(full_name, user_name) def test_rule_engine_no_regex_match(self): """Should deny authorization, the email of the tester won't match. @@ -633,7 +654,9 @@ class MappingRuleEngineTests(FederationTests): assertion = mapping_fixtures.BAD_TESTER_ASSERTION rp = mapping_utils.RuleProcessor(mapping['rules']) mapped_properties = rp.process(assertion) - self.assertIsNone(mapped_properties['name']) + + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) self.assertListEqual(list(), mapped_properties['group_ids']) def test_rule_engine_regex_many_groups(self): @@ -651,9 +674,10 @@ class MappingRuleEngineTests(FederationTests): rp = mapping_utils.RuleProcessor(mapping['rules']) values = rp.process(assertion) + self.assertValidMappedUserObject(values) user_name = assertion.get('UserName') group_ids = values.get('group_ids') - name = values.get('name') + name = values.get('user', {}).get('name') self.assertEqual(user_name, name) self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) @@ -673,9 +697,10 @@ class MappingRuleEngineTests(FederationTests): rp = mapping_utils.RuleProcessor(mapping['rules']) values = rp.process(assertion) + self.assertValidMappedUserObject(values) user_name = assertion.get('UserName') group_ids = values.get('group_ids') - name = values.get('name') + name = values.get('user', {}).get('name') self.assertEqual(user_name, name) self.assertIn(mapping_fixtures.CONTRACTOR_GROUP_ID, group_ids) @@ -694,9 +719,10 @@ class MappingRuleEngineTests(FederationTests): rp = mapping_utils.RuleProcessor(mapping['rules']) values = rp.process(assertion) + self.assertValidMappedUserObject(values) user_name = assertion.get('UserName') group_ids = values.get('group_ids') - name = values.get('name') + name = values.get('user', {}).get('name') self.assertEqual(name, user_name) self.assertEqual(group_ids, []) @@ -714,9 +740,11 @@ class MappingRuleEngineTests(FederationTests): assertion = mapping_fixtures.EMPLOYEE_ASSERTION rp = mapping_utils.RuleProcessor(mapping['rules']) values = rp.process(assertion) + + self.assertValidMappedUserObject(values) user_name = assertion.get('UserName') group_ids = values.get('group_ids') - name = values.get('name') + name = values.get('user', {}).get('name') self.assertEqual(name, user_name) self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) @@ -736,9 +764,10 @@ class MappingRuleEngineTests(FederationTests): rp = mapping_utils.RuleProcessor(mapping['rules']) values = rp.process(assertion) + self.assertValidMappedUserObject(values) user_name = assertion.get('UserName') group_ids = values.get('group_ids') - name = values.get('name') + name = values.get('user', {}).get('name') self.assertEqual(user_name, name) self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) @@ -757,7 +786,9 @@ class MappingRuleEngineTests(FederationTests): assertion = mapping_fixtures.BAD_DEVELOPER_ASSERTION rp = mapping_utils.RuleProcessor(mapping['rules']) mapped_properties = rp.process(assertion) - self.assertIsNone(mapped_properties['name']) + + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) self.assertListEqual(list(), mapped_properties['group_ids']) def _rule_engine_regex_match_and_many_groups(self, assertion): @@ -771,10 +802,12 @@ class MappingRuleEngineTests(FederationTests): mapping = mapping_fixtures.MAPPING_LARGE rp = mapping_utils.RuleProcessor(mapping['rules']) values = rp.process(assertion) + user_name = assertion.get('UserName') group_ids = values.get('group_ids') - name = values.get('name') + name = values.get('user', {}).get('name') + self.assertValidMappedUserObject(values) self.assertEqual(user_name, name) self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) @@ -814,7 +847,8 @@ class MappingRuleEngineTests(FederationTests): rp = mapping_utils.RuleProcessor(mapping['rules']) assertion = mapping_fixtures.CONTRACTOR_MALFORMED_ASSERTION mapped_properties = rp.process(assertion) - self.assertIsNone(mapped_properties['name']) + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) self.assertListEqual(list(), mapped_properties['group_ids']) def test_rule_engine_returns_group_names(self): @@ -830,6 +864,7 @@ class MappingRuleEngineTests(FederationTests): assertion = mapping_fixtures.EMPLOYEE_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) reference = { mapping_fixtures.DEVELOPER_GROUP_NAME: { @@ -849,6 +884,162 @@ class MappingRuleEngineTests(FederationTests): for rule in mapped_properties['group_names']: self.assertDictEqual(reference.get(rule.get('name')), rule) + def test_mapping_federated_domain_specified(self): + """Test mapping engine when domain 'ephemeral' is explicitely set. + + For that, we use mapping rule MAPPING_EPHEMERAL_USER and assertion + EMPLOYEE_ASSERTION + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + def test_create_user_object_with_bad_mapping(self): + """Test if user object is created even with bad mapping + + User objects will be created by mapping engine always as long as there + is corresponding local rule. This test shows, that even with assertion + where no group names nor ids are matched, but there is 'blind' rule + for mapping user, such object will be created. + + In this test MAPPING_EHPEMERAL_USER expects UserName set to jsmith + whereas value from assertion is 'tbo'. + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + self.assertNotIn('id', mapped_properties['user']) + self.assertNotIn('name', mapped_properties['user']) + + def test_set_ephemeral_domain_to_ephemeral_users(self): + """Test auto assigning service domain to ephemeral users + + Test that ephemeral users will always become members of federated + service domain. The check depends on ``type`` value which must be set + to ``ephemeral`` in case of ephemeral user. + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + def test_local_user_local_domain(self): + """Test that local users can have non-service domains assigned.""" + mapping = mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject( + mapped_properties, user_type='local', + domain_id=mapping_fixtures.LOCAL_DOMAIN) + + def test_user_identifications_name(self): + """Test varius mapping options and how users are identified + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has property type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and equal to name, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username({}, mapped_properties) + self.assertEqual('jsmith', mapped_properties['user']['id']) + self.assertEqual('jsmith', mapped_properties['user']['name']) + + def test_user_identifications_name_and_federated_domain(self): + """Test varius mapping options and how users are identified + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and equal to name, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username({}, mapped_properties) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual('tbo', mapped_properties['user']['id']) + + def test_user_identification_id(self): + """Test varius mapping options and how users are identified + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's id is properly mapped from the assertion + - Check if user's name is properly set and equal to id, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.ADMIN_ASSERTION + mapped_properties = rp.process(assertion) + context = {'environment': {}} + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username(context, mapped_properties) + self.assertEqual('bob', mapped_properties['user']['name']) + self.assertEqual('bob', mapped_properties['user']['id']) + + def test_user_identification_id_and_name(self): + """Test varius mapping options and how users are identified + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and and equal to value hardcoded + in the mapping + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CUSTOMER_ASSERTION + mapped_properties = rp.process(assertion) + context = {'environment': {}} + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username(context, mapped_properties) + self.assertEqual('bwilliams', mapped_properties['user']['name']) + self.assertEqual('abc123', mapped_properties['user']['id']) + class FederatedTokenTests(FederationTests):