Parcourir la source

Merge "Authorization rules: support YAML nested dictionaries"

tags/3.15.0
Zuul Gerrit Code Review il y a 4 mois
Parent
révision
1729696c81
3 fichiers modifiés avec 116 ajouts et 46 suppressions
  1. +59
    -44
      doc/source/admin/tenants.rst
  2. +26
    -1
      tests/unit/test_configloader.py
  3. +31
    -1
      zuul/model.py

+ 59
- 44
doc/source/admin/tenants.rst Voir le fichier

@@ -355,7 +355,9 @@ Below are some examples of how access rules can be defined:
- admin-rule:
name: affiliate_or_admin
conditions:
- resources_access.account.roles: "affiliate"
- resources_access:
account:
roles: "affiliate"
iss: external_institution
- resources_access.account.roles: "admin"
- admin-rule:
@@ -381,7 +383,8 @@ Below are some examples of how access rules can be defined:
This is the list of conditions that define a rule. A JWT must match **at
least one** of the conditions for the rule to apply. A condition is a
dictionary where keys are claims. **All** the associated values must
match the claims in the user's token.
match the claims in the user's token; in other words the condition dictionary
must be a "sub-dictionary" of the user's JWT.

Zuul's authorization engine will adapt matching tests depending on the
nature of the claim in the token, eg:
@@ -391,45 +394,57 @@ Below are some examples of how access rules can be defined:
* if the claim is a string, check that the condition value is equal to
the claim's value

In order to allow the parsing of claims with complex structures like
dictionaries, claim names can be written in the XPath format.

The special ``zuul_uid`` claim refers to the ``uid_claim`` setting in an
authenticator's configuration. By default it refers to the ``sub`` claim
of a token. For more details see the :ref:`configuration section
<web-server-tenant-scoped-api>` for Zuul web server.

Under the above example, the following token would match rules
``affiliate_or_admin`` and ``alice_or_bob``:

.. code-block:: javascript

{
'iss': 'external_institution',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'iat': 1234556780,
'sub': 'alice',
'resources_access': {
'account': {
'roles': ['affiliate', 'other_role']
}
},
}

And this token would only match rule ``affiliate_or_admin``:

.. code-block:: javascript

{
'iss': 'some_other_institution',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'sub': 'carol',
'iat': 1234556780,
'resources_access': {
'account': {
'roles': ['admin', 'other_role']
}
},
}
The claim names can also be written in the XPath format for clarity: the
condition

.. code-block:: yaml

resources_access:
account:
roles: "affiliate"

is equivalent to the condition

.. code-block:: yaml

resources_access.account.roles: "affiliate"

The special ``zuul_uid`` claim refers to the ``uid_claim`` setting in an
authenticator's configuration. By default it refers to the ``sub`` claim
of a token. For more details see the :ref:`configuration section
<web-server-tenant-scoped-api>` for Zuul web server.

Under the above example, the following token would match rules
``affiliate_or_admin`` and ``alice_or_bob``:

.. code-block:: javascript

{
'iss': 'external_institution',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'iat': 1234556780,
'sub': 'alice',
'resources_access': {
'account': {
'roles': ['affiliate', 'other_role']
}
},
}

And this token would only match rule ``affiliate_or_admin``:

.. code-block:: javascript

{
'iss': 'some_other_institution',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'sub': 'carol',
'iat': 1234556780,
'resources_access': {
'account': {
'roles': ['admin', 'other_role']
}
},
}

+ 26
- 1
tests/unit/test_configloader.py Voir le fichier

@@ -512,7 +512,7 @@ class TestAuthorizationRuleParser(ZuulTestCase):
'groups': ['admin', 'ghostbusters']}
self.assertTrue(rule(claims))

def test_check_complex_rule_from_yaml(self):
def test_check_complex_rule_from_yaml_jsonpath(self):
rule_d = {'name': 'my-rule',
'conditions': [{'hello.this.is': 'a complex value'},
],
@@ -523,6 +523,31 @@ class TestAuthorizationRuleParser(ZuulTestCase):
'hello': {
'this': {
'is': 'a complex value'
},
'and': {
'this one': 'too'
}
}
}
self.assertTrue(rule(claims))

def test_check_complex_rule_from_yaml_nested_dict(self):
rule_d = {'name': 'my-rule',
'conditions': [{'hello': {'this': {'is': 'a complex value'
}
}
},
],
}
rule = AuthorizationRuleParser().fromYaml(rule_d)
self.assertEqual('my-rule', rule.name)
claims = {'iss': 'my-idp',
'hello': {
'this': {
'is': 'a complex value'
},
'and': {
'this one': 'too'
}
}
}


+ 31
- 1
zuul/model.py Voir le fichier

@@ -4821,7 +4821,7 @@ class ClaimRule(AuthZRule):
self.claim = claim or 'sub'
self.value = value

def __call__(self, claims):
def _match_jsonpath(self, claims):
matches = [match.value
for match in jsonpath_rw.parse(self.claim).find(claims)]
if len(matches) == 1:
@@ -4837,6 +4837,36 @@ class ClaimRule(AuthZRule):
# TODO we should differentiate no match and 2+ matches
return False

def _match_dict(self, claims):
def _compare(value, claim):
if isinstance(value, list):
if isinstance(claim, list):
# if the claim is empty, the value must be empty too:
if claim == []:
return value == []
else:
return (set(claim) <= set(value))
else:
return claim in value
elif isinstance(value, dict):
if not isinstance(claim, dict):
return False
elif value == {}:
return claim == {}
else:
return all(_compare(value[x], claim.get(x, {}))
for x in value.keys())
else:
return value == claim

return _compare(self.value, claims.get(self.claim, {}))

def __call__(self, claims):
if isinstance(self.value, dict):
return self._match_dict(claims)
else:
return self._match_jsonpath(claims)

def __eq__(self, other):
if not isinstance(other, ClaimRule):
return False


Chargement…
Annuler
Enregistrer