Skip to content

Commit 9d6c1be

Browse files
committed
Make attribute presence enforcement configurable
Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent d7de930 commit 9d6c1be

File tree

2 files changed

+77
-39
lines changed

2 files changed

+77
-39
lines changed

src/satosa/micro_services/attribute_authorization.py

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,86 @@
55
from ..util import get_dict_defaults
66

77
class AttributeAuthorization(ResponseMicroService):
8-
98
"""
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:
1320
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:
1826
requester1:
19-
attr1:
20-
- "^foo:bar$"
21-
- "^kaka$"
27+
attr1:
28+
- "^foo:bar$"
29+
- "^kaka$"
2230
default:
23-
attr1:
24-
- "plupp@.+$"
25-
"":
31+
attr1:
32+
- "plupp@.+$"
33+
"":
2634
"":
27-
attr2:
28-
- "^knytte:.*$"
29-
attribute_deny:
30-
default:
31-
default:
32-
eppn:
33-
- "^[^@]+$"
35+
attr2:
36+
- "^knytte:.*$"
3437
35-
```
38+
force_attributes_presence_on_deny: false
39+
attribute_deny:
40+
default:
41+
default:
42+
eppn:
43+
- "^[^@]+$"
44+
```
3645
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.
4453
"""
4554

4655
def __init__(self, config, *args, **kwargs):
4756
super().__init__(*args, **kwargs)
4857
self.attribute_allow = config.get("attribute_allow", {})
4958
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)
5061

5162
def _check_authz(self, context, attributes, requester, provider):
5263
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+
):
5572
raise SATOSAAuthenticationError(context.state, "Permission denied")
56-
else:
73+
elif self.force_attributes_presence_on_allow:
5774
raise SATOSAAuthenticationError(context.state, "Permission denied")
5875

59-
6076
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+
):
6385
raise SATOSAAuthenticationError(context.state, "Permission denied")
86+
elif self.force_attributes_presence_on_deny:
87+
raise SATOSAAuthenticationError(context.state, "Permission denied")
6488

6589
def process(self, context, data):
6690
self._check_authz(context, data.attributes, data.requester, data.auth_info.issuer)

tests/satosa/micro_services/test_attribute_authorization.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,23 @@
66
from satosa.context import Context
77

88
class TestAttributeAuthorization:
9-
def create_authz_service(self, attribute_allow, attribute_deny):
10-
authz_service = AttributeAuthorization(config=dict(attribute_allow=attribute_allow,attribute_deny=attribute_deny), name="test_authz",
11-
base_url="https://satosa.example.com")
9+
def create_authz_service(
10+
self,
11+
attribute_allow,
12+
attribute_deny,
13+
force_attributes_presence_on_allow=False,
14+
force_attributes_presence_on_deny=False,
15+
):
16+
authz_service = AttributeAuthorization(
17+
config=dict(
18+
force_attributes_presence_on_allow=force_attributes_presence_on_allow,
19+
force_attributes_presence_on_deny=force_attributes_presence_on_deny,
20+
attribute_allow=attribute_allow,
21+
attribute_deny=attribute_deny,
22+
),
23+
name="test_authz",
24+
base_url="https://satosa.example.com",
25+
)
1226
authz_service.next = lambda ctx, data: data
1327
return authz_service
1428

@@ -49,7 +63,7 @@ def test_authz_allow_missing(self):
4963
"": { "default": {"a0": ['foo1','foo2']} }
5064
}
5165
attribute_deny = {}
52-
authz_service = self.create_authz_service(attribute_allow, attribute_deny)
66+
authz_service = self.create_authz_service(attribute_allow, attribute_deny, force_attributes_presence_on_allow=True)
5367
resp = InternalData(auth_info=AuthenticationInformation())
5468
resp.attributes = {
5569
}

0 commit comments

Comments
 (0)