Block new registrations based on a domain blacklist (#2)

master
Richard van der Hoff 4 years ago committed by GitHub
parent 7d346c8106
commit 2556450557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -42,7 +42,15 @@ listeners:
Synapse allows SAML mapping providers to specify custom configuration through the Synapse allows SAML mapping providers to specify custom configuration through the
`saml2_config.user_mapping_provider.config` option. `saml2_config.user_mapping_provider.config` option.
There are no options currently supported by this provider. Currently the following options are supported:
* `use_name_id_for_remote_uid`: if set to `False`, we will use the SAML
attribute mapped to `uid` to identify the remote user instead of the `NameID`
from the assertion. `True` by default.
* `domain_block_file`: should point a file containing a list of domains (one
per line); users who have an email address on any of these domains will be
blocked from registration.
## Implementation notes ## Implementation notes
@ -63,3 +71,11 @@ errors in the codebase.
This repository uses `unittest` to run the tests located in the `tests` This repository uses `unittest` to run the tests located in the `tests`
directory. They can be ran with `tox -e tests`. directory. They can be ran with `tox -e tests`.
### Making a release
```
git tag vX.Y
python3 setup.py sdist
twine upload dist/matrix-synapse-saml-mozilla-X.Y.tar.gz
```

@ -16,7 +16,7 @@ import logging
import random import random
import string import string
import time import time
from typing import Tuple from typing import Set, Tuple
import attr import attr
import saml2.response import saml2.response
@ -40,7 +40,8 @@ MAPPING_SESSION_VALIDITY_PERIOD_MS = 15 * 60 * 1000
@attr.s @attr.s
class SamlConfig(object): class SamlConfig(object):
use_name_id_for_remote_uid = attr.ib(type=bool) use_name_id_for_remote_uid = attr.ib(type=bool, default=True)
domain_block_list = attr.ib(type=Set[str], default={})
class SamlMappingProvider(object): class SamlMappingProvider(object):
@ -55,6 +56,8 @@ class SamlMappingProvider(object):
self._random = random.SystemRandom() self._random = random.SystemRandom()
self._config = parsed_config self._config = parsed_config
logger.info("Domain block list: %s", self._config.domain_block_list)
def get_remote_user_id( def get_remote_user_id(
self, saml_response: saml2.response.AuthnResponse, client_redirect_url: str self, saml_response: saml2.response.AuthnResponse, client_redirect_url: str
): ):
@ -69,7 +72,7 @@ class SamlMappingProvider(object):
try: try:
return saml_response.ava["uid"][0] return saml_response.ava["uid"][0]
except KeyError: except KeyError:
logger.warning("SAML2 response lacks a 'uid' attestation") logger.warning("SAML2 response lacks a 'uid' attribute")
raise CodeMessageException(400, "'uid' not in SAML2 response") raise CodeMessageException(400, "'uid' not in SAML2 response")
def saml_response_to_user_attributes( def saml_response_to_user_attributes(
@ -97,6 +100,29 @@ class SamlMappingProvider(object):
expire_old_sessions() expire_old_sessions()
# check the user's emails against our block list
if "emails" not in saml_response.ava:
logger.warning("SAML2 response lacks an 'emails' attribute")
raise CodeMessageException(400, "'emails' not in SAML2 response")
for email in saml_response.ava["emails"]:
parts = email.rsplit("@", 1)
if len(parts) != 2:
logger.warning(
"Rejecting registration from remote user %s with unparsable email %s",
remote_user_id,
email,
)
raise CodeMessageException(403, "Forbidden")
if parts[1].lower() in self._config.domain_block_list:
logger.warning(
"Rejecting registration from remote user %s with blacklisted email %s",
remote_user_id,
email,
)
raise CodeMessageException(403, "Forbidden")
# make up a cryptorandom session id # make up a cryptorandom session id
session_id = "".join( session_id = "".join(
self._random.choice(string.ascii_letters) for _ in range(16) self._random.choice(string.ascii_letters) for _ in range(16)
@ -128,10 +154,24 @@ class SamlMappingProvider(object):
Returns: Returns:
SamlConfig: A custom config object SamlConfig: A custom config object
""" """
return SamlConfig( parsed = SamlConfig()
use_name_id_for_remote_uid=config.get("use_name_id_for_remote_uid"), if "use_name_id_for_remote_uid" in config:
parsed.use_name_id_for_remote_uid = config["use_name_id_for_remote_uid"]
domain_block_file = config.get("domain_block_file")
if domain_block_file:
try:
with open(domain_block_file, encoding="ascii") as fh:
parsed.domain_block_list = {
line.strip().lower() for line in fh.readlines()
}
except Exception as e:
raise Exception(
"Error reading domain block file %s: %s" % (domain_block_file, e)
) )
return parsed
@staticmethod @staticmethod
def get_saml_attributes(config: SamlConfig) -> Tuple[set, set]: def get_saml_attributes(config: SamlConfig) -> Tuple[set, set]:
"""Returns the required and optional attributes of a SAML auth response object """Returns the required and optional attributes of a SAML auth response object
@ -145,4 +185,10 @@ class SamlMappingProvider(object):
second set consists of those attributes which can be used if second set consists of those attributes which can be used if
available, but are not necessary available, but are not necessary
""" """
return {"uid"}, {"displayName"} required = set()
optional = {"uid", "emails", "displayName"}
if not config.use_name_id_for_remote_uid:
required += "uid"
return required, optional

@ -0,0 +1,5 @@
MAP = {
"identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
"fro": {"displayName": "displayName"},
"to": {"displayName": "displayName"},
}

@ -0,0 +1,11 @@
MAP = {
"identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"fro": {
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "uid",
"http://schemas.auth0.com/emails": "emails",
},
"to": {
"emails": "http://schemas.auth0.com/emails",
"uid": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
},
}

@ -1,7 +1,8 @@
[flake8] [flake8]
max-line-length = 90 max-line-length = 90
# W503 requires that binary operators be at the end, not start, of lines. Erik doesn't like it. # W503 requires that binary operators be at the end, not start, of lines. Erik doesn't like it.
ignore = W503 # E501: Line too long (black enforces this for us)
ignore = W503,E501
[isort] [isort]
line_length = 88 line_length = 88

@ -22,9 +22,10 @@ from saml2.config import SPConfig
from saml2.response import AuthnResponse from saml2.response import AuthnResponse
from saml2.sigver import CryptoBackend, SecurityContext from saml2.sigver import CryptoBackend, SecurityContext
from synapse.api.errors import RedirectException from synapse.api.errors import CodeMessageException, RedirectException
from matrix_synapse_saml_mozilla._sessions import username_mapping_sessions from matrix_synapse_saml_mozilla._sessions import username_mapping_sessions
from matrix_synapse_saml_mozilla.mapping_provider import SamlConfig, SamlMappingProvider
from . import create_mapping_provider from . import create_mapping_provider
@ -33,6 +34,7 @@ class FakeResponse:
def __init__(self, source_uid, display_name): def __init__(self, source_uid, display_name):
self.ava = { self.ava = {
"uid": [source_uid], "uid": [source_uid],
"emails": [],
} }
if display_name: if display_name:
@ -49,7 +51,13 @@ def _load_test_response() -> AuthnResponse:
).decode("utf-8") ).decode("utf-8")
config = SPConfig() config = SPConfig()
config.load({}) config.load(
{
"attribute_map_dir": pkg_resources.resource_filename(
"matrix_synapse_saml_mozilla", "saml_maps"
)
}
)
assert config.attribute_converters is not None assert config.attribute_converters is not None
response = AuthnResponse( response = AuthnResponse(
@ -57,6 +65,7 @@ def _load_test_response() -> AuthnResponse:
attribute_converters=config.attribute_converters, attribute_converters=config.attribute_converters,
entity_id="https://host/_matrix/saml2/metadata.xml", entity_id="https://host/_matrix/saml2/metadata.xml",
allow_unsolicited=True, allow_unsolicited=True,
allow_unknown_attributes=True,
# tell it not to check the `destination` # tell it not to check the `destination`
asynchop=False, asynchop=False,
# tell it not to check the issue time # tell it not to check the issue time
@ -70,7 +79,7 @@ def _load_test_response() -> AuthnResponse:
class SamlUserAttributeTestCase(unittest.TestCase): class SamlUserAttributeTestCase(unittest.TestCase):
def test_get_remote_user_id_from_name_id(self): def test_get_remote_user_id_from_name_id(self):
resp = _load_test_response() resp = _load_test_response()
provider = create_mapping_provider({"use_name_id_for_remote_uid": True}) provider = create_mapping_provider()
remote_user_id = provider.get_remote_user_id(resp, "",) remote_user_id = provider.get_remote_user_id(resp, "",)
self.assertEqual(remote_user_id, "test@domain.com") self.assertEqual(remote_user_id, "test@domain.com")
@ -78,7 +87,7 @@ class SamlUserAttributeTestCase(unittest.TestCase):
"""Creates a dummy response, feeds it to the provider and checks that it """Creates a dummy response, feeds it to the provider and checks that it
redirects to the username picker. redirects to the username picker.
""" """
provider = create_mapping_provider() provider = create_mapping_provider({"use_name_id_for_remote_uid": False})
response = FakeResponse(123435, "Jonny") response = FakeResponse(123435, "Jonny")
# we expect this to redirect to the username picker # we expect this to redirect to the username picker
@ -105,3 +114,15 @@ class SamlUserAttributeTestCase(unittest.TestCase):
expected_expiry = (time.time() + 15 * 60) * 1000 expected_expiry = (time.time() + 15 * 60) * 1000
self.assertGreaterEqual(session.expiry_time_ms, expected_expiry - 1000) self.assertGreaterEqual(session.expiry_time_ms, expected_expiry - 1000)
self.assertLessEqual(session.expiry_time_ms, expected_expiry + 1000) self.assertLessEqual(session.expiry_time_ms, expected_expiry + 1000)
def test_reject_blacklisted_email(self):
config = SamlConfig(
use_name_id_for_remote_uid=True, domain_block_list={"otherdomain.com"}
)
provider = SamlMappingProvider(config, None)
resp = _load_test_response()
with self.assertRaises(CodeMessageException) as e:
provider.saml_response_to_user_attributes(resp, 0, "http://client/")
self.assertEqual(e.exception.code, 403)

@ -26,15 +26,13 @@
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">testuser@domain.com</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">testuser@domain.com</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="displayName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <ns0:Attribute Name="displayName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Jan de <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Test Testuser</ns0:AttributeValue>
Mooij</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="givenname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <ns0:Attribute Name="givenname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Jan</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Test</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="surname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <ns0:Attribute Name="surname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">de <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Testuser</ns0:AttributeValue>
Mooij</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="http://schemas.xmlsoap.org/claims/Group" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:Attribute Name="http://schemas.xmlsoap.org/claims/Group" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">everyone</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">everyone</ns0:AttributeValue>
@ -55,12 +53,11 @@
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:boolean">false</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:boolean">false</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="http://schemas.auth0.com/nickname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:Attribute Name="http://schemas.auth0.com/nickname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Jan de <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Test Testuser</ns0:AttributeValue>
Mooij</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="http://schemas.auth0.com/emails" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:Attribute Name="http://schemas.auth0.com/emails" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">testuser@domain.com</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">testuser@domain.com</ns0:AttributeValue>
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">other@domain.com</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">other@otherdomain.com</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="http://schemas.auth0.com/dn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:Attribute Name="http://schemas.auth0.com/dn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">mail=testuser@domain.com,o=com,dc=domain</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">mail=testuser@domain.com,o=com,dc=domain</ns0:AttributeValue>
@ -69,7 +66,7 @@
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">mail=testuser@domain.com,o=com,dc=domain</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">mail=testuser@domain.com,o=com,dc=domain</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="http://schemas.auth0.com/email_aliases" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:Attribute Name="http://schemas.auth0.com/email_aliases" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">other@domain.com</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">other@otherdomain.com</ns0:AttributeValue>
</ns0:Attribute> </ns0:Attribute>
<ns0:Attribute Name="http://schemas.auth0.com/_HRData" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:Attribute Name="http://schemas.auth0.com/_HRData" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:anyType">[object Object]</ns0:AttributeValue> <ns0:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:anyType">[object Object]</ns0:AttributeValue>

Loading…
Cancel
Save