Пример #1
0
    def _handle_authn_request(self, context, binding_in, idp):
        """
        See doc for handle_authn_request method.

        :type context: satosa.context.Context
        :type binding_in: str
        :type idp: saml.server.Server
        :rtype: satosa.response.Response

        :param context: The current context
        :param binding_in: The pysaml binding type
        :param idp: The saml frontend idp server
        :return: response
        """
        req_info = idp.parse_authn_request(context.request["SAMLRequest"],
                                           binding_in)
        authn_req = req_info.message
        msg = "{}".format(authn_req)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        # keep the ForceAuthn value to be used by plugins
        context.decorate(Context.KEY_FORCE_AUTHN, authn_req.force_authn)

        try:
            resp_args = idp.response_args(authn_req)
        except SAMLError as e:
            msg = "Could not find necessary info about entity: {}".format(e)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.error(logline)
            return ServiceError("Incorrect request from requester: %s" % e)

        requester = resp_args["sp_entity_id"]
        context.state[self.name] = self._create_state_data(
            context, idp.response_args(authn_req),
            context.request.get("RelayState"))

        subject = authn_req.subject
        name_id_value = subject.name_id.text if subject else None

        nameid_formats = {
            "from_policy":
            authn_req.name_id_policy and authn_req.name_id_policy.format,
            "from_response":
            subject and subject.name_id and subject.name_id.format,
            "from_metadata":
            (idp.metadata[requester].get("spsso_descriptor",
                                         [{}])[0].get("name_id_format",
                                                      [{}])[0].get("text")),
            "default":
            NAMEID_FORMAT_TRANSIENT,
        }

        name_id_format = (
            nameid_formats["from_policy"]
            or (nameid_formats["from_response"] != NAMEID_FORMAT_UNSPECIFIED
                and nameid_formats["from_response"])
            or nameid_formats["from_metadata"]
            or nameid_formats["from_response"] or nameid_formats["default"])

        requester_name = self._get_sp_display_name(idp, requester)
        internal_req = InternalData(
            subject_id=name_id_value,
            subject_type=name_id_format,
            requester=requester,
            requester_name=requester_name,
        )

        idp_policy = idp.config.getattr("policy", "idp")
        if idp_policy:
            internal_req.attributes = self._get_approved_attributes(
                idp, idp_policy, requester, context.state)

        return self.auth_req_callback_func(context, internal_req)
Пример #2
0
    def _handle_authn_response(self, context, internal_response, idp):
        """
        See super class satosa.frontends.base.FrontendModule

        :type context: satosa.context.Context
        :type internal_response: satosa.internal.InternalData
        :type idp: saml.server.Server

        :param context: The current context
        :param internal_response: The internal response
        :param idp: The saml frontend idp server
        :return: A saml response
        """
        request_state = self.load_state(context.state)

        resp_args = request_state["resp_args"]
        sp_entity_id = resp_args["sp_entity_id"]
        internal_response.attributes = self._filter_attributes(
            idp, internal_response, context)
        ava = self.converter.from_internal(self.attribute_profile,
                                           internal_response.attributes)

        auth_info = {}
        if self.acr_mapping:
            auth_info["class_ref"] = self.acr_mapping.get(
                internal_response.auth_info.issuer, self.acr_mapping[""])
        else:
            auth_info["class_ref"] = internal_response.auth_info.auth_class_ref

        auth_info["authn_auth"] = internal_response.auth_info.issuer

        if self.custom_attribute_release:
            custom_release = util.get_dict_defaults(
                self.custom_attribute_release,
                internal_response.auth_info.issuer, sp_entity_id)
            attributes_to_remove = custom_release.get("exclude", [])
            for k in attributes_to_remove:
                ava.pop(k, None)

        nameid_value = internal_response.subject_id
        nameid_format = subject_type_to_saml_nameid_format(
            internal_response.subject_type)

        # If the backend did not receive a SAML <NameID> and so
        # name_id is set to None then do not create a NameID instance.
        # Instead pass None as the name name_id to the IdP server
        # instance and it will use its configured policy to construct
        # a <NameID>, with the default to create a transient <NameID>.
        name_id = None if not nameid_value else NameID(
            text=nameid_value,
            format=nameid_format,
            sp_name_qualifier=None,
            name_qualifier=None,
        )

        msg = "returning attributes {}".format(json.dumps(ava))
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        policies = self.idp_config.get('service',
                                       {}).get('idp', {}).get('policy', {})
        sp_policy = policies.get('default', {})
        sp_policy.update(policies.get(sp_entity_id, {}))

        sign_assertion = sp_policy.get('sign_assertion', False)
        sign_response = sp_policy.get('sign_response', True)
        sign_alg = sp_policy.get('sign_alg', 'SIG_RSA_SHA256')
        digest_alg = sp_policy.get('digest_alg', 'DIGEST_SHA256')
        encrypt_assertion = sp_policy.get('encrypt_assertion', False)
        encrypted_advice_attributes = sp_policy.get(
            'encrypted_advice_attributes', False)

        # Construct arguments for method create_authn_response
        # on IdP Server instance
        args = {
            'identity': ava,
            'name_id': name_id,
            'authn': auth_info,
            'sign_response': sign_response,
            'sign_assertion': sign_assertion,
            'encrypt_assertion': encrypt_assertion,
            'encrypted_advice_attributes': encrypted_advice_attributes
        }

        # Add the SP details
        args.update(**resp_args)

        try:
            args['sign_alg'] = getattr(xmldsig, sign_alg)
        except AttributeError as e:
            msg = "Unsupported sign algorithm {}".format(sign_alg)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.error(logline)
            raise Exception(msg) from e
        else:
            msg = "signing with algorithm {}".format(args['sign_alg'])
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

        try:
            args['digest_alg'] = getattr(xmldsig, digest_alg)
        except AttributeError as e:
            msg = "Unsupported digest algorithm {}".format(digest_alg)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.error(logline)
            raise Exception(msg) from e
        else:
            msg = "using digest algorithm {}".format(args['digest_alg'])
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

        resp = idp.create_authn_response(**args)
        http_args = idp.apply_binding(resp_args["binding"],
                                      str(resp),
                                      resp_args["destination"],
                                      request_state["relay_state"],
                                      response=True)

        # Set the common domain cookie _saml_idp if so configured.
        if self.config.get('common_domain_cookie'):
            self._set_common_domain_cookie(internal_response, http_args,
                                           context)

        del context.state[self.name]
        return make_saml_response(resp_args["binding"], http_args)
Пример #3
0
    def _set_common_domain_cookie(self, internal_response, http_args, context):
        """
        """
        # Find any existing common domain cookie and deconsruct it to
        # obtain the list of IdPs.
        cookie = SimpleCookie(context.cookie)
        if '_saml_idp' in cookie:
            common_domain_cookie = cookie['_saml_idp']
            msg = "Found existing common domain cookie {}".format(
                common_domain_cookie)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)
            space_separated_b64_idp_string = unquote(
                common_domain_cookie.value)
            b64_idp_list = space_separated_b64_idp_string.split()
            idp_list = [
                urlsafe_b64decode(b64_idp).decode('utf-8')
                for b64_idp in b64_idp_list
            ]
        else:
            msg = "No existing common domain cookie found"
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)
            idp_list = []

        msg = "Common domain cookie list of IdPs is {}".format(idp_list)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        # Identity the current IdP just used for authentication in this flow.
        this_flow_idp = internal_response.auth_info.issuer

        # Remove all occurrences of the current IdP from the list of IdPs.
        idp_list = [idp for idp in idp_list if idp != this_flow_idp]

        # Append the current IdP.
        idp_list.append(this_flow_idp)
        msg = "Added IdP {} to common domain cookie list of IdPs".format(
            this_flow_idp)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)
        msg = "Common domain cookie list of IdPs is now {}".format(idp_list)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        # Construct the cookie.
        b64_idp_list = [
            urlsafe_b64encode(idp.encode()).decode("utf-8") for idp in idp_list
        ]
        space_separated_b64_idp_string = " ".join(b64_idp_list)
        url_encoded_space_separated_b64_idp_string = quote(
            space_separated_b64_idp_string)

        cookie = SimpleCookie()
        cookie['_saml_idp'] = url_encoded_space_separated_b64_idp_string
        cookie['_saml_idp']['path'] = '/'

        # Use the domain from configuration if present else use the domain
        # from the base URL for the front end.
        domain = urlparse(self.base_url).netloc
        if isinstance(self.config['common_domain_cookie'], dict):
            if 'domain' in self.config['common_domain_cookie']:
                domain = self.config['common_domain_cookie']['domain']

        # Ensure that the domain begins with a '.'
        if domain[0] != '.':
            domain = '.' + domain

        cookie['_saml_idp']['domain'] = domain
        cookie['_saml_idp']['secure'] = True

        # Set the cookie.
        msg = "Setting common domain cookie with {}".format(cookie.output())
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)
        http_args['headers'].append(tuple(cookie.output().split(": ", 1)))
Пример #4
0
    def constructPrimaryIdentifier(self, data, ordered_identifier_candidates):
        """
        Construct and return a primary identifier value from the
        data asserted by the IdP using the ordered list of candidates
        from the configuration.
        """
        logprefix = PrimaryIdentifier.logprefix
        context = self.context

        attributes = data.attributes
        msg = "{} Input attributes {}".format(logprefix, attributes)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        value = None

        for candidate in ordered_identifier_candidates:
            msg = "{} Considering candidate {}".format(logprefix, candidate)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

            # Get the values asserted by the IdP for the configured list of attribute names for this candidate
            # and substitute None if the IdP did not assert any value for a configured attribute.
            values = [
                attributes.get(attribute_name, [None])[0]
                for attribute_name in candidate['attribute_names']
                if attribute_name != 'name_id'
            ]
            msg = "{} Found candidate values {}".format(logprefix, values)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

            # If one of the configured attribute names is name_id then if there is also a configured
            # name_id_format add the value for the NameID of that format if it was asserted by the IdP
            # or else add the value None.
            if 'name_id' in candidate['attribute_names']:
                candidate_nameid_value = None
                candidate_nameid_value = None
                candidate_name_id_format = candidate.get('name_id_format')
                name_id_value = data.subject_id
                name_id_format = data.subject_type
                if (name_id_value and candidate_name_id_format
                        and candidate_name_id_format == name_id_format):
                    msg = "{} IdP asserted NameID {}".format(
                        logprefix, name_id_value)
                    logline = lu.LOG_FMT.format(id=lu.get_session_id(
                        context.state),
                                                message=msg)
                    logger.debug(logline)
                    candidate_nameid_value = name_id_value

                # Only add the NameID value asserted by the IdP if it is not already
                # in the list of values. This is necessary because some non-compliant IdPs
                # have been known, for example, to assert the value of eduPersonPrincipalName
                # in the value for SAML2 persistent NameID as well as asserting
                # eduPersonPrincipalName.
                if candidate_nameid_value not in values:
                    msg = "{} Added NameID {} to candidate values".format(
                        logprefix, candidate_nameid_value)
                    logline = lu.LOG_FMT.format(id=lu.get_session_id(
                        context.state),
                                                message=msg)
                    logger.debug(logline)
                    values.append(candidate_nameid_value)
                else:
                    msg = "{} NameID {} value also asserted as attribute value".format(
                        logprefix, candidate_nameid_value)
                    logline = logline = lu.LOG_FMT.format(id=lu.get_session_id(
                        context.state),
                                                          message=msg)
                    logger.warn(logline)

            # If no value was asserted by the IdP for one of the configured list of attribute names
            # for this candidate then go onto the next candidate.
            if None in values:
                msg = "{} Candidate is missing value so skipping".format(
                    logprefix)
                logline = logline = lu.LOG_FMT.format(id=lu.get_session_id(
                    context.state),
                                                      message=msg)
                logger.debug(logline)
                continue

            # All values for the configured list of attribute names are present
            # so we can create a primary identifer. Add a scope if configured
            # to do so.
            if 'add_scope' in candidate:
                if candidate['add_scope'] == 'issuer_entityid':
                    scope = data.auth_info.issuer
                else:
                    scope = candidate['add_scope']
                msg = "{} Added scope {} to values".format(logprefix, scope)
                logline = lu.LOG_FMT.format(id=lu.get_session_id(
                    context.state),
                                            message=msg)
                logger.debug(logline)
                values.append(scope)

            # Concatenate all values to create the primary identifier.
            value = ''.join(values)
            break

        return value
Пример #5
0
    def process(self, context, data):
        logprefix = PrimaryIdentifier.logprefix
        self.context = context

        # Initialize the configuration to use as the default configuration
        # that is passed during initialization.
        config = self.config

        msg = "{} Using default configuration {}".format(logprefix, config)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        # Find the entityID for the SP that initiated the flow
        try:
            spEntityID = context.state.state_dict['SATOSA_BASE']['requester']
        except KeyError as err:
            msg = "{} Unable to determine the entityID for the SP requester".format(
                logprefix)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.error(logline)
            return super().process(context, data)

        msg = "{} entityID for the SP requester is {}".format(
            logprefix, spEntityID)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)

        # Find the entityID for the IdP that issued the assertion
        try:
            idpEntityID = data.auth_info.issuer
        except KeyError as err:
            msg = "{} Unable to determine the entityID for the IdP issuer".format(
                logprefix)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.error(logline)
            return super().process(context, data)

        # Examine our configuration to determine if there is a per-IdP configuration
        if idpEntityID in self.config:
            config = self.config[idpEntityID]
            msg = "{} For IdP {} using configuration {}".format(
                logprefix, idpEntityID, config)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

        # Examine our configuration to determine if there is a per-SP configuration.
        # An SP configuration overrides an IdP configuration when there is a conflict.
        if spEntityID in self.config:
            config = self.config[spEntityID]
            msg = "{} For SP {} using configuration {}".format(
                logprefix, spEntityID, config)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

        # Obtain configuration details from the per-SP configuration or the default configuration
        try:
            if 'ordered_identifier_candidates' in config:
                ordered_identifier_candidates = config[
                    'ordered_identifier_candidates']
            else:
                ordered_identifier_candidates = self.config[
                    'ordered_identifier_candidates']
            if 'primary_identifier' in config:
                primary_identifier = config['primary_identifier']
            elif 'primary_identifier' in self.config:
                primary_identifier = self.config['primary_identifier']
            else:
                primary_identifier = 'uid'
            if 'clear_input_attributes' in config:
                clear_input_attributes = config['clear_input_attributes']
            elif 'clear_input_attributes' in self.config:
                clear_input_attributes = self.config['clear_input_attributes']
            else:
                clear_input_attributes = False
            if 'replace_subject_id' in config:
                replace_subject_id = config['replace_subject_id']
            elif 'replace_subject_id' in self.config:
                replace_subject_id = self.config['replace_subject_id']
            else:
                replace_subject_id = False
            if 'ignore' in config:
                ignore = True
            else:
                ignore = False
            if 'on_error' in config:
                on_error = config['on_error']
            elif 'on_error' in self.config:
                on_error = self.config['on_error']
            else:
                on_error = None

        except KeyError as err:
            msg = "{} Configuration '{}' is missing".format(logprefix, err)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.error(logline)
            return super().process(context, data)

        # Ignore this SP entirely if so configured.
        if ignore:
            msg = "{} Ignoring SP {}".format(logprefix, spEntityID)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.info(logline)
            return super().process(context, data)

        # Construct the primary identifier.
        msg = "{} Constructing primary identifier".format(logprefix)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)
        primary_identifier_val = self.constructPrimaryIdentifier(
            data, ordered_identifier_candidates)

        if not primary_identifier_val:
            msg = "{} No primary identifier found".format(logprefix)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.warn(logline)
            if on_error:
                # Redirect to the configured error handling service with
                # the entityIDs for the target SP and IdP used by the user
                # as query string parameters (URL encoded).
                encodedSpEntityID = urllib.parse.quote_plus(spEntityID)
                encodedIdpEntityID = urllib.parse.quote_plus(
                    data.auth_info.issuer)
                url = "{}?sp={}&idp={}".format(on_error, encodedSpEntityID,
                                               encodedIdpEntityID)
                msg = "{} Redirecting to {}".format(logprefix, url)
                logline = lu.LOG_FMT.format(id=lu.get_session_id(
                    context.state),
                                            message=msg)
                logger.info(logline)
                return Redirect(url)

        msg = "{} Found primary identifier: {}".format(logprefix,
                                                       primary_identifier_val)
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.info(logline)

        # Clear input attributes if so configured.
        if clear_input_attributes:
            msg = "{} Clearing values for these input attributes: {}".format(
                logprefix, data.attributes.keys())
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)
            data.attributes = {}

        if primary_identifier:
            # Set the primary identifier attribute to the value found.
            data.attributes[primary_identifier] = primary_identifier_val
            msg = "{} Setting attribute {} to value {}".format(
                logprefix, primary_identifier, primary_identifier_val)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

        # Replace subject_id with the constructed primary identifier if so configured.
        if replace_subject_id:
            msg = "{} Setting subject_id to value {}".format(
                logprefix, primary_identifier_val)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)
            data.subject_id = primary_identifier_val

        msg = "{} returning data.attributes {}".format(logprefix,
                                                       str(data.attributes))
        logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                    message=msg)
        logger.debug(logline)
        return super().process(context, data)
Пример #6
0
    def process(self, context, data):
        """
        Default interface for microservices. Process the input data for
        the input context.
        """
        state = context.state
        session_id = lu.get_session_id(state)

        requester = data.requester
        issuer = data.auth_info.issuer

        frontend_name = state.get(ROUTING_STATE_KEY)
        co_entity_id_key = SAMLVirtualCoFrontend.KEY_CO_ENTITY_ID
        co_entity_id = state.get(frontend_name, {}).get(co_entity_id_key)

        entity_ids = [requester, issuer, co_entity_id, "default"]

        config, entity_id = next(
            (self.config.get(e), e) for e in entity_ids if self.config.get(e))

        msg = {
            "message": "entityID for the involved entities",
            "requester": requester,
            "issuer": issuer,
            "config": self._filter_config(config),
        }
        if co_entity_id:
            msg["co_entity_id"] = co_entity_id
        logline = lu.LOG_FMT.format(id=session_id, message=msg)
        logger.debug(logline)

        # Ignore this entityID entirely if so configured.
        if config["ignore"]:
            msg = "Ignoring entityID {}".format(entity_id)
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.info(logline)
            return super().process(context, data)

        # The list of values for the LDAP search filters that will be tried in
        # order to find the LDAP directory record for the user.
        filter_values = [
            filter_value
            for candidate in config["ordered_identifier_candidates"]
            # Consider and find asserted values to construct the ordered list
            # of values for the LDAP search filters.
            for filter_value in [
                self._construct_filter_value(
                    candidate,
                    data.subject_id,
                    data.subject_type,
                    issuer,
                    data.attributes,
                )
            ]
            # If we have constructed a non empty value then add it as the next
            # filter value to use when searching for the user record.
            if filter_value
        ]
        msg = {"message": "Search filters", "filter_values": filter_values}
        logline = lu.LOG_FMT.format(id=session_id, message=msg)
        logger.debug(logline)

        # Initialize an empty LDAP record. The first LDAP record found using
        # the ordered # list of search filter values will be the record used.
        record = None
        results = None
        exp_msg = None

        connection = config["connection"]
        msg = {
            "message": "LDAP server host",
            "server host": connection.server.host,
        }
        logline = lu.LOG_FMT.format(id=session_id, message=msg)
        logger.debug(logline)

        for filter_val in filter_values:
            ldap_ident_attr = config["ldap_identifier_attribute"]
            search_filter = "({0}={1})".format(ldap_ident_attr, filter_val)
            msg = {
                "message": "LDAP query with constructed search filter",
                "search filter": search_filter,
            }
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.debug(logline)

            attributes = (
                config["query_return_attributes"]
                if config["query_return_attributes"]
                # Deprecated configuration. Will be removed in future.
                else config["search_return_attributes"].keys())
            try:
                results = connection.search(config["search_base"],
                                            search_filter,
                                            attributes=attributes)
            except LDAPException as err:
                exp_msg = "Caught LDAP exception: {}".format(err)
            except LdapAttributeStoreError as err:
                exp_msg = "Caught LDAP Attribute Store exception: {}"
                exp_msg = exp_msg.format(err)
            except Exception as err:
                exp_msg = "Caught unhandled exception: {}".format(err)

            if exp_msg:
                logline = lu.LOG_FMT.format(id=session_id, message=exp_msg)
                logger.error(logline)
                return super().process(context, data)

            if not results:
                msg = "Querying LDAP server: No results for {}."
                msg = msg.format(filter_val)
                logline = lu.LOG_FMT.format(id=session_id, message=msg)
                logger.debug(logline)
                continue

            if isinstance(results, bool):
                responses = connection.entries
            else:
                responses = connection.get_response(results)[0]

            msg = "Done querying LDAP server"
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.debug(logline)
            msg = "LDAP server returned {} records".format(len(responses))
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.info(logline)

            # For now consider only the first record found (if any).
            if len(responses) > 0:
                if len(responses) > 1:
                    msg = "LDAP server returned {} records using search filter"
                    msg = msg + " value {}"
                    msg = msg.format(len(responses), filter_val)
                    logline = lu.LOG_FMT.format(id=session_id, message=msg)
                    logger.warning(logline)
                record = responses[0]
                break

        # Before using a found record, if any, to populate attributes
        # clear any attributes incoming to this microservice if so configured.
        if config["clear_input_attributes"]:
            msg = "Clearing values for these input attributes: {}"
            msg = msg.format(data.attributes)
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.debug(logline)
            data.attributes = {}

        # This adapts records with different search and connection strategy
        # (sync without pool), it should be tested with anonimous bind with
        # message_id.
        if isinstance(results, bool) and record:
            record = {
                "dn":
                record.entry_dn if hasattr(record, "entry_dn") else "",
                "attributes": (record.entry_attributes_as_dict if hasattr(
                    record, "entry_attributes_as_dict") else {}),
            }

        # Use a found record, if any, to populate attributes and input for
        # NameID
        if record:
            msg = {
                "message": "Using record with DN and attributes",
                "DN": record["dn"],
                "attributes": record["attributes"],
            }
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.debug(logline)

            # Populate attributes as configured.
            new_attrs = self._populate_attributes(config, record)

            overwrite = config["overwrite_existing_attributes"]
            for attr, values in new_attrs.items():
                if not overwrite:
                    values = list(set(data.attributes.get(attr, []) + values))
                data.attributes[attr] = values

            # Populate input for NameID if configured. SATOSA core does the
            # hashing of input to create a persistent NameID.
            user_ids = self._populate_input_for_name_id(config, record, data)
            if user_ids:
                data.subject_id = "".join(user_ids)
            msg = "NameID value is {}".format(data.subject_id)
            logger.debug(msg)

            # Add the record to the context so that later microservices
            # may use it if required.
            context.decorate(KEY_FOUND_LDAP_RECORD, record)
            msg = "Added record {} to context".format(record)
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.debug(logline)
        else:
            msg = "No record found in LDAP so no attributes will be added"
            logline = lu.LOG_FMT.format(id=session_id, message=msg)
            logger.warning(logline)
            on_ldap_search_result_empty = config["on_ldap_search_result_empty"]
            if on_ldap_search_result_empty:
                # Redirect to the configured URL with
                # the entityIDs for the target SP and IdP used by the user
                # as query string parameters (URL encoded).
                encoded_sp_entity_id = urllib.parse.quote_plus(requester)
                encoded_idp_entity_id = urllib.parse.quote_plus(issuer)
                url = "{}?sp={}&idp={}".format(
                    on_ldap_search_result_empty,
                    encoded_sp_entity_id,
                    encoded_idp_entity_id,
                )
                msg = "Redirecting to {}".format(url)
                logline = lu.LOG_FMT.format(id=session_id, message=msg)
                logger.info(logline)
                return Redirect(url)

        msg = "Returning data.attributes {}".format(data.attributes)
        logline = lu.LOG_FMT.format(id=session_id, message=msg)
        logger.debug(logline)
        return super().process(context, data)
Пример #7
0
    def authn_request(self, context, entity_id):
        """
        Do an authorization request on idp with given entity id.
        This is the start of the authorization.

        :type context: satosa.context.Context
        :type entity_id: str
        :rtype: satosa.response.Response

        :param context: The current context
        :param entity_id: Target IDP entity id
        :return: response to the user agent
        """

        # If IDP blacklisting is enabled and the selected IDP is blacklisted,
        # stop here
        if self.idp_blacklist_file:
            with open(self.idp_blacklist_file) as blacklist_file:
                blacklist_array = json.load(blacklist_file)['blacklist']
                if entity_id in blacklist_array:
                    msg = "IdP with EntityID {} is blacklisted".format(
                        entity_id)
                    logline = lu.LOG_FMT.format(id=lu.get_session_id(
                        context.state),
                                                message=msg)
                    logger.debug(logline, exc_info=False)
                    raise SATOSAAuthenticationError(
                        context.state,
                        "Selected IdP is blacklisted for this backend")

        kwargs = {}
        authn_context = self.construct_requested_authn_context(entity_id)
        if authn_context:
            kwargs["requested_authn_context"] = authn_context
        if self.config.get(SAMLBackend.KEY_MIRROR_FORCE_AUTHN):
            kwargs["force_authn"] = get_force_authn(context, self.config,
                                                    self.sp.config)

        try:
            binding, destination = self.sp.pick_binding(
                "single_sign_on_service", None, "idpsso", entity_id=entity_id)
            msg = "binding: {}, destination: {}".format(binding, destination)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)

            acs_endp, response_binding = self.sp.config.getattr(
                "endpoints", "sp")["assertion_consumer_service"][0]
            req_id, req = self.sp.create_authn_request(
                destination, binding=response_binding, **kwargs)
            relay_state = util.rndstr()
            ht_args = self.sp.apply_binding(binding,
                                            "%s" % req,
                                            destination,
                                            relay_state=relay_state)
            msg = "ht_args: {}".format(ht_args)
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline)
        except Exception as exc:
            msg = "Failed to construct the AuthnRequest for state"
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state),
                                        message=msg)
            logger.debug(logline, exc_info=True)
            raise SATOSAAuthenticationError(
                context.state, "Failed to construct the AuthnRequest") from exc

        if self.sp.config.getattr('allow_unsolicited', 'sp') is False:
            if req_id in self.outstanding_queries:
                msg = "Request with duplicate id {}".format(req_id)
                logline = lu.LOG_FMT.format(id=lu.get_session_id(
                    context.state),
                                            message=msg)
                logger.debug(logline)
                raise SATOSAAuthenticationError(context.state, msg)
            self.outstanding_queries[req_id] = req

        context.state[self.name] = {"relay_state": relay_state}
        return make_saml_response(binding, ht_args)