mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-13 21:43:22 +01:00
Fix a regression when grandfathering SAML users. (#8855)
This was broken in #8801 when abstracting code shared with OIDC. After this change both SAML and OIDC have a concept of grandfathering users, but with different implementations.
This commit is contained in:
parent
c21bdc813f
commit
8388384a64
6 changed files with 94 additions and 48 deletions
1
changelog.d/8855.feature
Normal file
1
changelog.d/8855.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add support for re-trying generation of a localpart for OpenID Connect mapping providers.
|
|
@ -39,7 +39,7 @@ from synapse.handlers._base import BaseHandler
|
||||||
from synapse.handlers.sso import MappingException, UserAttributes
|
from synapse.handlers.sso import MappingException, UserAttributes
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging.context import make_deferred_yieldable
|
from synapse.logging.context import make_deferred_yieldable
|
||||||
from synapse.types import JsonDict, map_username_to_mxid_localpart
|
from synapse.types import JsonDict, UserID, map_username_to_mxid_localpart
|
||||||
from synapse.util import json_decoder
|
from synapse.util import json_decoder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -898,13 +898,39 @@ class OidcHandler(BaseHandler):
|
||||||
|
|
||||||
return UserAttributes(**attributes)
|
return UserAttributes(**attributes)
|
||||||
|
|
||||||
|
async def grandfather_existing_users() -> Optional[str]:
|
||||||
|
if self._allow_existing_users:
|
||||||
|
# If allowing existing users we want to generate a single localpart
|
||||||
|
# and attempt to match it.
|
||||||
|
attributes = await oidc_response_to_user_attributes(failures=0)
|
||||||
|
|
||||||
|
user_id = UserID(attributes.localpart, self.server_name).to_string()
|
||||||
|
users = await self.store.get_users_by_id_case_insensitive(user_id)
|
||||||
|
if users:
|
||||||
|
# If an existing matrix ID is returned, then use it.
|
||||||
|
if len(users) == 1:
|
||||||
|
previously_registered_user_id = next(iter(users))
|
||||||
|
elif user_id in users:
|
||||||
|
previously_registered_user_id = user_id
|
||||||
|
else:
|
||||||
|
# Do not attempt to continue generating Matrix IDs.
|
||||||
|
raise MappingException(
|
||||||
|
"Attempted to login as '{}' but it matches more than one user inexactly: {}".format(
|
||||||
|
user_id, users
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return previously_registered_user_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
return await self._sso_handler.get_mxid_from_sso(
|
return await self._sso_handler.get_mxid_from_sso(
|
||||||
self._auth_provider_id,
|
self._auth_provider_id,
|
||||||
remote_user_id,
|
remote_user_id,
|
||||||
user_agent,
|
user_agent,
|
||||||
ip_address,
|
ip_address,
|
||||||
oidc_response_to_user_attributes,
|
oidc_response_to_user_attributes,
|
||||||
self._allow_existing_users,
|
grandfather_existing_users,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -268,7 +268,7 @@ class SamlHandler(BaseHandler):
|
||||||
emails=result.get("emails", []),
|
emails=result.get("emails", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
with (await self._mapping_lock.queue(self._auth_provider_id)):
|
async def grandfather_existing_users() -> Optional[str]:
|
||||||
# backwards-compatibility hack: see if there is an existing user with a
|
# backwards-compatibility hack: see if there is an existing user with a
|
||||||
# suitable mapping from the uid
|
# suitable mapping from the uid
|
||||||
if (
|
if (
|
||||||
|
@ -290,17 +290,18 @@ class SamlHandler(BaseHandler):
|
||||||
if users:
|
if users:
|
||||||
registered_user_id = list(users.keys())[0]
|
registered_user_id = list(users.keys())[0]
|
||||||
logger.info("Grandfathering mapping to %s", registered_user_id)
|
logger.info("Grandfathering mapping to %s", registered_user_id)
|
||||||
await self.store.record_user_external_id(
|
|
||||||
self._auth_provider_id, remote_user_id, registered_user_id
|
|
||||||
)
|
|
||||||
return registered_user_id
|
return registered_user_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
with (await self._mapping_lock.queue(self._auth_provider_id)):
|
||||||
return await self._sso_handler.get_mxid_from_sso(
|
return await self._sso_handler.get_mxid_from_sso(
|
||||||
self._auth_provider_id,
|
self._auth_provider_id,
|
||||||
remote_user_id,
|
remote_user_id,
|
||||||
user_agent,
|
user_agent,
|
||||||
ip_address,
|
ip_address,
|
||||||
saml_response_to_remapped_user_attributes,
|
saml_response_to_remapped_user_attributes,
|
||||||
|
grandfather_existing_users,
|
||||||
)
|
)
|
||||||
|
|
||||||
def expire_sessions(self):
|
def expire_sessions(self):
|
||||||
|
|
|
@ -116,7 +116,7 @@ class SsoHandler(BaseHandler):
|
||||||
user_agent: str,
|
user_agent: str,
|
||||||
ip_address: str,
|
ip_address: str,
|
||||||
sso_to_matrix_id_mapper: Callable[[int], Awaitable[UserAttributes]],
|
sso_to_matrix_id_mapper: Callable[[int], Awaitable[UserAttributes]],
|
||||||
allow_existing_users: bool = False,
|
grandfather_existing_users: Optional[Callable[[], Awaitable[Optional[str]]]],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Given an SSO ID, retrieve the user ID for it and possibly register the user.
|
Given an SSO ID, retrieve the user ID for it and possibly register the user.
|
||||||
|
@ -125,6 +125,10 @@ class SsoHandler(BaseHandler):
|
||||||
if it has that matrix ID is returned regardless of the current mapping
|
if it has that matrix ID is returned regardless of the current mapping
|
||||||
logic.
|
logic.
|
||||||
|
|
||||||
|
If a callable is provided for grandfathering users, it is called and can
|
||||||
|
potentially return a matrix ID to use. If it does, the SSO ID is linked to
|
||||||
|
this matrix ID for subsequent calls.
|
||||||
|
|
||||||
The mapping function is called (potentially multiple times) to generate
|
The mapping function is called (potentially multiple times) to generate
|
||||||
a localpart for the user.
|
a localpart for the user.
|
||||||
|
|
||||||
|
@ -132,17 +136,6 @@ class SsoHandler(BaseHandler):
|
||||||
given user-agent and IP address and the SSO ID is linked to this matrix
|
given user-agent and IP address and the SSO ID is linked to this matrix
|
||||||
ID for subsequent calls.
|
ID for subsequent calls.
|
||||||
|
|
||||||
If allow_existing_users is true the mapping function is only called once
|
|
||||||
and results in:
|
|
||||||
|
|
||||||
1. The use of a previously registered matrix ID. In this case, the
|
|
||||||
SSO ID is linked to the matrix ID. (Note it is possible that
|
|
||||||
other SSO IDs are linked to the same matrix ID.)
|
|
||||||
2. An unused localpart, in which case the user is registered (as
|
|
||||||
discussed above).
|
|
||||||
3. An error if the generated localpart matches multiple pre-existing
|
|
||||||
matrix IDs. Generally this should not happen.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
auth_provider_id: A unique identifier for this SSO provider, e.g.
|
auth_provider_id: A unique identifier for this SSO provider, e.g.
|
||||||
"oidc" or "saml".
|
"oidc" or "saml".
|
||||||
|
@ -152,8 +145,9 @@ class SsoHandler(BaseHandler):
|
||||||
sso_to_matrix_id_mapper: A callable to generate the user attributes.
|
sso_to_matrix_id_mapper: A callable to generate the user attributes.
|
||||||
The only parameter is an integer which represents the amount of
|
The only parameter is an integer which represents the amount of
|
||||||
times the returned mxid localpart mapping has failed.
|
times the returned mxid localpart mapping has failed.
|
||||||
allow_existing_users: True if the localpart returned from the
|
grandfather_existing_users: A callable which can return an previously
|
||||||
mapping provider can be linked to an existing matrix ID.
|
existing matrix ID. The SSO ID is then linked to the returned
|
||||||
|
matrix ID.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The user ID associated with the SSO response.
|
The user ID associated with the SSO response.
|
||||||
|
@ -171,6 +165,16 @@ class SsoHandler(BaseHandler):
|
||||||
if previously_registered_user_id:
|
if previously_registered_user_id:
|
||||||
return previously_registered_user_id
|
return previously_registered_user_id
|
||||||
|
|
||||||
|
# Check for grandfathering of users.
|
||||||
|
if grandfather_existing_users:
|
||||||
|
previously_registered_user_id = await grandfather_existing_users()
|
||||||
|
if previously_registered_user_id:
|
||||||
|
# Future logins should also match this user ID.
|
||||||
|
await self.store.record_user_external_id(
|
||||||
|
auth_provider_id, remote_user_id, previously_registered_user_id
|
||||||
|
)
|
||||||
|
return previously_registered_user_id
|
||||||
|
|
||||||
# Otherwise, generate a new user.
|
# Otherwise, generate a new user.
|
||||||
for i in range(self._MAP_USERNAME_RETRIES):
|
for i in range(self._MAP_USERNAME_RETRIES):
|
||||||
try:
|
try:
|
||||||
|
@ -194,33 +198,7 @@ class SsoHandler(BaseHandler):
|
||||||
|
|
||||||
# Check if this mxid already exists
|
# Check if this mxid already exists
|
||||||
user_id = UserID(attributes.localpart, self.server_name).to_string()
|
user_id = UserID(attributes.localpart, self.server_name).to_string()
|
||||||
users = await self.store.get_users_by_id_case_insensitive(user_id)
|
if not await self.store.get_users_by_id_case_insensitive(user_id):
|
||||||
# Note, if allow_existing_users is true then the loop is guaranteed
|
|
||||||
# to end on the first iteration: either by matching an existing user,
|
|
||||||
# raising an error, or registering a new user. See the docstring for
|
|
||||||
# more in-depth an explanation.
|
|
||||||
if users and allow_existing_users:
|
|
||||||
# If an existing matrix ID is returned, then use it.
|
|
||||||
if len(users) == 1:
|
|
||||||
previously_registered_user_id = next(iter(users))
|
|
||||||
elif user_id in users:
|
|
||||||
previously_registered_user_id = user_id
|
|
||||||
else:
|
|
||||||
# Do not attempt to continue generating Matrix IDs.
|
|
||||||
raise MappingException(
|
|
||||||
"Attempted to login as '{}' but it matches more than one user inexactly: {}".format(
|
|
||||||
user_id, users
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Future logins should also match this user ID.
|
|
||||||
await self.store.record_user_external_id(
|
|
||||||
auth_provider_id, remote_user_id, previously_registered_user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return previously_registered_user_id
|
|
||||||
|
|
||||||
elif not users:
|
|
||||||
# This mxid is free
|
# This mxid is free
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -731,6 +731,14 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(mxid, "@test_user:test")
|
self.assertEqual(mxid, "@test_user:test")
|
||||||
|
|
||||||
|
# Subsequent calls should map to the same mxid.
|
||||||
|
mxid = self.get_success(
|
||||||
|
self.handler._map_userinfo_to_user(
|
||||||
|
userinfo, token, "user-agent", "10.10.10.10"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(mxid, "@test_user:test")
|
||||||
|
|
||||||
# Note that a second SSO user can be mapped to the same Matrix ID. (This
|
# Note that a second SSO user can be mapped to the same Matrix ID. (This
|
||||||
# requires a unique sub, but something that maps to the same matrix ID,
|
# requires a unique sub, but something that maps to the same matrix ID,
|
||||||
# in this case we'll just use the same username. A more realistic example
|
# in this case we'll just use the same username. A more realistic example
|
||||||
|
|
|
@ -16,7 +16,7 @@ import attr
|
||||||
|
|
||||||
from synapse.handlers.sso import MappingException
|
from synapse.handlers.sso import MappingException
|
||||||
|
|
||||||
from tests.unittest import HomeserverTestCase
|
from tests.unittest import HomeserverTestCase, override_config
|
||||||
|
|
||||||
# These are a few constants that are used as config parameters in the tests.
|
# These are a few constants that are used as config parameters in the tests.
|
||||||
BASE_URL = "https://synapse/"
|
BASE_URL = "https://synapse/"
|
||||||
|
@ -59,6 +59,10 @@ class SamlHandlerTestCase(HomeserverTestCase):
|
||||||
"grandfathered_mxid_source_attribute": None,
|
"grandfathered_mxid_source_attribute": None,
|
||||||
"user_mapping_provider": {"module": __name__ + ".TestMappingProvider"},
|
"user_mapping_provider": {"module": __name__ + ".TestMappingProvider"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Update this config with what's in the default config so that
|
||||||
|
# override_config works as expected.
|
||||||
|
saml_config.update(config.get("saml2_config", {}))
|
||||||
config["saml2_config"] = saml_config
|
config["saml2_config"] = saml_config
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -86,6 +90,34 @@ class SamlHandlerTestCase(HomeserverTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(mxid, "@test_user:test")
|
self.assertEqual(mxid, "@test_user:test")
|
||||||
|
|
||||||
|
@override_config({"saml2_config": {"grandfathered_mxid_source_attribute": "mxid"}})
|
||||||
|
def test_map_saml_response_to_existing_user(self):
|
||||||
|
"""Existing users can log in with SAML account."""
|
||||||
|
store = self.hs.get_datastore()
|
||||||
|
self.get_success(
|
||||||
|
store.register_user(user_id="@test_user:test", password_hash=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map a user via SSO.
|
||||||
|
saml_response = FakeAuthnResponse(
|
||||||
|
{"uid": "tester", "mxid": ["test_user"], "username": "test_user"}
|
||||||
|
)
|
||||||
|
redirect_url = ""
|
||||||
|
mxid = self.get_success(
|
||||||
|
self.handler._map_saml_response_to_user(
|
||||||
|
saml_response, redirect_url, "user-agent", "10.10.10.10"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(mxid, "@test_user:test")
|
||||||
|
|
||||||
|
# Subsequent calls should map to the same mxid.
|
||||||
|
mxid = self.get_success(
|
||||||
|
self.handler._map_saml_response_to_user(
|
||||||
|
saml_response, redirect_url, "user-agent", "10.10.10.10"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(mxid, "@test_user:test")
|
||||||
|
|
||||||
def test_map_saml_response_to_invalid_localpart(self):
|
def test_map_saml_response_to_invalid_localpart(self):
|
||||||
"""If the mapping provider generates an invalid localpart it should be rejected."""
|
"""If the mapping provider generates an invalid localpart it should be rejected."""
|
||||||
saml_response = FakeAuthnResponse({"uid": "test", "username": "föö"})
|
saml_response = FakeAuthnResponse({"uid": "test", "username": "föö"})
|
||||||
|
|
Loading…
Reference in a new issue