Skip to content

Commit

Permalink
feat: Allow multiple values for SSO attributes via comma separation
Browse files Browse the repository at this point in the history
  • Loading branch information
meise committed Nov 20, 2024
1 parent d0a474d commit a6bc148
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 4 deletions.
5 changes: 3 additions & 2 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3299,7 +3299,8 @@ This setting has the following sub-options:
* `attribute_requirements`: It is possible to configure Synapse to only allow logins if SAML attributes
match particular values. The requirements can be listed under
`attribute_requirements` as shown in the example. All of the listed attributes must
match for the login to be permitted.
match for the login to be permitted. Values can be comma-seperated to allow
multiple values for one attribute.
* `idp_entityid`: If the metadata XML contains multiple IdP entities then the `idp_entityid`
option must be set to the entity to redirect users to.
Most deployments only have a single IdP entity and so should omit this option.
Expand Down Expand Up @@ -3380,7 +3381,7 @@ saml2_config:
- attribute: userGroup
value: "staff"
- attribute: department
value: "sales"
value: "sales,admins"
idp_entityid: 'https://our_idp/entityid'
```
Expand Down
5 changes: 3 additions & 2 deletions synapse/handlers/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,8 +1280,9 @@ def _check_attribute_requirement(
return True

values = attributes[req.attribute]
if req.value in values:
return True
for req_value in req.value.split(","):
if req_value in values:
return True

logger.info(
"SSO attribute %s did not match required value '%s' (was '%s')",
Expand Down
30 changes: 30 additions & 0 deletions tests/handlers/test_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,36 @@ def test_attribute_requirements_contains(self) -> None:
auth_provider_session_id=None,
)

@override_config(
{
"oidc_config": {
**DEFAULT_CONFIG,
"attribute_requirements": [{"attribute": "test", "value": "foo,bar"}],
}
}
)
def test_attribute_requirements_multiple_values(self) -> None:
"""Test that auth succeeds if userinfo attribute has multiple values and CONTAINS required value"""
# userinfo with "test": ["bar"] attribute should succeed.
userinfo = {
"sub": "tester",
"username": "tester",
"test": ["bar"],
}
request, _ = self.start_authorization(userinfo)
self.get_success(self.handler.handle_oidc_callback(request))

# check that the auth handler got called as expected
self.complete_sso_login.assert_called_once_with(
"@tester:test",
self.provider.idp_id,
request,
ANY,
None,
new_user=True,
auth_provider_session_id=None,
)

@override_config(
{
"oidc_config": {
Expand Down
46 changes: 46 additions & 0 deletions tests/handlers/test_saml.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,52 @@ def test_attribute_requirements(self) -> None:
auth_provider_session_id=None,
)

@override_config(
{
"saml2_config": {
"attribute_requirements": [
{"attribute": "userGroup", "value": "staff,admin"},
],
},
}
)
def test_attribute_requirements_multiple_values(self) -> None:
"""The required attributes can be comma-separated."""

# stub out the auth handler
auth_handler = self.hs.get_auth_handler()
auth_handler.complete_sso_login = AsyncMock() # type: ignore[method-assign]

# The response doesn't have the proper department.
saml_response = FakeAuthnResponse(
{"uid": "test_user", "username": "test_user", "userGroup": ["nogroup"]}
)
request = _mock_request()
self.get_success(
self.handler._handle_authn_response(request, saml_response, "redirect_uri")
)
auth_handler.complete_sso_login.assert_not_called()

# Add the proper attributes and it should succeed.
saml_response = FakeAuthnResponse(
{"uid": "test_user", "username": "test_user", "userGroup": ["admin"]}
)
request.reset_mock()
self.get_success(
self.handler._handle_authn_response(request, saml_response, "redirect_uri")
)

# check that the auth handler got called as expected
auth_handler.complete_sso_login.assert_called_once_with(
"@test_user:test",
"saml",
request,
"redirect_uri",
None,
new_user=True,
auth_provider_session_id=None,
)


def _mock_request() -> Mock:
"""Returns a mock which will stand in as a SynapseRequest"""
Expand Down

0 comments on commit a6bc148

Please sign in to comment.