Exemplo n.º 1
0
    def test_empty_input(self):
        parts = chunk_seq([], 5)

        self.assertEqual(
            list(parts),
            [],
        )
Exemplo n.º 2
0
    def test_uneven_parts(self):
        parts = chunk_seq("abcdefghijklmnop", 5)

        self.assertEqual(
            list(parts),
            ["abcde", "fghij", "klmno", "p"],
        )
Exemplo n.º 3
0
    def test_long_seq(self):
        parts = chunk_seq("abcdefghijklmnop", 8)

        self.assertEqual(
            list(parts),
            ["abcdefgh", "ijklmnop"],
        )
Exemplo n.º 4
0
    def test_short_seq(self):
        parts = chunk_seq("123", 8)

        self.assertEqual(
            list(parts),
            ["123"],
        )
Exemplo n.º 5
0
    def test_empty_input(self):
        parts = chunk_seq([], 5)  # type: Iterable[Sequence]

        self.assertEqual(
            list(parts),
            [],
        )
Exemplo n.º 6
0
def _write_bytes_to_request(request: Request, bytes_to_write: bytes) -> None:
    """Writes the bytes to the request using an appropriate producer.

    Note: This should be used instead of `Request.write` to correctly handle
    large response bodies.
    """

    # The problem with dumping all of the response into the `Request` object at
    # once (via `Request.write`) is that doing so starts the timeout for the
    # next request to be received: so if it takes longer than 60s to stream back
    # the response to the client, the client never gets it.
    #
    # The correct solution is to use a Producer; then the timeout is only
    # started once all of the content is sent over the TCP connection.

    # To make sure we don't write all of the bytes at once we split it up into
    # chunks.
    chunk_size = 4096
    bytes_generator = chunk_seq(bytes_to_write, chunk_size)

    # We use a `_ByteProducer` here rather than `NoRangeStaticProducer` as the
    # unit tests can't cope with being given a pull producer.
    _ByteProducer(request, bytes_generator)
Exemplo n.º 7
0
    async def _handle_authn_response(
        self,
        request: SynapseRequest,
        saml2_auth: saml2.response.AuthnResponse,
        relay_state: str,
    ) -> None:
        """Handle an AuthnResponse, having parsed it from the request params

        Assumes that the signature on the response object has been checked. Maps
        the user onto an MXID, registering them if necessary, and returns a response
        to the browser.

        Args:
            request: the incoming request from the browser. We'll respond to it with an
                HTML page or a redirect
            saml2_auth: the parsed AuthnResponse object
            relay_state: the RelayState query param, which encodes the URI to rediret
               back to
        """

        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)

        # first check if we're doing a UIA
        if current_session and current_session.ui_auth_session_id:
            try:
                remote_user_id = self._remote_id_from_saml_response(
                    saml2_auth, None)
            except MappingException as e:
                logger.exception(
                    "Failed to extract remote user id from SAML response")
                self._sso_handler.render_error(request, "mapping_error",
                                               str(e))
                return

            return await self._sso_handler.complete_sso_ui_auth_request(
                self.idp_id,
                remote_user_id,
                current_session.ui_auth_session_id,
                request,
            )

        # otherwise, we're handling a login request.

        # Ensure that the attributes of the logged in user meet the required
        # attributes.
        if not self._sso_handler.check_required_attributes(
                request, saml2_auth.ava, self._saml2_attribute_requirements):
            return

        # Call the mapper to register/login the user
        try:
            await self._complete_saml_login(saml2_auth, request, relay_state)
        except MappingException as e:
            logger.exception("Could not map user")
            self._sso_handler.render_error(request, "mapping_error", str(e))
Exemplo n.º 8
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
Exemplo n.º 9
0
    async def handle_saml_response(self, request: SynapseRequest) -> None:
        """Handle an incoming request to /_matrix/saml2/authn_response

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

        Returns:
            Completes once we have handled the request.
        """
        resp_bytes = parse_string(request, "SAMLResponse", required=True)
        relay_state = parse_string(request, "RelayState", required=True)

        # expire outstanding sessions before parse_authn_request_response checks
        # the dict.
        self.expire_sessions()

        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))
            self._render_error(request, "unsolicited_response",
                               "Unexpected SAML2 login.")
            return
        except Exception as e:
            self._render_error(
                request,
                "invalid_response",
                "Unable to parse SAML2 response: %s." % (e, ),
            )
            return

        if saml2_auth.not_signed:
            self._render_error(request, "unsigned_respond",
                               "SAML2 response was not signed.")
            return

        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)

        # Ensure that the attributes of the logged in user meet the required
        # attributes.
        for requirement in self._saml2_attribute_requirements:
            if not _check_attribute_requirement(saml2_auth.ava, requirement):
                self._render_error(request, "unauthorised",
                                   "You are not authorised to log in here.")
                return

        # Pull out the user-agent and IP from the request.
        user_agent = request.requestHeaders.getRawHeaders(
            b"User-Agent", default=[b""])[0].decode("ascii", "surrogateescape")
        ip_address = self.hs.get_ip_from_request(request)

        # Call the mapper to register/login the user
        try:
            user_id = await self._map_saml_response_to_user(
                saml2_auth, relay_state, user_agent, ip_address)
        except MappingException as e:
            logger.exception("Could not map user")
            self._render_error(request, "mapping_error", str(e))
            return

        # Complete the interactive auth session or the login.
        if current_session and current_session.ui_auth_session_id:
            await self._auth_handler.complete_sso_ui_auth(
                user_id, current_session.ui_auth_session_id, request)

        else:
            await self._auth_handler.complete_sso_login(
                user_id, request, relay_state)
Exemplo n.º 10
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