610 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			610 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
| # Copyright 2014 - 2016 OpenMarket Ltd
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| import logging
 | |
| from typing import TYPE_CHECKING, Optional, Tuple
 | |
| 
 | |
| import pymacaroons
 | |
| from netaddr import IPAddress
 | |
| 
 | |
| from twisted.web.server import Request
 | |
| 
 | |
| from synapse import event_auth
 | |
| from synapse.api.constants import EventTypes, HistoryVisibility, Membership
 | |
| from synapse.api.errors import (
 | |
|     AuthError,
 | |
|     Codes,
 | |
|     InvalidClientTokenError,
 | |
|     MissingClientTokenError,
 | |
|     UnstableSpecAuthError,
 | |
| )
 | |
| from synapse.appservice import ApplicationService
 | |
| from synapse.http import get_request_user_agent
 | |
| from synapse.http.site import SynapseRequest
 | |
| from synapse.logging.opentracing import (
 | |
|     SynapseTags,
 | |
|     active_span,
 | |
|     force_tracing,
 | |
|     start_active_span,
 | |
|     trace,
 | |
| )
 | |
| from synapse.types import Requester, create_requester
 | |
| from synapse.util.cancellation import cancellable
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from synapse.server import HomeServer
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| # guests always get this device id.
 | |
| GUEST_DEVICE_ID = "guest_device"
 | |
| 
 | |
| 
 | |
| class Auth:
 | |
|     """
 | |
|     This class contains functions for authenticating users of our client-server API.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, hs: "HomeServer"):
 | |
|         self.hs = hs
 | |
|         self.clock = hs.get_clock()
 | |
|         self.store = hs.get_datastores().main
 | |
|         self._account_validity_handler = hs.get_account_validity_handler()
 | |
|         self._storage_controllers = hs.get_storage_controllers()
 | |
|         self._macaroon_generator = hs.get_macaroon_generator()
 | |
| 
 | |
|         self._track_appservice_user_ips = hs.config.appservice.track_appservice_user_ips
 | |
|         self._track_puppeted_user_ips = hs.config.api.track_puppeted_user_ips
 | |
|         self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
 | |
| 
 | |
|     async def check_user_in_room(
 | |
|         self,
 | |
|         room_id: str,
 | |
|         requester: Requester,
 | |
|         allow_departed_users: bool = False,
 | |
|     ) -> Tuple[str, Optional[str]]:
 | |
|         """Check if the user is in the room, or was at some point.
 | |
|         Args:
 | |
|             room_id: The room to check.
 | |
| 
 | |
|             requester: The user making the request, according to the access token.
 | |
| 
 | |
|             current_state: Optional map of the current state of the room.
 | |
|                 If provided then that map is used to check whether they are a
 | |
|                 member of the room. Otherwise the current membership is
 | |
|                 loaded from the database.
 | |
| 
 | |
|             allow_departed_users: if True, accept users that were previously
 | |
|                 members but have now departed.
 | |
| 
 | |
|         Raises:
 | |
|             AuthError if the user is/was not in the room.
 | |
|         Returns:
 | |
|             The current membership of the user in the room and the
 | |
|             membership event ID of the user.
 | |
|         """
 | |
| 
 | |
|         user_id = requester.user.to_string()
 | |
|         (
 | |
|             membership,
 | |
|             member_event_id,
 | |
|         ) = await self.store.get_local_current_membership_for_user_in_room(
 | |
|             user_id=user_id,
 | |
|             room_id=room_id,
 | |
|         )
 | |
| 
 | |
|         if membership:
 | |
|             if membership == Membership.JOIN:
 | |
|                 return membership, member_event_id
 | |
| 
 | |
|             # XXX this looks totally bogus. Why do we not allow users who have been banned,
 | |
|             # or those who were members previously and have been re-invited?
 | |
|             if allow_departed_users and membership == Membership.LEAVE:
 | |
|                 forgot = await self.store.did_forget(user_id, room_id)
 | |
|                 if not forgot:
 | |
|                     return membership, member_event_id
 | |
|         raise UnstableSpecAuthError(
 | |
|             403,
 | |
|             "User %s not in room %s" % (user_id, room_id),
 | |
|             errcode=Codes.NOT_JOINED,
 | |
|         )
 | |
| 
 | |
|     @cancellable
 | |
|     async def get_user_by_req(
 | |
|         self,
 | |
|         request: SynapseRequest,
 | |
|         allow_guest: bool = False,
 | |
|         allow_expired: bool = False,
 | |
|     ) -> Requester:
 | |
|         """Get a registered user's ID.
 | |
| 
 | |
|         Args:
 | |
|             request: An HTTP request with an access_token query parameter.
 | |
|             allow_guest: If False, will raise an AuthError if the user making the
 | |
|                 request is a guest.
 | |
|             allow_expired: If True, allow the request through even if the account
 | |
|                 is expired, or session token lifetime has ended. Note that
 | |
|                 /login will deliver access tokens regardless of expiration.
 | |
| 
 | |
|         Returns:
 | |
|             Resolves to the requester
 | |
|         Raises:
 | |
|             InvalidClientCredentialsError if no user by that token exists or the token
 | |
|                 is invalid.
 | |
|             AuthError if access is denied for the user in the access token
 | |
|         """
 | |
|         parent_span = active_span()
 | |
|         with start_active_span("get_user_by_req"):
 | |
|             requester = await self._wrapped_get_user_by_req(
 | |
|                 request, allow_guest, allow_expired
 | |
|             )
 | |
| 
 | |
|             if parent_span:
 | |
|                 if requester.authenticated_entity in self._force_tracing_for_users:
 | |
|                     # request tracing is enabled for this user, so we need to force it
 | |
|                     # tracing on for the parent span (which will be the servlet span).
 | |
|                     #
 | |
|                     # It's too late for the get_user_by_req span to inherit the setting,
 | |
|                     # so we also force it on for that.
 | |
|                     force_tracing()
 | |
|                     force_tracing(parent_span)
 | |
|                 parent_span.set_tag(
 | |
|                     "authenticated_entity", requester.authenticated_entity
 | |
|                 )
 | |
|                 # We tag the Synapse instance name so that it's an easy jumping
 | |
|                 # off point into the logs. Can also be used to filter for an
 | |
|                 # instance that is under load.
 | |
|                 parent_span.set_tag(
 | |
|                     SynapseTags.INSTANCE_NAME, self.hs.get_instance_name()
 | |
|                 )
 | |
|                 parent_span.set_tag("user_id", requester.user.to_string())
 | |
|                 if requester.device_id is not None:
 | |
|                     parent_span.set_tag("device_id", requester.device_id)
 | |
|                 if requester.app_service is not None:
 | |
|                     parent_span.set_tag("appservice_id", requester.app_service.id)
 | |
|             return requester
 | |
| 
 | |
|     @cancellable
 | |
|     async def _wrapped_get_user_by_req(
 | |
|         self,
 | |
|         request: SynapseRequest,
 | |
|         allow_guest: bool,
 | |
|         allow_expired: bool,
 | |
|     ) -> Requester:
 | |
|         """Helper for get_user_by_req
 | |
| 
 | |
|         Once get_user_by_req has set up the opentracing span, this does the actual work.
 | |
|         """
 | |
|         try:
 | |
|             ip_addr = request.getClientAddress().host
 | |
|             user_agent = get_request_user_agent(request)
 | |
| 
 | |
|             access_token = self.get_access_token_from_request(request)
 | |
| 
 | |
|             # First check if it could be a request from an appservice
 | |
|             requester = await self._get_appservice_user(request)
 | |
|             if not requester:
 | |
|                 # If not, it should be from a regular user
 | |
|                 requester = await self.get_user_by_access_token(
 | |
|                     access_token, allow_expired=allow_expired
 | |
|                 )
 | |
| 
 | |
|                 # Deny the request if the user account has expired.
 | |
|                 # This check is only done for regular users, not appservice ones.
 | |
|                 if not allow_expired:
 | |
|                     if await self._account_validity_handler.is_user_expired(
 | |
|                         requester.user.to_string()
 | |
|                     ):
 | |
|                         # Raise the error if either an account validity module has determined
 | |
|                         # the account has expired, or the legacy account validity
 | |
|                         # implementation is enabled and determined the account has expired
 | |
|                         raise AuthError(
 | |
|                             403,
 | |
|                             "User account has expired",
 | |
|                             errcode=Codes.EXPIRED_ACCOUNT,
 | |
|                         )
 | |
| 
 | |
|             if ip_addr and (
 | |
|                 not requester.app_service or self._track_appservice_user_ips
 | |
|             ):
 | |
|                 # XXX(quenting): I'm 95% confident that we could skip setting the
 | |
|                 # device_id to "dummy-device" for appservices, and that the only impact
 | |
|                 # would be some rows which whould not deduplicate in the 'user_ips'
 | |
|                 # table during the transition
 | |
|                 recorded_device_id = (
 | |
|                     "dummy-device"
 | |
|                     if requester.device_id is None and requester.app_service is not None
 | |
|                     else requester.device_id
 | |
|                 )
 | |
|                 await self.store.insert_client_ip(
 | |
|                     user_id=requester.authenticated_entity,
 | |
|                     access_token=access_token,
 | |
|                     ip=ip_addr,
 | |
|                     user_agent=user_agent,
 | |
|                     device_id=recorded_device_id,
 | |
|                 )
 | |
| 
 | |
|                 # Track also the puppeted user client IP if enabled and the user is puppeting
 | |
|                 if (
 | |
|                     requester.user.to_string() != requester.authenticated_entity
 | |
|                     and self._track_puppeted_user_ips
 | |
|                 ):
 | |
|                     await self.store.insert_client_ip(
 | |
|                         user_id=requester.user.to_string(),
 | |
|                         access_token=access_token,
 | |
|                         ip=ip_addr,
 | |
|                         user_agent=user_agent,
 | |
|                         device_id=requester.device_id,
 | |
|                     )
 | |
| 
 | |
|             if requester.is_guest and not allow_guest:
 | |
|                 raise AuthError(
 | |
|                     403,
 | |
|                     "Guest access not allowed",
 | |
|                     errcode=Codes.GUEST_ACCESS_FORBIDDEN,
 | |
|                 )
 | |
| 
 | |
|             request.requester = requester
 | |
|             return requester
 | |
|         except KeyError:
 | |
|             raise MissingClientTokenError()
 | |
| 
 | |
|     async def validate_appservice_can_control_user_id(
 | |
|         self, app_service: ApplicationService, user_id: str
 | |
|     ) -> None:
 | |
|         """Validates that the app service is allowed to control
 | |
|         the given user.
 | |
| 
 | |
|         Args:
 | |
|             app_service: The app service that controls the user
 | |
|             user_id: The author MXID that the app service is controlling
 | |
| 
 | |
|         Raises:
 | |
|             AuthError: If the application service is not allowed to control the user
 | |
|                 (user namespace regex does not match, wrong homeserver, etc)
 | |
|                 or if the user has not been registered yet.
 | |
|         """
 | |
| 
 | |
|         # It's ok if the app service is trying to use the sender from their registration
 | |
|         if app_service.sender == user_id:
 | |
|             pass
 | |
|         # Check to make sure the app service is allowed to control the user
 | |
|         elif not app_service.is_interested_in_user(user_id):
 | |
|             raise AuthError(
 | |
|                 403,
 | |
|                 "Application service cannot masquerade as this user (%s)." % user_id,
 | |
|             )
 | |
|         # Check to make sure the user is already registered on the homeserver
 | |
|         elif not (await self.store.get_user_by_id(user_id)):
 | |
|             raise AuthError(
 | |
|                 403, "Application service has not registered this user (%s)" % user_id
 | |
|             )
 | |
| 
 | |
|     @cancellable
 | |
|     async def _get_appservice_user(self, request: Request) -> Optional[Requester]:
 | |
|         """
 | |
|         Given a request, reads the request parameters to determine:
 | |
|         - whether it's an application service that's making this request
 | |
|         - what user the application service should be treated as controlling
 | |
|           (the user_id URI parameter allows an application service to masquerade
 | |
|           any applicable user in its namespace)
 | |
|         - what device the application service should be treated as controlling
 | |
|           (the device_id[^1] URI parameter allows an application service to masquerade
 | |
|           as any device that exists for the relevant user)
 | |
| 
 | |
|         [^1] Unstable and provided by MSC3202.
 | |
|              Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
 | |
| 
 | |
|         Returns:
 | |
|             the application service `Requester` of that request
 | |
| 
 | |
|         Postconditions:
 | |
|         - The `app_service` field in the returned `Requester` is set
 | |
|         - The `user_id` field in the returned `Requester` is either the application
 | |
|           service sender or the controlled user set by the `user_id` URI parameter
 | |
|         - The returned application service is permitted to control the returned user ID.
 | |
|         - The returned device ID, if present, has been checked to be a valid device ID
 | |
|           for the returned user ID.
 | |
|         """
 | |
|         DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
 | |
| 
 | |
|         app_service = self.store.get_app_service_by_token(
 | |
|             self.get_access_token_from_request(request)
 | |
|         )
 | |
|         if app_service is None:
 | |
|             return None
 | |
| 
 | |
|         if app_service.ip_range_whitelist:
 | |
|             ip_address = IPAddress(request.getClientAddress().host)
 | |
|             if ip_address not in app_service.ip_range_whitelist:
 | |
|                 return None
 | |
| 
 | |
|         # This will always be set by the time Twisted calls us.
 | |
|         assert request.args is not None
 | |
| 
 | |
|         if b"user_id" in request.args:
 | |
|             effective_user_id = request.args[b"user_id"][0].decode("utf8")
 | |
|             await self.validate_appservice_can_control_user_id(
 | |
|                 app_service, effective_user_id
 | |
|             )
 | |
|         else:
 | |
|             effective_user_id = app_service.sender
 | |
| 
 | |
|         effective_device_id: Optional[str] = None
 | |
| 
 | |
|         if (
 | |
|             self.hs.config.experimental.msc3202_device_masquerading_enabled
 | |
|             and DEVICE_ID_ARG_NAME in request.args
 | |
|         ):
 | |
|             effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
 | |
|             # We only just set this so it can't be None!
 | |
|             assert effective_device_id is not None
 | |
|             device_opt = await self.store.get_device(
 | |
|                 effective_user_id, effective_device_id
 | |
|             )
 | |
|             if device_opt is None:
 | |
|                 # For now, use 400 M_EXCLUSIVE if the device doesn't exist.
 | |
|                 # This is an open thread of discussion on MSC3202 as of 2021-12-09.
 | |
|                 raise AuthError(
 | |
|                     400,
 | |
|                     f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
 | |
|                     Codes.EXCLUSIVE,
 | |
|                 )
 | |
| 
 | |
|         return create_requester(
 | |
|             effective_user_id, app_service=app_service, device_id=effective_device_id
 | |
|         )
 | |
| 
 | |
|     async def get_user_by_access_token(
 | |
|         self,
 | |
|         token: str,
 | |
|         allow_expired: bool = False,
 | |
|     ) -> Requester:
 | |
|         """Validate access token and get user_id from it
 | |
| 
 | |
|         Args:
 | |
|             token: The access token to get the user by
 | |
|             allow_expired: If False, raises an InvalidClientTokenError
 | |
|                 if the token is expired
 | |
| 
 | |
|         Raises:
 | |
|             InvalidClientTokenError if a user by that token exists, but the token is
 | |
|                 expired
 | |
|             InvalidClientCredentialsError if no user by that token exists or the token
 | |
|                 is invalid
 | |
|         """
 | |
| 
 | |
|         # First look in the database to see if the access token is present
 | |
|         # as an opaque token.
 | |
|         user_info = await self.store.get_user_by_access_token(token)
 | |
|         if user_info:
 | |
|             valid_until_ms = user_info.valid_until_ms
 | |
|             if (
 | |
|                 not allow_expired
 | |
|                 and valid_until_ms is not None
 | |
|                 and valid_until_ms < self.clock.time_msec()
 | |
|             ):
 | |
|                 # there was a valid access token, but it has expired.
 | |
|                 # soft-logout the user.
 | |
|                 raise InvalidClientTokenError(
 | |
|                     msg="Access token has expired", soft_logout=True
 | |
|                 )
 | |
| 
 | |
|             # Mark the token as used. This is used to invalidate old refresh
 | |
|             # tokens after some time.
 | |
|             await self.store.mark_access_token_as_used(user_info.token_id)
 | |
| 
 | |
|             requester = create_requester(
 | |
|                 user_id=user_info.user_id,
 | |
|                 access_token_id=user_info.token_id,
 | |
|                 is_guest=user_info.is_guest,
 | |
|                 shadow_banned=user_info.shadow_banned,
 | |
|                 device_id=user_info.device_id,
 | |
|                 authenticated_entity=user_info.token_owner,
 | |
|             )
 | |
| 
 | |
|             return requester
 | |
| 
 | |
|         # If the token isn't found in the database, then it could still be a
 | |
|         # macaroon for a guest, so we check that here.
 | |
|         try:
 | |
|             user_id = self._macaroon_generator.verify_guest_token(token)
 | |
| 
 | |
|             # Guest access tokens are not stored in the database (there can
 | |
|             # only be one access token per guest, anyway).
 | |
|             #
 | |
|             # In order to prevent guest access tokens being used as regular
 | |
|             # user access tokens (and hence getting around the invalidation
 | |
|             # process), we look up the user id and check that it is indeed
 | |
|             # a guest user.
 | |
|             #
 | |
|             # It would of course be much easier to store guest access
 | |
|             # tokens in the database as well, but that would break existing
 | |
|             # guest tokens.
 | |
|             stored_user = await self.store.get_user_by_id(user_id)
 | |
|             if not stored_user:
 | |
|                 raise InvalidClientTokenError("Unknown user_id %s" % user_id)
 | |
|             if not stored_user["is_guest"]:
 | |
|                 raise InvalidClientTokenError(
 | |
|                     "Guest access token used for regular user"
 | |
|                 )
 | |
| 
 | |
|             return create_requester(
 | |
|                 user_id=user_id,
 | |
|                 is_guest=True,
 | |
|                 # all guests get the same device id
 | |
|                 device_id=GUEST_DEVICE_ID,
 | |
|                 authenticated_entity=user_id,
 | |
|             )
 | |
|         except (
 | |
|             pymacaroons.exceptions.MacaroonException,
 | |
|             TypeError,
 | |
|             ValueError,
 | |
|         ) as e:
 | |
|             logger.warning(
 | |
|                 "Invalid access token in auth: %s %s.",
 | |
|                 type(e),
 | |
|                 e,
 | |
|             )
 | |
|             raise InvalidClientTokenError("Invalid access token passed.")
 | |
| 
 | |
|     async def is_server_admin(self, requester: Requester) -> bool:
 | |
|         """Check if the given user is a local server admin.
 | |
| 
 | |
|         Args:
 | |
|             requester: The user making the request, according to the access token.
 | |
| 
 | |
|         Returns:
 | |
|             True if the user is an admin
 | |
|         """
 | |
|         return await self.store.is_server_admin(requester.user)
 | |
| 
 | |
|     async def check_can_change_room_list(
 | |
|         self, room_id: str, requester: Requester
 | |
|     ) -> bool:
 | |
|         """Determine whether the user is allowed to edit the room's entry in the
 | |
|         published room list.
 | |
| 
 | |
|         Args:
 | |
|             room_id: The room to check.
 | |
|             requester: The user making the request, according to the access token.
 | |
|         """
 | |
| 
 | |
|         is_admin = await self.is_server_admin(requester)
 | |
|         if is_admin:
 | |
|             return True
 | |
| 
 | |
|         await self.check_user_in_room(room_id, requester)
 | |
| 
 | |
|         # We currently require the user is a "moderator" in the room. We do this
 | |
|         # by checking if they would (theoretically) be able to change the
 | |
|         # m.room.canonical_alias events
 | |
| 
 | |
|         power_level_event = (
 | |
|             await self._storage_controllers.state.get_current_state_event(
 | |
|                 room_id, EventTypes.PowerLevels, ""
 | |
|             )
 | |
|         )
 | |
| 
 | |
|         auth_events = {}
 | |
|         if power_level_event:
 | |
|             auth_events[(EventTypes.PowerLevels, "")] = power_level_event
 | |
| 
 | |
|         send_level = event_auth.get_send_level(
 | |
|             EventTypes.CanonicalAlias, "", power_level_event
 | |
|         )
 | |
|         user_level = event_auth.get_user_power_level(
 | |
|             requester.user.to_string(), auth_events
 | |
|         )
 | |
| 
 | |
|         return user_level >= send_level
 | |
| 
 | |
|     @staticmethod
 | |
|     def has_access_token(request: Request) -> bool:
 | |
|         """Checks if the request has an access_token.
 | |
| 
 | |
|         Returns:
 | |
|             False if no access_token was given, True otherwise.
 | |
|         """
 | |
|         # This will always be set by the time Twisted calls us.
 | |
|         assert request.args is not None
 | |
| 
 | |
|         query_params = request.args.get(b"access_token")
 | |
|         auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
 | |
|         return bool(query_params) or bool(auth_headers)
 | |
| 
 | |
|     @staticmethod
 | |
|     @cancellable
 | |
|     def get_access_token_from_request(request: Request) -> str:
 | |
|         """Extracts the access_token from the request.
 | |
| 
 | |
|         Args:
 | |
|             request: The http request.
 | |
|         Returns:
 | |
|             The access_token
 | |
|         Raises:
 | |
|             MissingClientTokenError: If there isn't a single access_token in the
 | |
|                 request
 | |
|         """
 | |
|         # This will always be set by the time Twisted calls us.
 | |
|         assert request.args is not None
 | |
| 
 | |
|         auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
 | |
|         query_params = request.args.get(b"access_token")
 | |
|         if auth_headers:
 | |
|             # Try the get the access_token from a "Authorization: Bearer"
 | |
|             # header
 | |
|             if query_params is not None:
 | |
|                 raise MissingClientTokenError(
 | |
|                     "Mixing Authorization headers and access_token query parameters."
 | |
|                 )
 | |
|             if len(auth_headers) > 1:
 | |
|                 raise MissingClientTokenError("Too many Authorization headers.")
 | |
|             parts = auth_headers[0].split(b" ")
 | |
|             if parts[0] == b"Bearer" and len(parts) == 2:
 | |
|                 return parts[1].decode("ascii")
 | |
|             else:
 | |
|                 raise MissingClientTokenError("Invalid Authorization header.")
 | |
|         else:
 | |
|             # Try to get the access_token from the query params.
 | |
|             if not query_params:
 | |
|                 raise MissingClientTokenError()
 | |
| 
 | |
|             return query_params[0].decode("ascii")
 | |
| 
 | |
|     @trace
 | |
|     async def check_user_in_room_or_world_readable(
 | |
|         self, room_id: str, requester: Requester, allow_departed_users: bool = False
 | |
|     ) -> Tuple[str, Optional[str]]:
 | |
|         """Checks that the user is or was in the room or the room is world
 | |
|         readable. If it isn't then an exception is raised.
 | |
| 
 | |
|         Args:
 | |
|             room_id: The room to check.
 | |
|             requester: The user making the request, according to the access token.
 | |
|             allow_departed_users: If True, accept users that were previously
 | |
|                 members but have now departed.
 | |
| 
 | |
|         Returns:
 | |
|             Resolves to the current membership of the user in the room and the
 | |
|             membership event ID of the user. If the user is not in the room and
 | |
|             never has been, then `(Membership.JOIN, None)` is returned.
 | |
|         """
 | |
| 
 | |
|         try:
 | |
|             # check_user_in_room will return the most recent membership
 | |
|             # event for the user if:
 | |
|             #  * The user is a non-guest user, and was ever in the room
 | |
|             #  * The user is a guest user, and has joined the room
 | |
|             # else it will throw.
 | |
|             return await self.check_user_in_room(
 | |
|                 room_id, requester, allow_departed_users=allow_departed_users
 | |
|             )
 | |
|         except AuthError:
 | |
|             visibility = await self._storage_controllers.state.get_current_state_event(
 | |
|                 room_id, EventTypes.RoomHistoryVisibility, ""
 | |
|             )
 | |
|             if (
 | |
|                 visibility
 | |
|                 and visibility.content.get("history_visibility")
 | |
|                 == HistoryVisibility.WORLD_READABLE
 | |
|             ):
 | |
|                 return Membership.JOIN, None
 | |
|             raise UnstableSpecAuthError(
 | |
|                 403,
 | |
|                 "User %s not in room %s, and room previews are disabled"
 | |
|                 % (requester.user, room_id),
 | |
|                 errcode=Codes.NOT_JOINED,
 | |
|             )
 |