Example #1
0
    def _approve_new_consent(self, context, internal_response, id_hash):
        context.state[STATE_KEY]["internal_resp"] = internal_response.to_dict()

        consent_args = {
            "attr": internal_response.attributes,
            "id": id_hash,
            "redirect_endpoint": "%s/consent%s" % (self.base_url, self.endpoint),
            "requester_name": internal_response.requester_name,
        }
        if self.locked_attr:
            consent_args["locked_attrs"] = [self.locked_attr]
        if 'requester_logo' in context.state[STATE_KEY]:
             consent_args["requester_logo"] = context.state[STATE_KEY]['requester_logo']
        try:
            ticket = self._consent_registration(consent_args)
        except (ConnectionError, UnexpectedResponseError) as e:
            msg = "Consent request failed, no consent given: {}".format(str(e))
            logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
            logger.error(logline)
            # Send an internal_response without any attributes
            internal_response.attributes = {}
            return self._end_consent(context, internal_response)

        consent_redirect = "%s/%s" % (self.redirect_url, ticket)
        return Redirect(consent_redirect)
Example #2
0
 def _check_requirement(self, context, internal_response):
     """
     Assume BreakOut is only needed under certain conditions
     This is an example method that checks condition
     """
     logger.info("Check BreakOut requirement")
     if self.resumed:
         # If everything is ok, don't break out and continue
         return super().process(context, internal_response)
     else:
         # This is actual breakout redirect
         return Redirect(self.redirect_url)
Example #3
0
    def start_auth(self, context, internal_request, get_state=stateID):
        """
        See super class method satosa.backends.base#start_auth
        :param get_state: Generates a state to be used in the authentication call.

        :type get_state: Callable[[str, bytes], str]
        :type context: satosa.context.Context
        :type internal_request: satosa.internal.InternalData
        :rtype satosa.response.Redirect
        """
        request_args = self.get_request_args(get_state=get_state)
        context.state[self.name] = {"state": request_args["state"]}
        cis = self.consumer.construct_AuthorizationRequest(
            request_args=request_args)
        return Redirect(cis.request(self.consumer.authorization_endpoint))
Example #4
0
    def start_auth(self, context, internal_request, get_state=stateID):
        """
        :param get_state: Generates a state to be used in authentication call

        :type get_state: Callable[[str, bytes], str]
        :type context: satosa.context.Context
        :type internal_request: satosa.internal.InternalData
        :rtype satosa.response.Redirect
        """
        request_args = dict(
            client_id=self.config['client_config']['client_id'],
            redirect_uri=self.redirect_url,
            scope=' '.join(self.config['scope']),
        )
        cis = self.consumer.construct_AuthorizationRequest(
            request_args=request_args)
        return Redirect(cis.request(self.consumer.authorization_endpoint))
Example #5
0
    def create_authn_request(self,
                             state,
                             oidc_state,
                             nonce,
                             acr_value=None,
                             **kwargs):
        """
        Creates an oidc authentication request.
        :type state: satosa.state.State
        :type oidc_state: str
        :type nonce: str
        :type acr_value: list[str]
        :type kwargs: Any
        :rtype: satosa.response.Redirect

        :param state: Module state
        :param oidc_state: OIDC state
        :param nonce: A nonce
        :param acr_value: Authentication type
        :param kwargs: Whatever
        :return: A redirect to the OP
        """
        request_args = self.setup_authn_request_args(acr_value, kwargs,
                                                     oidc_state, nonce)

        cis = self.construct_AuthorizationRequest(request_args=request_args)
        satosa_logging(LOGGER, logging.DEBUG, "request: %s" % cis, state)

        url, body, ht_args, cis = self.uri_and_body(AuthorizationRequest,
                                                    cis,
                                                    method="GET",
                                                    request_args=request_args)

        satosa_logging(LOGGER, logging.DEBUG, "body: %s" % body, state)
        satosa_logging(LOGGER, logging.INFO, "URL: %s" % url, state)
        satosa_logging(LOGGER, logging.DEBUG, "ht_args: %s" % ht_args, state)

        resp = Redirect(str(url))
        if ht_args:
            resp.headers.extend([(a, b) for a, b in ht_args.items()])
        satosa_logging(LOGGER, logging.DEBUG,
                       "resp_headers: %s" % resp.headers, state)
        return resp
Example #6
0
    def process(self, context, internal_response):
        """
        Ask the user for consent of data to be released.
        :param context: request context
        :param internal_response: the internal response
        """
        transaction_log(context.state,
                        self.config.get("process_entry_order",
                                        700), "user_consent", "process",
                        "entry", "success", '', '', 'Requesting consent')

        consent_state = context.state[STATE_KEY]
        internal_response.attributes = {
            k: v
            for k, v in internal_response.attributes.items()
            if k in consent_state['filter']
        }
        consent_state['internal_response'] = internal_response.to_dict()
        handler = '/consent{}'.format(self.endpoint)
        return Redirect(handler)
Example #7
0
    def _approve_new_id(self, context, internal_response, ticket):
        """
        Redirect the user to approve the new id

        :type context: satosa.context.Context
        :type internal_response: satosa.internal_data.InternalResponse
        :type ticket: str
        :rtype: satosa.response.Redirect

        :param context: The current context
        :param internal_response: The internal response
        :param ticket: The ticket given by the al service
        :return: A redirect to approve the new id linking
        """
        satosa_logging(LOGGER, logging.INFO,
                       "A new ID must be linked by the AL service",
                       context.state)
        context.state.add(AccountLinkingModule.STATE_KEY,
                          internal_response.to_dict())
        return Redirect("%s/%s" % (self.al_redirect, ticket))
Example #8
0
    def start_auth(self, context, internal_request, get_state=stateID):
        """
        See super class method satosa.backends.base#start_auth
        :param get_state: Generates a state to be used in the authentication call.

        :type get_state: Callable[[str, bytes], str]
        :type context: satosa.context.Context
        :type internal_request: satosa.internal.InternalData
        :rtype satosa.response.Redirect
        """
        oauth_state = get_state(self.config["base_url"], rndstr().encode())

        state_data = dict(state=oauth_state)
        context.state[self.name] = state_data

        request_args = {
            "redirect_uri": self.redirect_url,
            "state": oauth_state
        }
        cis = self.consumer.construct_AuthorizationRequest(
            request_args=request_args)
        return Redirect(cis.request(self.consumer.authorization_endpoint))
Example #9
0
    def start_auth(self, context, internal_request, get_state=stateID):
        """
        :param get_state: Generates a state to be used in authentication call

        :type get_state: Callable[[str, bytes], str]
        :type context: satosa.context.Context
        :type internal_request: satosa.internal.InternalData
        :rtype satosa.response.Redirect
        """
        oauth_state = get_state(self.config["base_url"], rndstr().encode())
        context.state[self.name] = dict(state=oauth_state)

        request_args = dict(
            response_type='code',
            client_id=self.config['client_config']['client_id'],
            redirect_uri=self.redirect_url,
            state=oauth_state)
        scope = ' '.join(self.config['scope'])
        if scope:
            request_args['scope'] = scope

        cis = self.consumer.construct_AuthorizationRequest(
            request_args=request_args)
        return Redirect(cis.request(self.consumer.authorization_endpoint))
Example #10
0
    def start_auth(self, context, internal_request, get_state=stateID):
        """
        See super class method satosa.backends.base#start_auth
        :param get_state: Generates a state to be used in the authentication call.

        :type get_state: (str, bytes) -> str
        :type context: satosa.context.Context
        :type internal_request: satosa.internal_data.InternalRequest
        :rtype satosa.response.Redirect
        """
        consumer = self.get_consumer()
        request_args = {
            "redirect_uri": self.redirect_url,
            "state": get_state(self.config["base_url"],
                               rndstr().encode())
        }
        state_data = {"state": request_args["state"]}
        state = context.state
        state.add(self.config["state_id"], state_data)
        cis = consumer.construct_AuthorizationRequest(
            request_args=request_args)
        url, body, ht_args, cis = consumer.uri_and_body(
            AuthorizationRequest, cis, method="GET", request_args=request_args)
        return Redirect(url)
Example #11
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 '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.attribute_names)
            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)

        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)
    def process(self, context, data):
        """
        Default interface for microservices. Process the input data for
        the input context.
        """
        self.context = context

        # Find the entityID for the SP that initiated the flow.
        try:
            sp_entity_id = context.state.state_dict['SATOSA_BASE']['requester']
        except KeyError as err:
            msg = "Unable to determine the entityID for the SP requester"
            satosa_logging(logger, logging.ERROR, msg, context.state)
            raise NyuLangoneAttributeStoreError(msg)

        satosa_logging(logger, logging.DEBUG, "entityID for the SP requester is {}".format(sp_entity_id), context.state)

        # Get the configuration for the SP.
        if sp_entity_id in self.config.keys():
            config = self.config[sp_entity_id]
        else:
            config = self.config['default']

        satosa_logging(logger, logging.DEBUG, "Using config {}".format(self._filter_config(config)), context.state)

        # Ignore this SP entirely if so configured.
        if config['ignore']:
            satosa_logging(logger, logging.INFO, "Ignoring SP {}".format(sp_entity_id), None)
            return super().process(context, data)

        # Find the entityID for the IdP used for authentication.
        try:
            idp_entity_id = data.to_dict()['auth_info']['issuer']
        except KeyError as err:
            msg = "Unable to determine the entityID for the IdP issuer"
            satosa_logging(logger, logging.ERROR, msg, context.state)
            raise NyuLangoneAttributeStoreError(msg)

        satosa_logging(logger, logging.DEBUG, "entityID for the authenticating IdP is {}".format(idp_entity_id), context.state)

        # If the IdP used for authentication is the NYU Langone IdP then strip
        # off the scope and assert the remaining part of the identifier.
        if idp_entity_id == 'https://login.nyumc.org/idp/shibboleth':
            for candidate in config['ordered_identifier_candidates']:
                identifier_name = candidate['attribute_names'][0]
                satosa_logging(logger, logging.DEBUG, "Using identifier with label {}".format(identifier_name), context.state)
                identifier = data.attributes.get(identifier_name, None)
                if not identifier:
                    msg = "Unable to find identifier %s asserted by NYU Langone IdP" % identifier_name
                    satosa_logging(logger, logging.ERROR, msg, context.state)
                    raise NyuLangoneAttributeStoreError(msg)
                satosa_logging(logger, logging.DEBUG, "Using identifier value {}".format(identifier), context.state)
                break

            kerberos_id, scope = identifier.split('@')
            if scope != 'nyumc.org':
                msg = "Found NYU Langone IdP asserted identifier %s with bad scope %s" % (identifier, scope)
                satosa_logging(logger, logging.ERROR, msg, context.state)
                raise NyuLangoneAttributeStoreError(msg)

            # Before asserting the NYU Langone Kerberos identifier
            # clear any attributes incoming to this microservice if so configured.
            if config['clear_input_attributes']:
                satosa_logging(logger, logging.DEBUG, "Clearing values for these input attributes: {}".format(data.attributes), context.state)
                data.attributes = {}

            data.attributes[config['nyu_langone_return_attribute']] = kerberos_id
            satosa_logging(logger, logging.DEBUG, "Returning data.attributes {}".format(str(data.attributes)), context.state)
            return super().process(context, data)

        # The IdP was not the NYU Langone IdP so query LDAP to find a record.

        # 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 = []

        # Loop over the configured list of identifiers from the IdP to consider and find
        # asserted values to construct the ordered list of values for the LDAP search filters.
        for candidate in config['ordered_identifier_candidates']:
            value = self._construct_filter_value(candidate, data)

            # 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 value:
                filter_values.append(value)
                satosa_logging(logger, logging.DEBUG, "Added search filter value {} to list of search filters".format(value), context.state)

        # 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

        exception_happened = True
        try:
            connection = config['connection']

            for filter_val in filter_values:
                if record:
                    break

                search_filter = '({0}={1})'.format(config['ldap_identifier_attribute'], filter_val)
                satosa_logging(logger, logging.DEBUG, "Constructed search filter {}".format(search_filter), context.state)

                satosa_logging(logger, logging.DEBUG, "Querying LDAP server...", context.state)
                message_id = connection.search(config['search_base'], search_filter, attributes=config['search_return_attributes'].keys())
                responses = connection.get_response(message_id)[0]
                satosa_logging(logger, logging.DEBUG, "Done querying LDAP server", context.state)
                satosa_logging(logger, logging.DEBUG, "LDAP server returned {} records".format(len(responses)), context.state)

                # for now consider only the first record found (if any)
                if len(responses) > 0:
                    if len(responses) > 1:
                        satosa_logging(logger, logging.WARN, "LDAP server returned {} records using search filter value {}".format(len(responses), filter_val), context.state)
                    record = responses[0]
                    break
        except LDAPException as err:
            satosa_logging(logger, logging.ERROR, "Caught LDAP exception: {}".format(err), context.state)
        except NyuLangoneAttributeStoreError as err:
            satosa_logging(logger, logging.ERROR, "Caught LDAP Attribute Store exception: {}".format(err), context.state)
        except Exception as err:
            satosa_logging(logger, logging.ERROR, "Caught unhandled exception: {}".format(err), context.state)
        else:
            exception_happened = False

        if exception_happened:
            return super().process(context, data)

        # 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']:
            satosa_logging(logger, logging.DEBUG, "Clearing values for these input attributes: {}".format(data.attributes), context.state)
            data.attributes = {}

        # Use a found record, if any, to populate attributes and input for NameID
        if record:
            satosa_logging(logger, logging.DEBUG, "Using record with DN {}".format(record["dn"]), context.state)
            satosa_logging(logger, logging.DEBUG, "Record with DN {} has attributes {}".format(record["dn"], record["attributes"]), context.state)

            # Populate attributes as configured.
            self._populate_attributes(config, record, context, data)

            # Populate input for NameID if configured. SATOSA core does the hashing of input
            # to create a persistent NameID.
            self._populate_input_for_name_id(config, record, context, data)

            # If the voPersonApplicationUID attribute is populated we need to
            # use it to populate uid before asserting uid.
            #
            # TODO When attribute options are configured for the LDAP
            # Provisioner in COmanage change voPersonApplicationUID to 
            # voPersonApplicationUID;app-rn.
            ATTR_KEY = 'voPersonApplicationUID'
            if ATTR_KEY in record["attributes"]:
                try:
                    voPersonApplicationUID = record["attributes"][ATTR_KEY][0]
                    match = re.search('^.+:(EXT\d+)$', voPersonApplicationUID)
                    uid = match.group(1)
                    data.attributes['uid'] = uid
                    msg = "Parsed {} and asserting value {} for uid"
                    msg.format(ATTR_KEY, uid)
                    satosa_logging(logger, logging.DEBUG, msg, context.state)
                except (KeyError, AttributeError, IndexError) as e:
                    msg = "{} value did not match expected pattern: {}"
                    msg = msg.format(ATTR_KEY, e)
                    satosa_logging(logger, logging.WARN, msg, context.state)

        else:
            satosa_logging(logger, logging.WARN, "No record found in LDAP so no attributes will be added", context.state)
            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(sp_entity_id)
                encoded_idp_entity_id = urllib.parse.quote_plus(data.to_dict()['auth_info']['issuer'])
                url = "{}?sp={}&idp={}".format(on_ldap_search_result_empty, encoded_sp_entity_id, encoded_idp_entity_id)
                satosa_logging(logger, logging.INFO, "Redirecting to {}".format(url), context.state)
                return Redirect(url)

        satosa_logging(logger, logging.DEBUG, "Returning data.attributes {}".format(str(data.attributes)), context.state)
        return super().process(context, data)
Example #13
0
    def manage_consent(self, context, internal_response):
        """
        Manage consent and attribute filtering

        :type context: satosa.context.Context
        :type internal_response: satosa.internal_data.InternalResponse
        :rtype: satosa.response.Response

        :param context: response context
        :param internal_response: the response
        :return: response
        """
        state = context.state
        if not self.enabled:
            satosa_logging(LOGGER, logging.INFO, "Consent flow not activated", state)
            return self._end_consent(context, internal_response)

        consent_state = state.get(ConsentModule.STATE_KEY)
        filter = consent_state["filter"]
        requestor = internal_response.to_requestor
        requester_name = consent_state["requester_name"]

        internal_response = self._filter_attributes(internal_response, filter)
        filtered_data = internal_response.get_attributes()

        id_hash = self._get_consent_id(requestor, internal_response.get_user_id(), filtered_data)

        try:
            # Check if consent is already given
            consent_attributes = self._verify_consent(id_hash)
            if consent_attributes:
                internal_response = self._filter_attributes(internal_response, consent_attributes)
                return self._end_consent(context, internal_response)
        except ConnectionError:
            satosa_logging(LOGGER, logging.ERROR,
                           "Consent service is not reachable, no consent given.", state)
            # Send an internal_response without any attributes
            internal_response._attributes = {}
            return self._end_consent(context, internal_response)

        consent_state["internal_resp"] = internal_response.to_dict()
        state.add(ConsentModule.STATE_KEY, consent_state)

        consent_args = {"attr": filtered_data,
                        "locked_attr": self.locked_attr,
                        "id": id_hash,
                        "redirect_endpoint": "%s/consent/%s" % (self.proxy_base, self.endpoint),
                        "requestor": requestor,
                        "requester_name": requester_name}
        consent_args_jws = self._to_jws(consent_args)

        try:
            ticket = self._consent_registration(consent_args_jws)
        except (ConnectionError, AssertionError):
            satosa_logging(LOGGER, logging.ERROR,
                           "Consent service is not reachable, no consent given.", state)
            # Send an internal_response without any attributes
            internal_response._attributes = {}
            return self._end_consent(context, internal_response)

        consent_redirect = "%s?ticket=%s" % (self.consent_redirect_url, ticket)
        return Redirect(consent_redirect)
    def process(self, context, data):
        logprefix = RandSAcl.logprefix

        # Initialize the configuration to use as the default configuration
        # that is passed during initialization.
        config = self.config
        configClean = copy.deepcopy(config)

        satosa_logging(
            logger, logging.DEBUG,
            "{} Using default configuration {}".format(logprefix, configClean),
            context.state)

        # Obtain configuration details from the per-SP configuration or the default configuration
        try:
            if 'attribute_mapping' in config:
                attribute_mapping = config['attribute_mapping']
            if 'access_denied' in config:
                access_denied = config['access_denied']

        except KeyError as err:
            satosa_logging(
                logger, logging.ERROR,
                "{} Configuration '{}' is missing".format(logprefix,
                                                          err), context.state)
            return super().process(context, data)

        # Show what we have
        satosa_logging(
            logger, logging.DEBUG,
            "{} attribute mapping: {}".format(logprefix, attribute_mapping),
            context.state)

        received_attributes = data.attributes
        satosa_logging(
            logger, logging.DEBUG,
            "{} attributes received: {}".format(logprefix,
                                                received_attributes),
            context.state)

        # Do the hard work
        valid_attributes = {
            a: received_attributes[v]
            for (a, v) in attribute_mapping.items()
            if (v in received_attributes and ''.join(received_attributes[v]))
        }
        satosa_logging(
            logger, logging.DEBUG,
            "{} valid attributes: {}".format(logprefix,
                                             valid_attributes), context.state)

        isset = {a: a in valid_attributes for a in attribute_mapping.keys()}
        satosa_logging(logger, logging.DEBUG,
                       "{} isset: {}".format(logprefix, isset), context.state)

        # valid_r_and_s = (isset['edupersonprincipalname'] or (isset['edupersonprincipalname'] and isset['edupersontargetedid'])) and (isset['displayname'] or (isset['givenname'] and isset['sn'])) and isset['mail']
        valid_r_and_s = (
            isset['edupersonprincipalname'] or
            (isset['edupersonprincipalname'] and isset['edupersontargetedid'])
        ) and (isset['displayname'] or
               (isset['givenname'] and isset['sn'])) and (isset['mail'])

        if valid_r_and_s:
            satosa_logging(
                logger, logging.DEBUG,
                "{} R&S attribute set found, user may continue".format(
                    logprefix), context.state)
        else:
            satosa_logging(
                logger, logging.DEBUG,
                "{} missing R&S attribute set, user may not continue".format(
                    logprefix), context.state)
            context.state['substitutions'] = {
                '%custom%': data.auth_info.issuer
            }
            return Redirect(access_denied)

        return super().process(context, data)
Example #15
0
    def process(self, context, data):
        """
        Default interface for microservices. Process the input data for
        the input context.
        """
        issuer = data.auth_info.issuer
        requester = data.requester
        config = self.config.get(requester) or self.config["default"]
        msg = {
            "message": "entityID for the involved entities",
            "requester": requester,
            "issuer": issuer,
            "config": self._filter_config(config),
        }
        satosa_logging(logger, logging.DEBUG, msg, context.state)

        # Ignore this SP entirely if so configured.
        if config["ignore"]:
            msg = "Ignoring SP {}".format(requester)
            satosa_logging(logger, logging.INFO, msg, context.state)
            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}
        satosa_logging(logger, logging.DEBUG, msg, context.state)

        # 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

        for filter_val in filter_values:
            connection = config["connection"]
            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,
            }
            satosa_logging(logger, logging.DEBUG, msg, context.state)

            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:
                satosa_logging(logger, logging.ERROR, exp_msg, context.state)
                return super().process(context, data)

            if not results:
                msg = "Querying LDAP server: No results for {}."
                msg = msg.format(filter_val)
                satosa_logging(logger, logging.DEBUG, msg, context.state)
                continue

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

            msg = "Done querying LDAP server"
            satosa_logging(logger, logging.DEBUG, msg, context.state)
            msg = "LDAP server returned {} records".format(len(responses))
            satosa_logging(logger, logging.INFO, msg, context.state)

            # 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)
                    satosa_logging(logger, logging.WARN, msg, context.state)
                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)
            satosa_logging(logger, logging.DEBUG, msg, context.state)
            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):
            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"],
            }
            satosa_logging(logger, logging.DEBUG, msg, context.state)

            # 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)
            satosa_logging(logger, logging.DEBUG, msg, None)

            # 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)
            satosa_logging(logger, logging.DEBUG, msg, context.state)
        else:
            msg = "No record found in LDAP so no attributes will be added"
            satosa_logging(logger, logging.WARN, msg, context.state)
            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)
                satosa_logging(logger, logging.INFO, msg, context.state)
                return Redirect(url)

        msg = "Returning data.attributes {}".format(data.attributes)
        satosa_logging(logger, logging.DEBUG, msg, context.state)
        return super().process(context, data)
    def process(self, context, data):
        """
        Default interface for microservices. Process the input data for
        the input context.
        """
        self.context = context

        # Find the entityID for the SP that initiated the flow.
        try:
            sp_entity_id = context.state.state_dict['SATOSA_BASE']['requester']
        except KeyError as err:
            satosa_logging(
                logger, logging.ERROR,
                "Unable to determine the entityID for the SP requester",
                context.state)
            return super().process(context, data)

        satosa_logging(
            logger, logging.DEBUG,
            "entityID for the SP requester is {}".format(sp_entity_id),
            context.state)

        # Get the configuration for the SP.
        if sp_entity_id in self.config.keys():
            config = self.config[sp_entity_id]
        else:
            config = self.config['default']

        satosa_logging(logger, logging.DEBUG,
                       "Using config {}".format(self._filter_config(config)),
                       context.state)

        # Ignore this SP entirely if so configured.
        if config['ignore']:
            satosa_logging(logger, logging.INFO,
                           "Ignoring SP {}".format(sp_entity_id), None)
            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 = []

        # Loop over the configured list of identifiers from the IdP to consider and find
        # asserted values to construct the ordered list of values for the LDAP search filters.
        for candidate in config['ordered_identifier_candidates']:
            value = self._construct_filter_value(candidate, data)

            # 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 value:
                filter_values.append(value)
                satosa_logging(
                    logger, logging.DEBUG,
                    "Added search filter value {} to list of search filters".
                    format(value), context.state)

        # 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

        for filter_val in filter_values:
            connection = config['connection']
            search_filter = '({0}={1})'.format(
                config['ldap_identifier_attribute'], filter_val)
            # show ldap filter
            satosa_logging(logger, logging.INFO,
                           "LDAP query for {}".format(search_filter),
                           context.state)
            satosa_logging(
                logger, logging.DEBUG,
                "Constructed search filter {}".format(search_filter),
                context.state)

            try:
                # message_id only works in REUSABLE async connection strategy
                results = connection.search(
                    config['search_base'],
                    search_filter,
                    attributes=config['search_return_attributes'].keys())
            except LDAPException as err:
                exp_msg = "Caught LDAP exception: {}".format(err)
            except LdapAttributeStoreError as err:
                exp_msg = "Caught LDAP Attribute Store exception: {}".format(
                    err)
            except Exception as err:
                exp_msg = "Caught unhandled exception: {}".format(err)

            if exp_msg:
                satosa_logging(logger, logging.ERROR, exp_msg, context.state)
                return super().process(context, data)

            if not results:
                satosa_logging(
                    logger, logging.DEBUG,
                    "Querying LDAP server: No results for {}.".format(
                        filter_val), context.state)
                continue

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

            satosa_logging(logger, logging.DEBUG, "Done querying LDAP server",
                           context.state)
            satosa_logging(
                logger, logging.INFO,
                "LDAP server returned {} records".format(len(responses)),
                context.state)

            # for now consider only the first record found (if any)
            if len(responses) > 0:
                if len(responses) > 1:
                    satosa_logging(
                        logger, logging.WARN,
                        "LDAP server returned {} records using search filter value {}"
                        .format(len(responses), filter_val), context.state)
                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']:
            satosa_logging(
                logger, logging.DEBUG,
                "Clearing values for these input attributes: {}".format(
                    data.attributes), context.state)
            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):
            drec = dict()
            drec['dn'] = record.entry_dn if hasattr(record, 'entry_dn') else ''
            drec['attributes'] = record.entry_attributes_as_dict if hasattr(
                record, 'entry_attributes_as_dict') else {}
            record = drec
        # ends adaptation

        # Use a found record, if any, to populate attributes and input for NameID
        if record:
            satosa_logging(logger, logging.DEBUG,
                           "Using record with DN {}".format(record["dn"]),
                           context.state)
            satosa_logging(
                logger, logging.DEBUG,
                "Record with DN {} has attributes {}".format(
                    record["dn"], record["attributes"]), context.state)

            # Populate attributes as configured.
            self._populate_attributes(config, record, context, data)

            # Populate input for NameID if configured. SATOSA core does the hashing of input
            # to create a persistent NameID.
            self._populate_input_for_name_id(config, record, context, data)

        else:
            satosa_logging(
                logger, logging.WARN,
                "No record found in LDAP so no attributes will be added",
                context.state)
            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(sp_entity_id)
                encoded_idp_entity_id = urllib.parse.quote_plus(
                    data.auth_info.issuer)
                url = "{}?sp={}&idp={}".format(on_ldap_search_result_empty,
                                               encoded_sp_entity_id,
                                               encoded_idp_entity_id)
                satosa_logging(logger, logging.INFO,
                               "Redirecting to {}".format(url), context.state)
                return Redirect(url)

        satosa_logging(
            logger, logging.DEBUG,
            "Returning data.attributes {}".format(str(data.attributes)),
            context.state)
        return ResponseMicroService.process(self, context, data)