Implements whitelist and blacklist mapping rules

Adds whitelist and blacklist rules to allow passing through a list of
groups from the IdP, after filtering them according to either of these
rules.

Co-Authored-By: Rodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br>
Co-Authored-By: Marek Denis <marek.denis@cern.ch>
Co-Authored-By: David Stanek <dstanek@dstanek.com>
Partially-Implements: bp mapping-enhancements
Change-Id: I4592ba1c05e99c16645b5303c0192fc462b474aa
This commit is contained in:
Victor Silva 2014-12-16 10:30:31 -03:00 committed by Steve Martinelli
parent 40c9b5fa28
commit 1cc9ee152e
3 changed files with 348 additions and 9 deletions

View File

@ -12,6 +12,7 @@
"""Utilities for Federation Extension."""
import ast
import re
import jsonschema
@ -52,7 +53,9 @@ MAPPING_SCHEMA = {
"oneOf": [
{"$ref": "#/definitions/empty"},
{"$ref": "#/definitions/any_one_of"},
{"$ref": "#/definitions/not_any_of"}
{"$ref": "#/definitions/not_any_of"},
{"$ref": "#/definitions/blacklist"},
{"$ref": "#/definitions/whitelist"}
],
}
}
@ -102,6 +105,32 @@ MAPPING_SCHEMA = {
"type": "boolean"
}
}
},
"blacklist": {
"type": "object",
"additionalProperties": False,
"required": ['type', 'blacklist'],
"properties": {
"type": {
"type": "string"
},
"blacklist": {
"type": "array"
}
}
},
"whitelist": {
"type": "object",
"additionalProperties": False,
"required": ['type', 'whitelist'],
"properties": {
"type": {
"type": "string"
},
"whitelist": {
"type": "array"
}
}
}
}
}
@ -323,6 +352,8 @@ class RuleProcessor(object):
"""Mapping rule evaluation types."""
ANY_ONE_OF = 'any_one_of'
NOT_ANY_OF = 'not_any_of'
BLACKLIST = 'blacklist'
WHITELIST = 'whitelist'
def __init__(self, rules):
"""Initialize RuleProcessor.
@ -435,9 +466,23 @@ class RuleProcessor(object):
Example identity_values::
[{'group': {'id': '0cd5e9'}, 'user': {'email': 'bob@example.com'}}]
[
{
'group': {'id': '0cd5e9'},
'user': {
'email': 'bob@example.com'
},
},
{
'groups': ['member', 'admin', tester'],
'domain': {
'name': 'default_domain'
}
}
]
:returns: dictionary with user name, group_ids and group_names.
:rtype: dict
"""
@ -487,6 +532,26 @@ class RuleProcessor(object):
group['domain'].get('id'))
groups_by_domain.setdefault(domain, list()).append(group)
group_names.extend(extract_groups(groups_by_domain))
if 'groups' in identity_value:
if 'domain' not in identity_value:
msg = _("Invalid rule: %(identity_value)s. Both 'groups' "
"and 'domain' keywords must be specified.")
msg = msg % {'identity_value': identity_value}
raise exception.ValidationError(msg)
# In this case, identity_value['groups'] is a string
# representation of a list, and we want a real list. This is
# due to the way we do direct mapping substitutions today (see
# function _update_local_mapping() )
try:
group_names_list = ast.literal_eval(
identity_value['groups'])
except ValueError:
group_names_list = [identity_value['groups']]
domain = identity_value['domain']
group_dicts = [{'name': name, 'domain': domain} for name in
group_names_list]
group_names.extend(group_dicts)
normalize_user(user)
@ -537,8 +602,9 @@ class RuleProcessor(object):
doesn't apply.
If an array of zero length is returned, then there are no direct
mappings to be performed, but the rule is valid.
Otherwise, then it will return the values, in order, to be directly
mapped, again, the rule is valid.
Otherwise, then it will first attempt to filter the values according
to blacklist or whitelist rules and finally return the values in
order, to be directly mapped.
:param requirements: list of remote requirements from rules
:type requirements: list
@ -554,6 +620,12 @@ class RuleProcessor(object):
"any_one_of": [
"Customer"
]
},
{
"type": "ADFS_GROUPS",
"whitelist": [
"g1", "g2", "g3", "g4"
]
}
]
@ -567,7 +639,8 @@ class RuleProcessor(object):
'LastName': ['Account'],
'orgPersonType': ['Tester'],
'Email': ['testacct@example.com'],
'FirstName': ['Test']
'FirstName': ['Test'],
'ADFS_GROUPS': ['g1', 'g2']
}
:returns: identity values used to update local
@ -604,12 +677,26 @@ class RuleProcessor(object):
return None
# If 'any_one_of' or 'not_any_of' are not found, then values are
# within 'type'. Attempt to find that 'type' within the assertion.
# within 'type'. Attempt to find that 'type' within the assertion,
# and filter these values if 'whitelist' or 'blacklist' is set.
direct_map_values = assertion.get(requirement_type)
if direct_map_values:
LOG.debug('updating a direct mapping: %s', direct_map_values)
blacklisted_values = requirement.get(self._EvalType.BLACKLIST)
whitelisted_values = requirement.get(self._EvalType.WHITELIST)
# If a blacklist or whitelist is used, we want to map to the
# whole list instead of just its values separately.
if blacklisted_values:
direct_map_values = [v for v in direct_map_values
if v not in blacklisted_values]
elif whitelisted_values:
direct_map_values = [v for v in direct_map_values
if v in whitelisted_values]
direct_maps.add(direct_map_values)
LOG.debug('updating a direct mapping: %s', direct_map_values)
return direct_maps
def _evaluate_values_by_regex(self, values, assertion_values):

View File

@ -17,13 +17,13 @@ CONTRACTOR_GROUP_ID = "85a868"
TESTER_GROUP_ID = "123"
TESTER_GROUP_NAME = "tester"
DEVELOPER_GROUP_ID = "xyz"
DEVELOPER_GROUP_NAME = "developer"
DEVELOPER_GROUP_NAME = "Developer"
CONTRACTOR_GROUP_NAME = "Contractor"
DEVELOPER_GROUP_DOMAIN_NAME = "outsourcing"
DEVELOPER_GROUP_DOMAIN_ID = "5abc43"
FEDERATED_DOMAIN = "Federated"
LOCAL_DOMAIN = "Local"
# Mapping summary:
# LastName Smith & Not Contractor or SubContractor -> group 0cd5e9
# FirstName Jill & Contractor or SubContractor -> to group 85a868
@ -584,6 +584,37 @@ MAPPING_EPHEMERAL_USER = {
]
}
MAPPING_GROUPS_WHITELIST = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"whitelist": [
"Developer", "Contractor"
]
},
{
"type": "UserName"
}
],
"local": [
{
"groups": "{0}",
"domain": {
"id": DEVELOPER_GROUP_DOMAIN_ID
}
},
{
"user": {
"name": "{1}"
}
}
]
}
]
}
MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN = {
"rules": [
{
@ -613,6 +644,26 @@ MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN = {
]
}
MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"whitelist": [
"Developer", "Contractor"
]
},
],
"local": [
{
"groups": "{0}",
}
]
}
]
}
MAPPING_LOCAL_USER_LOCAL_DOMAIN = {
"rules": [
{
@ -642,6 +693,37 @@ MAPPING_LOCAL_USER_LOCAL_DOMAIN = {
]
}
MAPPING_GROUPS_BLACKLIST = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"blacklist": [
"Developer", "Manager"
]
},
{
"type": "UserName"
}
],
"local": [
{
"groups": "{0}",
"domain": {
"id": DEVELOPER_GROUP_DOMAIN_ID
}
},
{
"user": {
"name": "{1}"
}
}
]
}
]
}
# Excercise all possibilities of user identitfication. Values are hardcoded on
# purpose.
MAPPING_USER_IDS = {
@ -736,6 +818,52 @@ MAPPING_USER_IDS = {
]
}
MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"blacklist": [
"Developer", "Manager"
]
},
],
"local": [
{
"groups": "{0}",
},
]
}
]
}
MAPPING_GROUPS_WHITELIST_AND_BLACKLIST = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"blacklist": [
"Employee"
],
"whitelist": [
"Contractor"
]
},
],
"local": [
{
"groups": "{0}",
"domain": {
"id": DEVELOPER_GROUP_DOMAIN_ID
}
},
]
}
]
}
EMPLOYEE_ASSERTION = {
'Email': 'tim@example.com',
'UserName': 'tbo',
@ -744,6 +872,14 @@ EMPLOYEE_ASSERTION = {
'orgPersonType': 'Employee;BuildingX'
}
EMPLOYEE_ASSERTION_MULTIPLE_GROUPS = {
'Email': 'tim@example.com',
'UserName': 'tbo',
'FirstName': 'Tim',
'LastName': 'Bo',
'orgPersonType': 'Developer;Manager;Contractor'
}
EMPLOYEE_ASSERTION_PREFIXED = {
'PREFIX_Email': 'tim@example.com',
'PREFIX_UserName': 'tbo',

View File

@ -1249,6 +1249,17 @@ class MappingCRUDTests(FederationTests):
self.put(url, expected_status=400,
body={'mapping': mapping_fixtures.MAPPING_EXTRA_RULES_PROPS})
def test_create_mapping_with_blacklist_and_whitelist(self):
"""Test for adding whitelist and blacklist in the rule
Server should respond with HTTP 400 error upon discovering both
``whitelist`` and ``blacklist`` keywords in the same rule.
"""
url = self.MAPPING_URL + uuid.uuid4().hex
mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_AND_BLACKLIST
self.put(url, expected_status=400, body={'mapping': mapping})
class MappingRuleEngineTests(FederationTests):
"""A class for testing the mapping rule engine."""
@ -1540,6 +1551,111 @@ class MappingRuleEngineTests(FederationTests):
for rule in mapped_properties['group_names']:
self.assertDictEqual(reference.get(rule.get('name')), rule)
def test_rule_engine_whitelist_and_direct_groups_mapping(self):
"""Should return user's groups Developer and Contractor.
The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match
in MAPPING_GROUPS_WHITELIST. It will test the case where 'whitelist'
correctly filters out Manager and only allows Developer and Contractor.
"""
mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST
assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS
rp = mapping_utils.RuleProcessor(mapping['rules'])
mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties)
reference = {
mapping_fixtures.DEVELOPER_GROUP_NAME:
{
"name": mapping_fixtures.DEVELOPER_GROUP_NAME,
"domain": {
"id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID
}
},
mapping_fixtures.CONTRACTOR_GROUP_NAME:
{
"name": mapping_fixtures.CONTRACTOR_GROUP_NAME,
"domain": {
"id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID
}
}
}
for rule in mapped_properties['group_names']:
self.assertDictEqual(reference.get(rule.get('name')), rule)
self.assertEqual('tbo', mapped_properties['user']['name'])
self.assertEqual([], mapped_properties['group_ids'])
def test_rule_engine_blacklist_and_direct_groups_mapping(self):
"""Should return user's group Developer.
The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match
in MAPPING_GROUPS_BLACKLIST. It will test the case where 'blacklist'
correctly filters out Manager and Developer and only allows Contractor.
"""
mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST
assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS
rp = mapping_utils.RuleProcessor(mapping['rules'])
mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties)
reference = {
mapping_fixtures.CONTRACTOR_GROUP_NAME:
{
"name": mapping_fixtures.CONTRACTOR_GROUP_NAME,
"domain": {
"id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID
}
}
}
for rule in mapped_properties['group_names']:
self.assertDictEqual(reference.get(rule.get('name')), rule)
self.assertEqual('tbo', mapped_properties['user']['name'])
self.assertEqual([], mapped_properties['group_ids'])
def test_rule_engine_whitelist_direct_group_mapping_missing_domain(self):
"""Test if the local rule is rejected upon missing domain value
This is a variation with a ``whitelist`` filter.
"""
mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN
assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS
rp = mapping_utils.RuleProcessor(mapping['rules'])
self.assertRaises(exception.ValidationError, rp.process, assertion)
def test_rule_engine_blacklist_direct_group_mapping_missing_domain(self):
"""Test if the local rule is rejected upon missing domain value
This is a variation with a ``blacklist`` filter.
"""
mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN
assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS
rp = mapping_utils.RuleProcessor(mapping['rules'])
self.assertRaises(exception.ValidationError, rp.process, assertion)
def test_rule_engine_no_groups_allowed(self):
"""Should return user mapped to no groups.
The EMPLOYEE_ASSERTION should successfully have a match
in MAPPING_GROUPS_WHITELIST, but 'whitelist' should filter out
the group values from the assertion and thus map to no groups.
"""
mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST
assertion = mapping_fixtures.EMPLOYEE_ASSERTION
rp = mapping_utils.RuleProcessor(mapping['rules'])
mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties)
self.assertListEqual(mapped_properties['group_names'], [])
self.assertListEqual(mapped_properties['group_ids'], [])
self.assertEqual('tbo', mapped_properties['user']['name'])
def test_mapping_federated_domain_specified(self):
"""Test mapping engine when domain 'ephemeral' is explicitely set.