Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Refactor SsoHandler.get_mxid_from_sso #8900

Merged
merged 5 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8900.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for allowing users to pick their own user ID during a single-sign-on login.
21 changes: 8 additions & 13 deletions synapse/handlers/saml_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
map_username_to_mxid_localpart,
mxid_localpart_allowed_characters,
)
from synapse.util.async_helpers import Linearizer
from synapse.util.iterutils import chunk_seq

if TYPE_CHECKING:
Expand Down Expand Up @@ -81,9 +80,6 @@ def __init__(self, hs: "HomeServer"):
# a map from saml session id to Saml2SessionData object
self._outstanding_requests_dict = {} # type: Dict[str, Saml2SessionData]

# a lock on the mappings
self._mapping_lock = Linearizer(name="saml_mapping", clock=self.clock)

self._sso_handler = hs.get_sso_handler()

def handle_redirect_request(
Expand Down Expand Up @@ -299,15 +295,14 @@ async def grandfather_existing_users() -> Optional[str]:

return None

with (await self._mapping_lock.queue(self._auth_provider_id)):
return await self._sso_handler.get_mxid_from_sso(
self._auth_provider_id,
remote_user_id,
user_agent,
ip_address,
saml_response_to_remapped_user_attributes,
grandfather_existing_users,
)
return await self._sso_handler.get_mxid_from_sso(
self._auth_provider_id,
remote_user_id,
user_agent,
ip_address,
saml_response_to_remapped_user_attributes,
grandfather_existing_users,
)

def _remote_id_from_saml_response(
self,
Expand Down
57 changes: 42 additions & 15 deletions synapse/handlers/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from synapse.api.errors import RedirectException
from synapse.http.server import respond_with_html
from synapse.types import UserID, contains_invalid_mxid_characters
from synapse.util.async_helpers import Linearizer

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -54,6 +55,9 @@ def __init__(self, hs: "HomeServer"):
self._error_template = hs.config.sso_error_template
self._auth_handler = hs.get_auth_handler()

# a lock on the mappings
self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock())

def render_error(
self, request, error: str, error_description: Optional[str] = None
) -> None:
Expand Down Expand Up @@ -172,24 +176,38 @@ async def get_mxid_from_sso(
to an additional page. (e.g. to prompt for more information)

"""
# first of all, check if we already have a mapping for this user
previously_registered_user_id = await self.get_sso_user_by_remote_user_id(
auth_provider_id, remote_user_id,
)
if 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()
# grab a lock while we try to find a mapping for this user. This seems...
# optimistic, especially for implementations that end up redirecting to
# interstitial pages.
with await self._mapping_lock.queue(auth_provider_id):
# first of all, check if we already have a mapping for this user
previously_registered_user_id = await self.get_sso_user_by_remote_user_id(
auth_provider_id, remote_user_id,
)
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.
# Check for grandfathering of users.
if grandfather_existing_users:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just make grandfather_existing_users required since all implementations provide it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that would be sensible. I can't be doing with rebuilding my PR stack to make it so though, so it can wait :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem! was just a passing thought anyway!

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.
attributes = await self._call_attribute_mapper(sso_to_matrix_id_mapper)
user_id = await self._register_mapped_user(
attributes, auth_provider_id, remote_user_id, user_agent, ip_address,
)
return user_id

async def _call_attribute_mapper(
self, sso_to_matrix_id_mapper: Callable[[int], Awaitable[UserAttributes]],
) -> UserAttributes:
"""Call the attribute mapper function in a loop, until we get a unique userid"""
for i in range(self._MAP_USERNAME_RETRIES):
try:
attributes = await sso_to_matrix_id_mapper(i)
Expand Down Expand Up @@ -227,7 +245,16 @@ async def get_mxid_from_sso(
raise MappingException(
"Unable to generate a Matrix ID from the SSO response"
)
return attributes

async def _register_mapped_user(
self,
attributes: UserAttributes,
auth_provider_id: str,
remote_user_id: str,
user_agent: str,
ip_address: str,
) -> str:
# Since the localpart is provided via a potentially untrusted module,
# ensure the MXID is valid before registering.
if contains_invalid_mxid_characters(attributes.localpart):
Expand Down