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)
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)
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))
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))
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
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)
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))
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))
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))
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)
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)
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)
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)