Browse Source

Merge "Support regexes in whitelists/blacklists"

changes/85/741785/1
Zuul 1 year ago
committed by Gerrit Code Review
parent
commit
dc68ee4816
  1. 61
      doc/source/admin/federation/mapping_combinations.rst
  2. 73
      keystone/federation/utils.py
  3. 38
      keystone/tests/unit/contrib/federation/test_utils.py
  4. 56
      keystone/tests/unit/mapping_fixtures.py
  5. 10
      releasenotes/notes/bug-1880252-51036d5353125e15.yaml

61
doc/source/admin/federation/mapping_combinations.rst

@ -276,7 +276,11 @@ empty condition
- Mapping to user with the email matching value in remote attribute Email
- Mapping to a group(s) with the name matching the value(s) in remote attribute OIDC_GROUPS
.. NOTE::
If the user id and name are not specified in the mapping, the server tries to
directly map ``REMOTE_USER`` environment variable. If this variable is also
unavailable the server returns an HTTP 401 Unauthorized error.
Groups can have multiple values. Each value must be separated by a `;`
Example: OIDC_GROUPS=developers;testers
@ -354,13 +358,42 @@ In ``<other_condition>`` shown below, please supply one of the following:
]
}
.. NOTE::
In the above example, a whitelist can be used to only map the user into a few of
the groups in their ``HTTP_OIDC_GROUPIDS`` remote attribute:
If the user id and name are not specified in the mapping, the server tries to
directly map ``REMOTE_USER`` environment variable. If this variable is also
unavailable the server returns an HTTP 401 Unauthorized error.
.. code-block:: json
{
"type": "HTTP_OIDC_GROUPIDS",
"whitelist": [
"Developers",
"OpsTeam"
]
}
A blacklist can map the user into all groups except those matched:
.. code-block:: json
{
"type": "HTTP_OIDC_GROUPIDS",
"blacklist": [
"Finance"
]
}
Regular expressions can be used in any condition for more flexible matches:
.. code-block:: json
{
"type": "HTTP_OIDC_GROUPIDS",
"whitelist": [
".*Team$"
]
}
Group ids and names can be provided in the local section:
When mapping into groups, either ids or names can be provided in the local section:
.. code-block:: json
@ -504,7 +537,10 @@ setting it to ``true``.
"name": "{0}"
},
"group": {
"id": "0cd5e9"
"name": "{1}",
"domain": {
"id": "abc1234"
}
}
},
],
@ -518,14 +554,23 @@ setting it to ``true``.
".*@yeah.com$"
]
"regex": true
}
},
{
"type": "HTTP_OIDC_GROUPIDS",
"whitelist": [
"Project.*$"
],
"regex": true
}
]
}
]
}
This allows any user with a claim containing a key with any value in
``HTTP_OIDC_GROUPIDS`` to be mapped to group with id ``0cd5e9``.
``HTTP_OIDC_GROUPIDS`` to be mapped to group with id ``0cd5e9``. Additionally,
for every value in the ``HTTP_OIDC_GROUPIDS`` claim matching the string
``Project.*``, the user will be assigned to the project with that name.
Condition Combinations
----------------------

73
keystone/federation/utils.py

@ -190,6 +190,9 @@ MAPPING_SCHEMA = {
},
"blacklist": {
"type": "array"
},
"regex": {
"type": "boolean"
}
}
},
@ -203,6 +206,9 @@ MAPPING_SCHEMA = {
},
"whitelist": {
"type": "array"
},
"regex": {
"type": "boolean"
}
}
},
@ -844,11 +850,17 @@ class RuleProcessor(object):
# If a blacklist or whitelist is used, we want to map to the
# whole list instead of just its values separately.
if blacklisted_values is not None:
direct_map_values = [v for v in direct_map_values
if v not in blacklisted_values]
direct_map_values = (
self._evaluate_requirement(blacklisted_values,
direct_map_values,
self._EvalType.BLACKLIST,
regex))
elif whitelisted_values is not None:
direct_map_values = [v for v in direct_map_values
if v in whitelisted_values]
direct_map_values = (
self._evaluate_requirement(whitelisted_values,
direct_map_values,
self._EvalType.WHITELIST,
regex))
direct_maps.add(direct_map_values)
@ -857,20 +869,26 @@ class RuleProcessor(object):
return direct_maps
def _evaluate_values_by_regex(self, values, assertion_values):
for value in values:
for assertion_value in assertion_values:
if re.search(value, assertion_value):
return True
return False
return [
assertion for assertion in assertion_values
if any([re.search(regex, assertion) for regex in values])
]
def _evaluate_requirement(self, values, assertion_values,
eval_type, regex):
"""Evaluate the incoming requirement and assertion.
If the requirement type does not exist in the assertion data, then
return False. If regex is specified, then compare the values and
assertion values. Otherwise, grab the intersection of the values
and use that to compare against the evaluation type.
Filter the incoming assertions against the requirement values. If regex
is specified, the assertion list is filtered by checking if any of the
requirement regexes matches. Otherwise, the list is filtered by string
equality with any of the allowed values.
Once the assertion values are filtered, the output is determined by the
evaluation type:
any_one_of: return True if there are any matches, False otherwise
not_any_of: return True if there are no matches, False otherwise
blacklist: return the incoming values minus any matches
whitelist: return only the matched values
:param values: list of allowed values, defined in the requirement
:type values: list
@ -881,20 +899,29 @@ class RuleProcessor(object):
:param regex: perform evaluation with regex
:type regex: boolean
:returns: boolean, whether requirement is valid or not.
:returns: list of filtered assertion values (if evaluation type is
'blacklist' or 'whitelist'), or boolean indicating if the
assertion values fulfill the requirement (if evaluation type
is 'any_one_of' or 'not_any_of')
"""
if regex:
any_match = self._evaluate_values_by_regex(values,
assertion_values)
matches = self._evaluate_values_by_regex(values, assertion_values)
else:
any_match = bool(set(values).intersection(set(assertion_values)))
if any_match and eval_type == self._EvalType.ANY_ONE_OF:
return True
if not any_match and eval_type == self._EvalType.NOT_ANY_OF:
return True
return False
matches = set(values).intersection(set(assertion_values))
if eval_type == self._EvalType.ANY_ONE_OF:
return bool(matches)
elif eval_type == self._EvalType.NOT_ANY_OF:
return not bool(matches)
elif eval_type == self._EvalType.BLACKLIST:
return list(set(assertion_values).difference(set(matches)))
elif eval_type == self._EvalType.WHITELIST:
return list(matches)
else:
raise exception.UnexpectedError(
_('Unexpected evaluation type "%(eval_type)s"') % {
'eval_type': eval_type})
def assert_enabled_identity_provider(federation_api, idp_id):

38
keystone/tests/unit/contrib/federation/test_utils.py

@ -262,6 +262,44 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self._rule_engine_regex_match_and_many_groups(
mapping_fixtures.MALFORMED_TESTER_ASSERTION)
def test_rule_engine_regex_blacklist(self):
mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_REGEX
assertion = mapping_fixtures.EMPLOYEE_PARTTIME_ASSERTION
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
mapped = rp.process(assertion)
expected = {
'user': {'type': 'ephemeral'},
'projects': [],
'group_ids': [],
'group_names': [
{'name': 'Manager', 'domain': {
'id': mapping_fixtures.FEDERATED_DOMAIN}}
]
}
self.assertEqual(expected, mapped)
def test_rule_engine_regex_whitelist(self):
mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_REGEX
assertion = mapping_fixtures.EMPLOYEE_PARTTIME_ASSERTION
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
mapped = rp.process(assertion)
expected = {
'user': {'type': 'ephemeral'},
'projects': [],
'group_ids': [],
'group_names': [
{'name': 'Employee', 'domain': {
'id': mapping_fixtures.FEDERATED_DOMAIN}},
{'name': 'PartTimeEmployee', 'domain': {
'id': mapping_fixtures.FEDERATED_DOMAIN}}
]
}
self.assertEqual(expected, mapped)
def test_rule_engine_fails_after_discarding_nonstring(self):
"""Check whether RuleProcessor discards non string objects.

56
keystone/tests/unit/mapping_fixtures.py

@ -887,6 +887,54 @@ MAPPING_GROUPS_BLACKLIST = {
]
}
MAPPING_GROUPS_BLACKLIST_REGEX = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"blacklist": [
".*Employee$"
],
"regex": True
},
],
"local": [
{
"groups": "{0}",
"domain": {
"id": FEDERATED_DOMAIN
}
},
]
}
]
}
MAPPING_GROUPS_WHITELIST_REGEX = {
"rules": [
{
"remote": [
{
"type": "orgPersonType",
"whitelist": [
".*Employee$"
],
"regex": True
},
],
"local": [
{
"groups": "{0}",
"domain": {
"id": FEDERATED_DOMAIN
}
},
]
}
]
}
# Exercise all possibilities of user identification. Values are hardcoded on
# purpose.
MAPPING_USER_IDS = {
@ -1529,6 +1577,14 @@ EMPLOYEE_ASSERTION = {
'orgPersonType': 'Employee;BuildingX'
}
EMPLOYEE_PARTTIME_ASSERTION = {
'Email': 'tim@example.com',
'UserName': 'tbo',
'FirstName': 'Tim',
'LastName': 'Bo',
'orgPersonType': 'Employee;PartTimeEmployee;Manager'
}
EMPLOYEE_ASSERTION_MULTIPLE_GROUPS = {
'Email': 'tim@example.com',
'UserName': 'tbo',

10
releasenotes/notes/bug-1880252-51036d5353125e15.yaml

@ -0,0 +1,10 @@
---
features:
- |
Mappings can now specify "whitelist" and "blacklist" conditionals as
regular expressions. Prior, only "not_any_of" and "any_one_of" conditionals
supported regular expression matching.
fixes:
- |
[`bug 1880252 <https://bugs.launchpad.net/keystone/+bug/1880252>`_]
Regexes are not allowed in "whitelist" and "blacklist" conditionals
Loading…
Cancel
Save