示例#1
0
 def testNonAscii(self):
     # this should work with either a unicode or a bytes
     self.assertEqual(map_username_to_mxid_localpart(u'têst'), "t=c3=aast")
     self.assertEqual(
         map_username_to_mxid_localpart(u'têst'.encode('utf-8')),
         "t=c3=aast",
     )
示例#2
0
 def testNonAscii(self):
     # this should work with either a unicode or a bytes
     self.assertEqual(map_username_to_mxid_localpart(u'têst'), "t=c3=aast")
     self.assertEqual(
         map_username_to_mxid_localpart(u'têst'.encode('utf-8')),
         "t=c3=aast",
     )
示例#3
0
 def testUpperCase(self):
     self.assertEqual(map_username_to_mxid_localpart("tEST_1234"),
                      "test_1234")
     self.assertEqual(
         map_username_to_mxid_localpart("tEST_1234", case_sensitive=True),
         "t_e_s_t__1234",
     )
示例#4
0
    async def _map_cas_user_to_matrix_user(
        self,
        remote_user_id: str,
        display_name: Optional[str],
        user_agent: str,
        ip_address: str,
    ) -> str:
        """
        Given a CAS username, retrieve the user ID for it and possibly register the user.

        Args:
            remote_user_id: The username from the CAS response.
            display_name: The display name from the CAS response.
            user_agent: The user agent of the client making the request.
            ip_address: The IP address of the client making the request.

        Returns:
             The user ID associated with this response.
        """

        localpart = map_username_to_mxid_localpart(remote_user_id)
        user_id = UserID(localpart, self._hostname).to_string()
        registered_user_id = await self._auth_handler.check_user_exists(user_id
                                                                        )

        # If the user does not exist, register it.
        if not registered_user_id:
            registered_user_id = await self._registration_handler.register_user(
                localpart=localpart,
                default_display_name=display_name,
                user_agent_ips=[(user_agent, ip_address)],
            )

        return registered_user_id
示例#5
0
    async def map_user_attributes(self, userinfo: UserInfo, token: Token,
                                  failures: int) -> UserAttributeDict:
        localpart = None

        if self._config.localpart_template:
            localpart = self._config.localpart_template.render(
                user=userinfo).strip()

            # Ensure only valid characters are included in the MXID.
            localpart = map_username_to_mxid_localpart(localpart)

            # Append suffix integer if last call to this function failed to produce
            # a usable mxid.
            localpart += str(failures) if failures else ""

        def render_template_field(
                template: Optional[Template]) -> Optional[str]:
            if template is None:
                return None
            return template.render(user=userinfo).strip()

        display_name = render_template_field(
            self._config.display_name_template)
        if display_name == "":
            display_name = None

        emails = []  # type: List[str]
        email = render_template_field(self._config.email_template)
        if email:
            emails.append(email)

        return UserAttributeDict(localpart=localpart,
                                 display_name=display_name,
                                 emails=emails)
示例#6
0
        async def grandfather_existing_users() -> Optional[str]:
            # backwards-compatibility hack: see if there is an existing user with a
            # suitable mapping from the uid
            if (self._grandfathered_mxid_source_attribute
                    and self._grandfathered_mxid_source_attribute
                    in saml2_auth.ava):
                attrval = saml2_auth.ava[
                    self._grandfathered_mxid_source_attribute][0]
                user_id = UserID(map_username_to_mxid_localpart(attrval),
                                 self.server_name).to_string()

                logger.debug(
                    "Looking for existing account based on mapped %s %s",
                    self._grandfathered_mxid_source_attribute,
                    user_id,
                )

                users = await self.store.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    registered_user_id = list(users.keys())[0]
                    logger.info("Grandfathering mapping to %s",
                                registered_user_id)
                    return registered_user_id

            return None
示例#7
0
    async def map_user_attributes(self, userinfo: UserInfo, token: Token,
                                  failures: int) -> UserAttributeDict:
        localpart = None

        if self._config.localpart_template:
            localpart = self._config.localpart_template.render(
                user=userinfo).strip()

            # Ensure only valid characters are included in the MXID.
            localpart = map_username_to_mxid_localpart(localpart)

            # Append suffix integer if last call to this function failed to produce
            # a usable mxid.
            localpart += str(failures) if failures else ""

        display_name = None  # type: Optional[str]
        if self._config.display_name_template is not None:
            display_name = self._config.display_name_template.render(
                user=userinfo).strip()

            if display_name == "":
                display_name = None

        return UserAttributeDict(localpart=localpart,
                                 display_name=display_name)
示例#8
0
    async def handle_ticket(
        self,
        request: SynapseRequest,
        ticket: str,
        client_redirect_url: Optional[str],
        session: Optional[str],
    ) -> None:
        """
        Called once the user has successfully authenticated with the SSO.
        Validates a CAS ticket sent by the client and completes the auth process.

        If the user interactive authentication session is provided, marks the
        UI Auth session as complete, then returns an HTML page notifying the
        user they are done.

        Otherwise, this registers the user if necessary, and then returns a
        redirect (with a login token) to the client.

        Args:
            request: the incoming request from the browser. We'll
                respond to it with a redirect or an HTML page.

            ticket: The CAS ticket provided by the client.

            client_redirect_url: the redirectUrl parameter from the `/cas/ticket` HTTP request, if given.
                This should be the same as the redirectUrl from the original `/login/sso/redirect` request.

            session: The session parameter from the `/cas/ticket` HTTP request, if given.
                This should be the UI Auth session id.
        """
        args = {}
        if client_redirect_url:
            args["redirectUrl"] = client_redirect_url
        if session:
            args["session"] = session
        username, user_display_name = await self._validate_ticket(ticket, args)

        localpart = map_username_to_mxid_localpart(username)
        user_id = UserID(localpart, self._hostname).to_string()
        registered_user_id = await self._auth_handler.check_user_exists(user_id
                                                                        )

        if session:
            await self._auth_handler.complete_sso_ui_auth(
                registered_user_id,
                session,
                request,
            )

        else:
            if not registered_user_id:
                registered_user_id = await self._registration_handler.register_user(
                    localpart=localpart,
                    default_display_name=user_display_name)

            await self._auth_handler.complete_sso_login(
                registered_user_id, request, client_redirect_url)
示例#9
0
    def on_successful_auth(
        self,
        username,
        request,
        client_redirect_url,
        user_display_name=None,
    ):
        """Called once the user has successfully authenticated with the SSO.

        Registers the user if necessary, and then returns a redirect (with
        a login token) to the client.

        Args:
            username (unicode|bytes): the remote user id. We'll map this onto
                something sane for a MXID localpath.

            request (SynapseRequest): the incoming request from the browser. We'll
                respond to it with a redirect.

            client_redirect_url (unicode): the redirect_url the client gave us when
                it first started the process.

            user_display_name (unicode|None): if set, and we have to register a new user,
                we will set their displayname to this.

        Returns:
            Deferred[none]: Completes once we have handled the request.
        """
        localpart = map_username_to_mxid_localpart(username)
        user_id = UserID(localpart, self._hostname).to_string()
        registered_user_id = yield self._auth_handler.check_user_exists(
            user_id)
        if not registered_user_id:
            registered_user_id, _ = (yield self._registration_handler.register(
                localpart=localpart,
                generate_token=False,
                default_display_name=user_display_name,
            ))

        login_token = self._macaroon_gen.generate_short_term_login_token(
            registered_user_id)
        redirect_url = self._add_login_token_to_redirect_url(
            client_redirect_url, login_token)
        request.redirect(redirect_url)
        finish_request(request)
示例#10
0
    def on_successful_auth(
        self, username, request, client_redirect_url,
        user_display_name=None,
    ):
        """Called once the user has successfully authenticated with the SSO.

        Registers the user if necessary, and then returns a redirect (with
        a login token) to the client.

        Args:
            username (unicode|bytes): the remote user id. We'll map this onto
                something sane for a MXID localpath.

            request (SynapseRequest): the incoming request from the browser. We'll
                respond to it with a redirect.

            client_redirect_url (unicode): the redirect_url the client gave us when
                it first started the process.

            user_display_name (unicode|None): if set, and we have to register a new user,
                we will set their displayname to this.

        Returns:
            Deferred[none]: Completes once we have handled the request.
        """
        localpart = map_username_to_mxid_localpart(username)
        user_id = UserID(localpart, self._hostname).to_string()
        registered_user_id = yield self._auth_handler.check_user_exists(user_id)
        if not registered_user_id:
            registered_user_id, _ = (
                yield self._registration_handler.register(
                    localpart=localpart,
                    generate_token=False,
                    default_display_name=user_display_name,
                )
            )

        login_token = self._macaroon_gen.generate_short_term_login_token(
            registered_user_id
        )
        redirect_url = self._add_login_token_to_redirect_url(
            client_redirect_url, login_token
        )
        request.redirect(redirect_url)
        finish_request(request)
示例#11
0
    async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url):
        try:
            saml2_auth = self._saml_client.parse_authn_request_response(
                resp_bytes,
                saml2.BINDING_HTTP_POST,
                outstanding=self._outstanding_requests_dict,
            )
        except Exception as e:
            logger.warning("Exception parsing SAML2 response: %s", e)
            raise SynapseError(400, "Unable to parse SAML2 response: %s" % (e,))

        if saml2_auth.not_signed:
            logger.warning("SAML2 response was not signed")
            raise SynapseError(400, "SAML2 response was not signed")

        logger.debug("SAML2 response: %s", saml2_auth.origxml)
        for assertion in saml2_auth.assertions:
            # kibana limits the length of a log field, whereas this is all rather
            # useful, so split it up.
            count = 0
            for part in chunk_seq(str(assertion), 10000):
                logger.info(
                    "SAML2 assertion: %s%s", "(%i)..." % (count,) if count else "", part
                )
                count += 1

        logger.info("SAML2 mapped attributes: %s", saml2_auth.ava)

        self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)

        remote_user_id = self._user_mapping_provider.get_remote_user_id(
            saml2_auth, client_redirect_url
        )

        if not remote_user_id:
            raise Exception("Failed to extract remote user id from SAML response")

        with (await self._mapping_lock.queue(self._auth_provider_id)):
            # first of all, check if we already have a mapping for this user
            logger.info(
                "Looking for existing mapping for user %s:%s",
                self._auth_provider_id,
                remote_user_id,
            )
            registered_user_id = await self._datastore.get_user_by_external_id(
                self._auth_provider_id, remote_user_id
            )
            if registered_user_id is not None:
                logger.info("Found existing mapping %s", registered_user_id)
                return registered_user_id

            # backwards-compatibility hack: see if there is an existing user with a
            # suitable mapping from the uid
            if (
                self._grandfathered_mxid_source_attribute
                and self._grandfathered_mxid_source_attribute in saml2_auth.ava
            ):
                attrval = saml2_auth.ava[self._grandfathered_mxid_source_attribute][0]
                user_id = UserID(
                    map_username_to_mxid_localpart(attrval), self._hostname
                ).to_string()
                logger.info(
                    "Looking for existing account based on mapped %s %s",
                    self._grandfathered_mxid_source_attribute,
                    user_id,
                )

                users = await self._datastore.get_users_by_id_case_insensitive(user_id)
                if users:
                    registered_user_id = list(users.keys())[0]
                    logger.info("Grandfathering mapping to %s", registered_user_id)
                    await self._datastore.record_user_external_id(
                        self._auth_provider_id, remote_user_id, registered_user_id
                    )
                    return registered_user_id

            # Map saml response to user attributes using the configured mapping provider
            for i in range(1000):
                attribute_dict = self._user_mapping_provider.saml_response_to_user_attributes(
                    saml2_auth, i, client_redirect_url=client_redirect_url,
                )

                logger.debug(
                    "Retrieved SAML attributes from user mapping provider: %s "
                    "(attempt %d)",
                    attribute_dict,
                    i,
                )

                localpart = attribute_dict.get("mxid_localpart")
                if not localpart:
                    logger.error(
                        "SAML mapping provider plugin did not return a "
                        "mxid_localpart object"
                    )
                    raise SynapseError(500, "Error parsing SAML2 response")

                displayname = attribute_dict.get("displayname")

                # Check if this mxid already exists
                if not await self._datastore.get_users_by_id_case_insensitive(
                    UserID(localpart, self._hostname).to_string()
                ):
                    # This mxid is free
                    break
            else:
                # Unable to generate a username in 1000 iterations
                # Break and return error to the user
                raise SynapseError(
                    500, "Unable to generate a Matrix ID from the SAML response"
                )

            logger.info("Mapped SAML user to local part %s", localpart)

            registered_user_id = await self._registration_handler.register_user(
                localpart=localpart, default_display_name=displayname
            )

            await self._datastore.record_user_external_id(
                self._auth_provider_id, remote_user_id, registered_user_id
            )
            return registered_user_id
示例#12
0
    async def _map_saml_response_to_user(
        self,
        saml2_auth: saml2.response.AuthnResponse,
        client_redirect_url: str,
        user_agent: str,
        ip_address: str,
    ) -> str:
        """
        Given a SAML response, retrieve the user ID for it and possibly register the user.

        Args:
            saml2_auth: The parsed SAML2 response.
            client_redirect_url: The redirect URL passed in by the client.
            user_agent: The user agent of the client making the request.
            ip_address: The IP address of the client making the request.

        Returns:
             The user ID associated with this response.

        Raises:
            MappingException if there was a problem mapping the response to a user.
            RedirectException: some mapping providers may raise this if they need
                to redirect to an interstitial page.
        """

        remote_user_id = self._user_mapping_provider.get_remote_user_id(
            saml2_auth, client_redirect_url)

        if not remote_user_id:
            raise MappingException(
                "Failed to extract remote user id from SAML response")

        with (await self._mapping_lock.queue(self._auth_provider_id)):
            # first of all, check if we already have a mapping for this user
            logger.info(
                "Looking for existing mapping for user %s:%s",
                self._auth_provider_id,
                remote_user_id,
            )
            registered_user_id = await self._datastore.get_user_by_external_id(
                self._auth_provider_id, remote_user_id)
            if registered_user_id is not None:
                logger.info("Found existing mapping %s", registered_user_id)
                return registered_user_id

            # backwards-compatibility hack: see if there is an existing user with a
            # suitable mapping from the uid
            if (self._grandfathered_mxid_source_attribute
                    and self._grandfathered_mxid_source_attribute
                    in saml2_auth.ava):
                attrval = saml2_auth.ava[
                    self._grandfathered_mxid_source_attribute][0]
                user_id = UserID(map_username_to_mxid_localpart(attrval),
                                 self._hostname).to_string()
                logger.info(
                    "Looking for existing account based on mapped %s %s",
                    self._grandfathered_mxid_source_attribute,
                    user_id,
                )

                users = await self._datastore.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    registered_user_id = list(users.keys())[0]
                    logger.info("Grandfathering mapping to %s",
                                registered_user_id)
                    await self._datastore.record_user_external_id(
                        self._auth_provider_id, remote_user_id,
                        registered_user_id)
                    return registered_user_id

            # Map saml response to user attributes using the configured mapping provider
            for i in range(1000):
                attribute_dict = self._user_mapping_provider.saml_response_to_user_attributes(
                    saml2_auth,
                    i,
                    client_redirect_url=client_redirect_url,
                )

                logger.debug(
                    "Retrieved SAML attributes from user mapping provider: %s "
                    "(attempt %d)",
                    attribute_dict,
                    i,
                )

                localpart = attribute_dict.get("mxid_localpart")
                if not localpart:
                    raise MappingException(
                        "Error parsing SAML2 response: SAML mapping provider plugin "
                        "did not return a mxid_localpart value")

                displayname = attribute_dict.get("displayname")
                emails = attribute_dict.get("emails", [])

                # Check if this mxid already exists
                if not await self._datastore.get_users_by_id_case_insensitive(
                        UserID(localpart, self._hostname).to_string()):
                    # This mxid is free
                    break
            else:
                # Unable to generate a username in 1000 iterations
                # Break and return error to the user
                raise MappingException(
                    "Unable to generate a Matrix ID from the SAML response")

            logger.info("Mapped SAML user to local part %s", localpart)

            registered_user_id = await self._registration_handler.register_user(
                localpart=localpart,
                default_display_name=displayname,
                bind_emails=emails,
                user_agent_ips=(user_agent, ip_address),
            )

            await self._datastore.record_user_external_id(
                self._auth_provider_id, remote_user_id, registered_user_id)
            return registered_user_id
示例#13
0
    async def _map_userinfo_to_user(
        self, userinfo: UserInfo, token: Token, user_agent: str, ip_address: str
    ) -> str:
        """Maps a UserInfo object to a mxid.

        UserInfo should have a claim that uniquely identifies users. This claim
        is usually `sub`, but can be configured with `oidc_config.subject_claim`.
        It is then used as an `external_id`.

        If we don't find the user that way, we should register the user,
        mapping the localpart and the display name from the UserInfo.

        If a user already exists with the mxid we've mapped and allow_existing_users
        is disabled, raise an exception.

        Args:
            userinfo: an object representing the user
            token: a dict with the tokens obtained from the provider
            user_agent: The user agent of the client making the request.
            ip_address: The IP address of the client making the request.

        Raises:
            MappingException: if there was an error while mapping some properties

        Returns:
            The mxid of the user
        """
        try:
            remote_user_id = self._user_mapping_provider.get_remote_user_id(userinfo)
        except Exception as e:
            raise MappingException(
                "Failed to extract subject from OIDC response: %s" % (e,)
            )
        # Some OIDC providers use integer IDs, but Synapse expects external IDs
        # to be strings.
        remote_user_id = str(remote_user_id)

        logger.info(
            "Looking for existing mapping for user %s:%s",
            self._auth_provider_id,
            remote_user_id,
        )

        registered_user_id = await self._datastore.get_user_by_external_id(
            self._auth_provider_id, remote_user_id,
        )

        if registered_user_id is not None:
            logger.info("Found existing mapping %s", registered_user_id)
            return registered_user_id

        try:
            attributes = await self._user_mapping_provider.map_user_attributes(
                userinfo, token
            )
        except Exception as e:
            raise MappingException(
                "Could not extract user attributes from OIDC response: " + str(e)
            )

        logger.debug(
            "Retrieved user attributes from user mapping provider: %r", attributes
        )

        if not attributes["localpart"]:
            raise MappingException("localpart is empty")

        localpart = map_username_to_mxid_localpart(attributes["localpart"])

        user_id = UserID(localpart, self._hostname).to_string()
        users = await self._datastore.get_users_by_id_case_insensitive(user_id)
        if users:
            if self._allow_existing_users:
                if len(users) == 1:
                    registered_user_id = next(iter(users))
                elif user_id in users:
                    registered_user_id = user_id
                else:
                    raise MappingException(
                        "Attempted to login as '{}' but it matches more than one user inexactly: {}".format(
                            user_id, list(users.keys())
                        )
                    )
            else:
                # This mxid is taken
                raise MappingException("mxid '{}' is already taken".format(user_id))
        else:
            # It's the first time this user is logging in and the mapped mxid was
            # not taken, register the user
            registered_user_id = await self._registration_handler.register_user(
                localpart=localpart,
                default_display_name=attributes["display_name"],
                user_agent_ips=(user_agent, ip_address),
            )
        await self._datastore.record_user_external_id(
            self._auth_provider_id, remote_user_id, registered_user_id,
        )
        return registered_user_id
示例#14
0
 def testSymbols(self):
     self.assertEqual(map_username_to_mxid_localpart("test=$?_1234"),
                      "test=3d=24=3f_1234")
示例#15
0
 def testPassThrough(self):
     self.assertEqual(map_username_to_mxid_localpart("test1234"),
                      "test1234")
示例#16
0
 def testSymbols(self):
     self.assertEqual(
         map_username_to_mxid_localpart("test=$?_1234"),
         "test=3d=24=3f_1234",
     )
示例#17
0
 def testUpperCase(self):
     self.assertEqual(map_username_to_mxid_localpart("tEST_1234"), "test_1234")
     self.assertEqual(
         map_username_to_mxid_localpart("tEST_1234", case_sensitive=True),
         "t_e_s_t__1234",
     )
示例#18
0
 def testPassThrough(self):
     self.assertEqual(map_username_to_mxid_localpart("test1234"), "test1234")
示例#19
0
 def testLeadingUnderscore(self):
     self.assertEqual(map_username_to_mxid_localpart("_test_1234"), "=5ftest_1234")
示例#20
0
    async def _map_userinfo_to_user(self, userinfo: UserInfo, token: Token) -> str:
        """Maps a UserInfo object to a mxid.

        UserInfo should have a claim that uniquely identifies users. This claim
        is usually `sub`, but can be configured with `oidc_config.subject_claim`.
        It is then used as an `external_id`.

        If we don't find the user that way, we should register the user,
        mapping the localpart and the display name from the UserInfo.

        If a user already exists with the mxid we've mapped, raise an exception.

        Args:
            userinfo: an object representing the user
            token: a dict with the tokens obtained from the provider

        Raises:
            MappingException: if there was an error while mapping some properties

        Returns:
            The mxid of the user
        """
        try:
            remote_user_id = self._user_mapping_provider.get_remote_user_id(userinfo)
        except Exception as e:
            raise MappingException(
                "Failed to extract subject from OIDC response: %s" % (e,)
            )

        logger.info(
            "Looking for existing mapping for user %s:%s",
            self._auth_provider_id,
            remote_user_id,
        )

        registered_user_id = await self._datastore.get_user_by_external_id(
            self._auth_provider_id, remote_user_id,
        )

        if registered_user_id is not None:
            logger.info("Found existing mapping %s", registered_user_id)
            return registered_user_id

        try:
            attributes = await self._user_mapping_provider.map_user_attributes(
                userinfo, token
            )
        except Exception as e:
            raise MappingException(
                "Could not extract user attributes from OIDC response: " + str(e)
            )

        logger.debug(
            "Retrieved user attributes from user mapping provider: %r", attributes
        )

        if not attributes["localpart"]:
            raise MappingException("localpart is empty")

        localpart = map_username_to_mxid_localpart(attributes["localpart"])

        user_id = UserID(localpart, self._hostname)
        if await self._datastore.get_users_by_id_case_insensitive(user_id.to_string()):
            # This mxid is taken
            raise MappingException(
                "mxid '{}' is already taken".format(user_id.to_string())
            )

        # It's the first time this user is logging in and the mapped mxid was
        # not taken, register the user
        registered_user_id = await self._registration_handler.register_user(
            localpart=localpart, default_display_name=attributes["display_name"],
        )

        await self._datastore.record_user_external_id(
            self._auth_provider_id, remote_user_id, registered_user_id,
        )
        return registered_user_id
示例#21
0
    async def _map_saml_response_to_user(
        self,
        resp_bytes: str,
        client_redirect_url: str,
        user_agent: str,
        ip_address: str,
    ) -> Tuple[str, Optional[Saml2SessionData]]:
        """
        Given a sample response, retrieve the cached session and user for it.

        Args:
            resp_bytes: The SAML response.
            client_redirect_url: The redirect URL passed in by the client.
            user_agent: The user agent of the client making the request.
            ip_address: The IP address of the client making the request.

        Returns:
             Tuple of the user ID and SAML session associated with this response.

        Raises:
            SynapseError if there was a problem with the response.
            RedirectException: some mapping providers may raise this if they need
                to redirect to an interstitial page.
        """
        try:
            saml2_auth = self._saml_client.parse_authn_request_response(
                resp_bytes,
                saml2.BINDING_HTTP_POST,
                outstanding=self._outstanding_requests_dict,
            )
        except saml2.response.UnsolicitedResponse as e:
            # the pysaml2 library helpfully logs an ERROR here, but neglects to log
            # the session ID. I don't really want to put the full text of the exception
            # in the (user-visible) exception message, so let's log the exception here
            # so we can track down the session IDs later.
            logger.warning(str(e))
            raise SynapseError(400, "Unexpected SAML2 login.")
        except Exception as e:
            raise SynapseError(400,
                               "Unable to parse SAML2 response: %s." % (e, ))

        if saml2_auth.not_signed:
            raise SynapseError(400, "SAML2 response was not signed.")

        logger.debug("SAML2 response: %s", saml2_auth.origxml)
        for assertion in saml2_auth.assertions:
            # kibana limits the length of a log field, whereas this is all rather
            # useful, so split it up.
            count = 0
            for part in chunk_seq(str(assertion), 10000):
                logger.info("SAML2 assertion: %s%s",
                            "(%i)..." % (count, ) if count else "", part)
                count += 1

        logger.info("SAML2 mapped attributes: %s", saml2_auth.ava)

        current_session = self._outstanding_requests_dict.pop(
            saml2_auth.in_response_to, None)

        for requirement in self._saml2_attribute_requirements:
            _check_attribute_requirement(saml2_auth.ava, requirement)

        remote_user_id = self._user_mapping_provider.get_remote_user_id(
            saml2_auth, client_redirect_url)

        if not remote_user_id:
            raise Exception(
                "Failed to extract remote user id from SAML response")

        with (await self._mapping_lock.queue(self._auth_provider_id)):
            # first of all, check if we already have a mapping for this user
            logger.info(
                "Looking for existing mapping for user %s:%s",
                self._auth_provider_id,
                remote_user_id,
            )
            registered_user_id = await self._datastore.get_user_by_external_id(
                self._auth_provider_id, remote_user_id)
            if registered_user_id is not None:
                logger.info("Found existing mapping %s", registered_user_id)
                return registered_user_id, current_session

            # backwards-compatibility hack: see if there is an existing user with a
            # suitable mapping from the uid
            if (self._grandfathered_mxid_source_attribute
                    and self._grandfathered_mxid_source_attribute
                    in saml2_auth.ava):
                attrval = saml2_auth.ava[
                    self._grandfathered_mxid_source_attribute][0]
                user_id = UserID(map_username_to_mxid_localpart(attrval),
                                 self._hostname).to_string()
                logger.info(
                    "Looking for existing account based on mapped %s %s",
                    self._grandfathered_mxid_source_attribute,
                    user_id,
                )

                users = await self._datastore.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    registered_user_id = list(users.keys())[0]
                    logger.info("Grandfathering mapping to %s",
                                registered_user_id)
                    await self._datastore.record_user_external_id(
                        self._auth_provider_id, remote_user_id,
                        registered_user_id)
                    return registered_user_id, current_session

            # Map saml response to user attributes using the configured mapping provider
            for i in range(1000):
                attribute_dict = self._user_mapping_provider.saml_response_to_user_attributes(
                    saml2_auth,
                    i,
                    client_redirect_url=client_redirect_url,
                )

                logger.debug(
                    "Retrieved SAML attributes from user mapping provider: %s "
                    "(attempt %d)",
                    attribute_dict,
                    i,
                )

                localpart = attribute_dict.get("mxid_localpart")
                if not localpart:
                    raise Exception(
                        "Error parsing SAML2 response: SAML mapping provider plugin "
                        "did not return a mxid_localpart value")

                displayname = attribute_dict.get("displayname")
                emails = attribute_dict.get("emails", [])

                # Check if this mxid already exists
                if not await self._datastore.get_users_by_id_case_insensitive(
                        UserID(localpart, self._hostname).to_string()):
                    # This mxid is free
                    break
            else:
                # Unable to generate a username in 1000 iterations
                # Break and return error to the user
                raise SynapseError(
                    500,
                    "Unable to generate a Matrix ID from the SAML response")

            logger.info("Mapped SAML user to local part %s", localpart)

            registered_user_id = await self._registration_handler.register_user(
                localpart=localpart,
                default_display_name=displayname,
                bind_emails=emails,
                user_agent_ips=(user_agent, ip_address),
            )

            await self._datastore.record_user_external_id(
                self._auth_provider_id, remote_user_id, registered_user_id)
            return registered_user_id, current_session
示例#22
0
    async def _complete_cas_login(
        self,
        cas_response: CasResponse,
        request: SynapseRequest,
        client_redirect_url: str,
    ) -> None:
        """
        Given a CAS response, complete the login flow

        Retrieves the remote user ID, registers the user if necessary, and serves
        a redirect back to the client with a login-token.

        Args:
            cas_response: The parsed CAS response.
            request: The request to respond to
            client_redirect_url: The redirect URL passed in by the client.

        Raises:
            MappingException if there was a problem mapping the response to a user.
            RedirectException: some mapping providers may raise this if they need
                to redirect to an interstitial page.
        """
        # Note that CAS does not support a mapping provider, so the logic is hard-coded.
        localpart = map_username_to_mxid_localpart(cas_response.username)

        async def cas_response_to_user_attributes(
                failures: int) -> UserAttributes:
            """
            Map from CAS attributes to user attributes.
            """
            # Due to the grandfathering logic matching any previously registered
            # mxids it isn't expected for there to be any failures.
            if failures:
                raise RuntimeError(
                    "CAS is not expected to de-duplicate Matrix IDs")

            display_name = cas_response.attributes.get(
                self._cas_displayname_attribute, None)

            return UserAttributes(localpart=localpart,
                                  display_name=display_name)

        async def grandfather_existing_users() -> Optional[str]:
            # Since CAS did not always use the user_external_ids table, always
            # to attempt to map to existing users.
            user_id = UserID(localpart, self._hostname).to_string()

            logger.debug(
                "Looking for existing account based on mapped %s",
                user_id,
            )

            users = await self._store.get_users_by_id_case_insensitive(user_id)
            if users:
                registered_user_id = list(users.keys())[0]
                logger.info("Grandfathering mapping to %s", registered_user_id)
                return registered_user_id

            return None

        await self._sso_handler.complete_sso_login_request(
            self.idp_id,
            cas_response.username,
            request,
            client_redirect_url,
            cas_response_to_user_attributes,
            grandfather_existing_users,
        )
示例#23
0
    async def _map_saml_response_to_user(self, resp_bytes):
        try:
            saml2_auth = self._saml_client.parse_authn_request_response(
                resp_bytes,
                saml2.BINDING_HTTP_POST,
                outstanding=self._outstanding_requests_dict,
            )
        except Exception as e:
            logger.warning("Exception parsing SAML2 response: %s", e)
            raise SynapseError(400,
                               "Unable to parse SAML2 response: %s" % (e, ))

        if saml2_auth.not_signed:
            logger.warning("SAML2 response was not signed")
            raise SynapseError(400, "SAML2 response was not signed")

        logger.info("SAML2 response: %s", saml2_auth.origxml)
        logger.info("SAML2 mapped attributes: %s", saml2_auth.ava)

        try:
            remote_user_id = saml2_auth.ava["uid"][0]
        except KeyError:
            logger.warning("SAML2 response lacks a 'uid' attestation")
            raise SynapseError(400, "uid not in SAML2 response")

        try:
            mxid_source = saml2_auth.ava[self._mxid_source_attribute][0]
        except KeyError:
            logger.warning("SAML2 response lacks a '%s' attestation",
                           self._mxid_source_attribute)
            raise SynapseError(
                400,
                "%s not in SAML2 response" % (self._mxid_source_attribute, ))

        self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)

        displayName = saml2_auth.ava.get("displayName", [None])[0]

        with (await self._mapping_lock.queue(self._auth_provider_id)):
            # first of all, check if we already have a mapping for this user
            logger.info(
                "Looking for existing mapping for user %s:%s",
                self._auth_provider_id,
                remote_user_id,
            )
            registered_user_id = await self._datastore.get_user_by_external_id(
                self._auth_provider_id, remote_user_id)
            if registered_user_id is not None:
                logger.info("Found existing mapping %s", registered_user_id)
                return registered_user_id

            # backwards-compatibility hack: see if there is an existing user with a
            # suitable mapping from the uid
            if (self._grandfathered_mxid_source_attribute
                    and self._grandfathered_mxid_source_attribute
                    in saml2_auth.ava):
                attrval = saml2_auth.ava[
                    self._grandfathered_mxid_source_attribute][0]
                user_id = UserID(map_username_to_mxid_localpart(attrval),
                                 self._hostname).to_string()
                logger.info(
                    "Looking for existing account based on mapped %s %s",
                    self._grandfathered_mxid_source_attribute,
                    user_id,
                )

                users = await self._datastore.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    registered_user_id = list(users.keys())[0]
                    logger.info("Grandfathering mapping to %s",
                                registered_user_id)
                    await self._datastore.record_user_external_id(
                        self._auth_provider_id, remote_user_id,
                        registered_user_id)
                    return registered_user_id

            # figure out a new mxid for this user
            base_mxid_localpart = self._mxid_mapper(mxid_source)

            suffix = 0
            while True:
                localpart = base_mxid_localpart + (str(suffix)
                                                   if suffix else "")
                if not await self._datastore.get_users_by_id_case_insensitive(
                        UserID(localpart, self._hostname).to_string()):
                    break
                suffix += 1
            logger.info("Allocating mxid for new user with localpart %s",
                        localpart)

            registered_user_id = await self._registration_handler.register_user(
                localpart=localpart, default_display_name=displayName)
            await self._datastore.record_user_external_id(
                self._auth_provider_id, remote_user_id, registered_user_id)
            return registered_user_id
示例#24
0
 def testLeadingUnderscore(self):
     self.assertEqual(map_username_to_mxid_localpart("_test_1234"),
                      "=5ftest_1234")
示例#25
0
        Retrieves the remote user ID, registers the user if necessary, and serves
        a redirect back to the client with a login-token.

        Args:
            cas_response: The parsed CAS response.
            request: The request to respond to
            client_redirect_url: The redirect URL passed in by the client.

        Raises:
            MappingException if there was a problem mapping the response to a user.
            RedirectException: some mapping providers may raise this if they need
                to redirect to an interstitial page.
        """
        # Note that CAS does not support a mapping provider, so the logic is hard-coded.
        localpart = map_username_to_mxid_localpart(cas_response.username)

        async def cas_response_to_user_attributes(failures: int) -> UserAttributes:
            """
            Map from CAS attributes to user attributes.
            """
            # Due to the grandfathering logic matching any previously registered
            # mxids it isn't expected for there to be any failures.
            if failures:
                raise RuntimeError("CAS is not expected to de-duplicate Matrix IDs")

            # Arbitrarily use the first attribute found.
            display_name = cas_response.attributes.get(
                self._cas_displayname_attribute, [None]
            )[0]