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:
parent
40c9b5fa28
commit
1cc9ee152e
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue