|
5 | 5 | from ..util import get_dict_defaults
|
6 | 6 |
|
7 | 7 | class AttributeAuthorization(ResponseMicroService):
|
8 |
| - |
9 | 8 | """
|
10 |
| -A microservice that performs simple regexp-based authorization based on response |
11 |
| -attributes. The configuration assumes a dict with two keys: attributes_allow |
12 |
| -and attributes_deny. An examples speaks volumes: |
| 9 | + A microservice that performs simple regexp-based authorization based on response |
| 10 | + attributes. There are two configuration options to match attribute values in order |
| 11 | + to allow or deny authorization. |
| 12 | +
|
| 13 | + The configuration is wrapped in two nested dicts that specialize the options per |
| 14 | + requester (SP/RP) and issuer (IdP/OP). |
| 15 | +
|
| 16 | + There are also two options to enforce presence of the attributes that are going to |
| 17 | + be checked. |
| 18 | +
|
| 19 | + Example configuration: |
13 | 20 |
|
14 |
| -```yaml |
15 |
| -config: |
16 |
| - attribute_allow: |
17 |
| - target_provider1: |
| 21 | + ```yaml |
| 22 | + config: |
| 23 | + force_attributes_presence_on_allow: true |
| 24 | + attribute_allow: |
| 25 | + target_provider1: |
18 | 26 | requester1:
|
19 |
| - attr1: |
20 |
| - - "^foo:bar$" |
21 |
| - - "^kaka$" |
| 27 | + attr1: |
| 28 | + - "^foo:bar$" |
| 29 | + - "^kaka$" |
22 | 30 | default:
|
23 |
| - attr1: |
24 |
| - - "plupp@.+$" |
25 |
| - "": |
| 31 | + attr1: |
| 32 | + - "plupp@.+$" |
| 33 | + "": |
26 | 34 | "":
|
27 |
| - attr2: |
28 |
| - - "^knytte:.*$" |
29 |
| - attribute_deny: |
30 |
| - default: |
31 |
| - default: |
32 |
| - eppn: |
33 |
| - - "^[^@]+$" |
| 35 | + attr2: |
| 36 | + - "^knytte:.*$" |
34 | 37 |
|
35 |
| -``` |
| 38 | + force_attributes_presence_on_deny: false |
| 39 | + attribute_deny: |
| 40 | + default: |
| 41 | + default: |
| 42 | + eppn: |
| 43 | + - "^[^@]+$" |
| 44 | + ``` |
36 | 45 |
|
37 |
| -The use of "" and 'default' is synonymous. Attribute rules are not overloaded |
38 |
| -or inherited. For instance a response from "provider2" would only be allowed |
39 |
| -through if the eppn attribute had all values containing an '@' (something |
40 |
| -perhaps best implemented via an allow rule in practice). Responses from |
41 |
| -target_provider1 bound for requester1 would be allowed through only if attr1 |
42 |
| -contained foo:bar or kaka. Note that attribute filters (the leaves of the |
43 |
| -structure above) are ORed together - i.e any attribute match is sufficient. |
| 46 | + The use of "" and "default" is synonymous. Attribute rules are not overloaded |
| 47 | + or inherited. For instance a response from "provider2" would only be allowed |
| 48 | + through if the eppn attribute had all values containing an '@' (something |
| 49 | + perhaps best implemented via an allow rule in practice). Responses from |
| 50 | + target_provider1 bound for requester1 would be allowed through only if attr1 |
| 51 | + contained foo:bar or kaka. Note that attribute filters (the leaves of the |
| 52 | + structure above) are ORed together - i.e any attribute match is sufficient. |
44 | 53 | """
|
45 | 54 |
|
46 | 55 | def __init__(self, config, *args, **kwargs):
|
47 | 56 | super().__init__(*args, **kwargs)
|
48 | 57 | self.attribute_allow = config.get("attribute_allow", {})
|
49 | 58 | self.attribute_deny = config.get("attribute_deny", {})
|
| 59 | + self.force_attributes_presence_on_allow = config.get("force_attributes_presence_on_allow", False) |
| 60 | + self.force_attributes_presence_on_deny = config.get("force_attributes_presence_on_deny", False) |
50 | 61 |
|
51 | 62 | def _check_authz(self, context, attributes, requester, provider):
|
52 | 63 | for attribute_name, attribute_filters in get_dict_defaults(self.attribute_allow, requester, provider).items():
|
53 |
| - if attribute_name in attributes: |
54 |
| - if not any([any(filter(re.compile(af).search, attributes[attribute_name])) for af in attribute_filters]): |
| 64 | + attr_values = attributes.get(attribute_name) |
| 65 | + if attr_values is not None: |
| 66 | + if not any( |
| 67 | + [ |
| 68 | + any(filter(lambda x: re.search(af, x), attr_values)) |
| 69 | + for af in attribute_filters |
| 70 | + ] |
| 71 | + ): |
55 | 72 | raise SATOSAAuthenticationError(context.state, "Permission denied")
|
56 |
| - else: |
| 73 | + elif self.force_attributes_presence_on_allow: |
57 | 74 | raise SATOSAAuthenticationError(context.state, "Permission denied")
|
58 | 75 |
|
59 |
| - |
60 | 76 | for attribute_name, attribute_filters in get_dict_defaults(self.attribute_deny, requester, provider).items():
|
61 |
| - if attribute_name in attributes: |
62 |
| - if any([any(filter(re.compile(af).search, attributes[attribute_name])) for af in attribute_filters]): |
| 77 | + attr_values = attributes.get(attribute_name) |
| 78 | + if attr_values is not None: |
| 79 | + if any( |
| 80 | + [ |
| 81 | + any(filter(lambda x: re.search(af, x), attributes[attribute_name])) |
| 82 | + for af in attribute_filters |
| 83 | + ] |
| 84 | + ): |
63 | 85 | raise SATOSAAuthenticationError(context.state, "Permission denied")
|
| 86 | + elif self.force_attributes_presence_on_deny: |
| 87 | + raise SATOSAAuthenticationError(context.state, "Permission denied") |
64 | 88 |
|
65 | 89 | def process(self, context, data):
|
66 | 90 | self._check_authz(context, data.attributes, data.requester, data.auth_info.issuer)
|
|
0 commit comments