2222from twisted .web .client import PartialDownloadError
2323
2424from synapse .api .errors import HttpResponseException
25+ from synapse .handlers .sso import MappingException , UserAttributes
2526from synapse .http .site import SynapseRequest
2627from synapse .types import UserID , map_username_to_mxid_localpart
2728
@@ -62,6 +63,7 @@ class CasHandler:
6263 def __init__ (self , hs : "HomeServer" ):
6364 self .hs = hs
6465 self ._hostname = hs .hostname
66+ self ._store = hs .get_datastore ()
6567 self ._auth_handler = hs .get_auth_handler ()
6668 self ._registration_handler = hs .get_registration_handler ()
6769
@@ -72,6 +74,9 @@ def __init__(self, hs: "HomeServer"):
7274
7375 self ._http_client = hs .get_proxied_http_client ()
7476
77+ # identifier for the external_ids table
78+ self ._auth_provider_id = "cas"
79+
7580 self ._sso_handler = hs .get_sso_handler ()
7681
7782 def _build_service_param (self , args : Dict [str , str ]) -> str :
@@ -267,6 +272,14 @@ async def _handle_cas_response(
267272 This should be the UI Auth session id.
268273 """
269274
275+ # first check if we're doing a UIA
276+ if session :
277+ return await self ._sso_handler .complete_sso_ui_auth_request (
278+ self ._auth_provider_id , cas_response .username , session , request ,
279+ )
280+
281+ # otherwise, we're handling a login request.
282+
270283 # Ensure that the attributes of the logged in user meet the required
271284 # attributes.
272285 for required_attribute , required_value in self ._cas_required_attributes .items ():
@@ -293,54 +306,79 @@ async def _handle_cas_response(
293306 )
294307 return
295308
296- # Pull out the user-agent and IP from the request.
297- user_agent = request .get_user_agent ("" )
298- ip_address = self .hs .get_ip_from_request (request )
299-
300- # Get the matrix ID from the CAS username.
301- user_id = await self ._map_cas_user_to_matrix_user (
302- cas_response , user_agent , ip_address
303- )
309+ # Call the mapper to register/login the user
304310
305- if session :
306- await self ._auth_handler .complete_sso_ui_auth (
307- user_id , session , request ,
308- )
309- else :
310- # If this not a UI auth request than there must be a redirect URL.
311- assert client_redirect_url
311+ # If this not a UI auth request than there must be a redirect URL.
312+ assert client_redirect_url is not None
312313
313- await self ._auth_handler .complete_sso_login (
314- user_id , request , client_redirect_url
315- )
314+ try :
315+ await self ._complete_cas_login (cas_response , request , client_redirect_url )
316+ except MappingException as e :
317+ logger .exception ("Could not map user" )
318+ self ._sso_handler .render_error (request , "mapping_error" , str (e ))
316319
317- async def _map_cas_user_to_matrix_user (
318- self , cas_response : CasResponse , user_agent : str , ip_address : str ,
319- ) -> str :
320+ async def _complete_cas_login (
321+ self ,
322+ cas_response : CasResponse ,
323+ request : SynapseRequest ,
324+ client_redirect_url : str ,
325+ ) -> None :
320326 """
321- Given a CAS username, retrieve the user ID for it and possibly register the user.
327+ Given a CAS response, complete the login flow
328+
329+ Retrieves the remote user ID, registers the user if necessary, and serves
330+ a redirect back to the client with a login-token.
322331
323332 Args:
324333 cas_response: The parsed CAS response.
325- user_agent : The user agent of the client making the request.
326- ip_address : The IP address of the client making the request .
334+ request : The request to respond to
335+ client_redirect_url : The redirect URL passed in by the client .
327336
328- Returns:
329- The user ID associated with this response.
337+ Raises:
338+ MappingException if there was a problem mapping the response to a user.
339+ RedirectException: some mapping providers may raise this if they need
340+ to redirect to an interstitial page.
330341 """
331-
342+ # Note that CAS does not support a mapping provider, so the logic is hard-coded.
332343 localpart = map_username_to_mxid_localpart (cas_response .username )
333- user_id = UserID (localpart , self ._hostname ).to_string ()
334- registered_user_id = await self ._auth_handler .check_user_exists (user_id )
335344
336- displayname = cas_response .attributes .get (self ._cas_displayname_attribute , None )
345+ async def cas_response_to_user_attributes (failures : int ) -> UserAttributes :
346+ """
347+ Map from CAS attributes to user attributes.
348+ """
349+ # Due to the grandfathering logic matching any previously registered
350+ # mxids it isn't expected for there to be any failures.
351+ if failures :
352+ raise RuntimeError ("CAS is not expected to de-duplicate Matrix IDs" )
353+
354+ display_name = cas_response .attributes .get (
355+ self ._cas_displayname_attribute , None
356+ )
357+
358+ return UserAttributes (localpart = localpart , display_name = display_name )
337359
338- # If the user does not exist, register it.
339- if not registered_user_id :
340- registered_user_id = await self ._registration_handler .register_user (
341- localpart = localpart ,
342- default_display_name = displayname ,
343- user_agent_ips = [(user_agent , ip_address )],
360+ async def grandfather_existing_users () -> Optional [str ]:
361+ # Since CAS did not always use the user_external_ids table, always
362+ # to attempt to map to existing users.
363+ user_id = UserID (localpart , self ._hostname ).to_string ()
364+
365+ logger .debug (
366+ "Looking for existing account based on mapped %s" , user_id ,
344367 )
345368
346- return registered_user_id
369+ users = await self ._store .get_users_by_id_case_insensitive (user_id )
370+ if users :
371+ registered_user_id = list (users .keys ())[0 ]
372+ logger .info ("Grandfathering mapping to %s" , registered_user_id )
373+ return registered_user_id
374+
375+ return None
376+
377+ await self ._sso_handler .complete_sso_login_request (
378+ self ._auth_provider_id ,
379+ cas_response .username ,
380+ request ,
381+ client_redirect_url ,
382+ cas_response_to_user_attributes ,
383+ grandfather_existing_users ,
384+ )
0 commit comments