Enhance user identification in mapping engine

With advent of direct users mapping we need to treat users as objects,
not single identifiers (name, id). This patch introduces user
dictionaries filled as a result of assertion mapping.
The patch also introduces user type which is either ``local`` or
``ephemeral``.

Partially-Implements: bp federated-direct-user-mapping
Change-Id: I0a4d14daa3e1cb3572db65b1e91b0acc57c81234
This commit is contained in:
Marek Denis 2015-02-11 15:50:04 +01:00
parent 5c6ec02be6
commit 7a7b79673b
4 changed files with 449 additions and 32 deletions

View File

@ -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

View File

@ -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}

View File

@ -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',

View File

@ -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):