|
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