def test_get_single_auth_policy_pe(self, mock_get_policy_definitions): """ verify that (more specific) policy which refers to a client is selected this is the first one of a series to test that policy evaluation supports the filtering by client, now focusing on the function get_single_auth_policy which is used by the QRToken and the PushToken to retrieve the pairing and callback urls from the policy defintions """ mock_get_policy_definitions.return_value = { 'authentication': { 'qrtoken_pairing_callback_url': { 'type': 'str' } } } with patch.object( linotp.lib.policy, '_get_client', autospec=True) \ as mock_get_client: # ------------------------------------------------------------------ -- # call the get_policies function which must be mocked with patch.object( linotp.lib.policy.processing, 'get_policies', autospec=True) \ as mock_get_policies: # ---------------------------------------------------------- -- # setup the to be called mocked functions mock_get_policies.side_effect = m_get_policies mock_get_client.side_effect = m_get_client_match action_value = get_single_auth_policy( 'qrtoken_pairing_callback_url', realms=['*']) assert 'client' in action_value mock_get_client.side_effect = m_get_client_no_match action_value = get_single_auth_policy( 'qrtoken_pairing_callback_url', realms=['*']) assert 'client' not in action_value
def test_get_single_auth_policy_new_pe(self): """ verify that (more specific) policy which refers to a client is selected this is the first one of a series to test that policy evaluation supports the filtering by client, now focusing on the function get_single_auth_policy which is used by the QRToken and the PushToken to retrieve the pairing and callback urls from the policy defintions """ with patch.object( linotp.lib.policy, '_get_client', autospec=True) \ as mock_get_client: # ------------------------------------------------------------------ -- # the new policy engine is by defining this in the config mocked_context['Config']['NewPolicyEvaluation'] = True # and calls the get_policies function which must be mocked with patch.object( linotp.lib.policy.processing, 'get_policies', autospec=True) \ as mock_get_policies: # ---------------------------------------------------------- -- # setup the to be called mocked functions mock_get_policies.side_effect = m_get_policies mock_get_client.side_effect = m_get_client_match action_value = get_single_auth_policy( 'qrtoken_pairing_callback_url', realms=['*']) assert 'client' in action_value mock_get_client.side_effect = m_get_client_no_match action_value = get_single_auth_policy( 'qrtoken_pairing_callback_url', realms=['*']) assert 'client' not in action_value
def getInitDetail(self, params, user=None): """ returns initialization details in the enrollment process (gets called after update method). used here to pass the pairing url to the user :param params: parameters provided by the client :param user: (unused) :raises TokenStateError: If token state is not 'initialized' :returns: a dict consisting of a 'pairing_url' entry, containing the pairing url and a 'pushtoken_pairing_url' entry containing a data structure used in the manage frontend in the enrollment process """ _ = context['translate'] response_detail = {} self.ensure_state('initialized') # ------------------------------------------------------------------- -- # collect data used for generating the pairing url serial = self.getSerial() # ------------------------------------------------------------------- -- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() # it is guaranteed, that cb_url has a value # because we checked it in the update method cb_url = get_single_auth_policy('pushtoken_pairing_callback_url', user=owner, realms=realms) # --------------------------------------------------------------- -- partition = self.getFromTokenInfo('partition') # FIXME: certificate usage pairing_url = generate_pairing_url(token_type='push', partition=partition, serial=serial, callback_url=cb_url, use_cert=False) # --------------------------------------------------------------- -- self.addToInfo('pairing_url', pairing_url) response_detail['pairing_url'] = pairing_url # --------------------------------------------------------------- -- # add response tabs (used in the manage view on enrollment) response_detail['lse_qr_url'] = { 'description': _('Pairing URL'), 'img': create_img(pairing_url, width=250), 'order': 0, 'value': pairing_url} response_detail['serial'] = self.getSerial() # ------------------------------------------------------------------ -- self.change_state('unpaired') return response_detail
def update(self, params): """ initialization entry hook for the enrollment process. :param params: parameters provided by the client :raises Exception: If the client supplied unrecognized configuration parameters for this token type :raises Exception: If the policy 'pushtoken_pairing_callback_url' was not set. :raises TokenStateError: If token state is not None (default pre-enrollment state) """ param_keys = set(params.keys()) init_rollout_state_keys = set(['type', 'serial', '::scope::', 'user.login', 'description', 'user.realm', 'session', 'key_size', 'resConf', 'user', 'realm', 'pin']) # ------------------------------------------------------------------- -- if not param_keys.issubset(init_rollout_state_keys): # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type pushtoken') # if param keys are in above set, the token is # initialized for the first time. this is e.g. done on the # manage web ui. since the token doesn't exist in the database # yet, its rollout state must be None (that is: the data for # the rollout state doesn't exist yet) self.ensure_state(None) # --------------------------------------------------------------- -- # we check if callback policies are set. this must be done here # because the token gets saved directly after the update method # in the TokenHandler _ = context['translate'] owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() cb_url = get_single_auth_policy('pushtoken_pairing_callback_url', user=owner, realms=realms) if not cb_url: raise Exception(_('Policy pushtoken_pairing_callback_url must ' 'have a value')) partition = get_partition(realms, owner) self.addToTokenInfo('partition', partition) # --------------------------------------------------------------- -- # we set the the active state of the token to False, because # it should not be allowed to use it for validation before the # pairing process is done self.token.LinOtpIsactive = False # -------------------------------------------------------------- -- TokenClass.update(self, params, reset_failcount=True) # -------------------------------------------------------------- -- self.change_state('initialized')
def createChallenge(self, transaction_id, options): """ entry hook for the challenge logic. when this function is called a challenge with an transaction was created. :param transaction_id: A unique transaction id used to identity the challenge object :param options: additional options as a dictionary :raises TokenStateError: If token state is not 'active' or 'pairing_response_received' :returns: A tuple (success, message, data, attributes) with success being a boolean indicating if the call to this method was successful, message being a string that is passed to the user, attributes being additional output data (unused in here) """ valid_states = ['pairing_response_received', 'active'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- # inside the challenge url we sent a callback url for the client # which is defined by an authentication policy owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policy_name = 'pushtoken_challenge_callback_url' callback_url = get_single_auth_policy(callback_policy_name, user=owner, realms=realms) if not callback_url: raise Exception(_('Policy pushtoken_challenge_callback_url must ' 'have a value')) # ------------------------------------------------------------------- -- # load and configure provider # the realm logic was taken from the # provider loading in the smstoken class # TODO: refactor & centralize logic realm = None if realms: realm = realms[0] push_provider = loadProviderFromPolicy(provider_type='push', realm=realm, user=owner) # ------------------------------------------------------------------- -- if self.current_state == 'pairing_response_received': content_type = CONTENT_TYPE_PAIRING message = '' challenge_url, sig_base = self.create_challenge_url(transaction_id, content_type, callback_url) else: content_type_as_str = options.get('content_type') try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # --------------------------------------------------------------- -- if content_type == CONTENT_TYPE_SIGNREQ: message = options.get('data') challenge_url, sig_base = self.create_challenge_url( transaction_id, content_type, callback_url, message=message) # --------------------------------------------------------------- -- elif content_type == CONTENT_TYPE_LOGIN: message = options.get('data') login, __, host = message.partition('@') challenge_url, sig_base = self.create_challenge_url( transaction_id, content_type, callback_url, login=login, host=host) else: raise ValueError('Unrecognized content type: %s' % content_type) # ------------------------------------------------------------------- -- # send the challenge_url to the push notification proxy token_info = self.getTokenInfo() gda = token_info['gda'] log.debug("pushing notification: %r : %r", challenge_url, gda) success, response = push_provider.push_notification(challenge_url, gda) if not success: raise Exception('push mechanism failed. response was %r' % response) # ------------------------------------------------------------------- -- # we save sig_base in the challenge data, because we need it in # checkOtp to verify the signature b64_sig_base = b64encode(sig_base) data = {'sig_base': b64_sig_base} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') # ------------------------------------------------------------------- -- # don't pass the challenge_url as message to the user return (True, '', data, {})
def createChallenge(self, transaction_id, options): """ """ _ = context['translate'] valid_states = ['pairing_response_received', 'pairing_complete'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- if self.current_state == 'pairing_response_received': content_type = CONTENT_TYPE_PAIRING reset_url = True else: content_type_as_str = options.get('content_type') reset_url = False if content_type_as_str is None: content_type = None else: try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # ------------------------------------------------------------------- -- message = options.get('data') # ------------------------------------------------------------------- -- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policies = [ 'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms' ] callback_url = get_single_auth_policy(callback_policies[0], user=owner, realms=realms) callback_sms = get_single_auth_policy(callback_policies[1], user=owner, realms=realms) if not callback_url and not callback_sms: raise Exception( _('Policy %s must have a value') % _(" or ").join(callback_policies)) # TODO: get from policy/config compression = False # ------------------------------------------------------------------- -- challenge_url, user_sig = self.create_challenge_url( transaction_id, content_type, message, callback_url, callback_sms, compression, reset_url) data = {'message': message, 'user_sig': user_sig} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') return (True, challenge_url, data, {})
def getInitDetail(self, params, user=None): _ = context['translate'] response_detail = {} param_keys = set(params.keys()) init_rollout_state_keys = set([ 'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login', 'description', 'user.realm', 'session', 'otplen', 'pin', 'resConf', 'user', 'realm', 'qr' ]) # ------------------------------------------------------------------- -- if param_keys.issubset(init_rollout_state_keys): # collect data used for generating the pairing url serial = self.getSerial() # for qrtoken hashlib is ignored hash_algorithm = None otp_pin_length = int(self.getOtpLen()) owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] user = owner else: realms = self.getRealms() pairing_policies = [ 'qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms' ] # it is guaranteed, that either cb_url or cb_sms has a value # because we checked it in the update method cb_url = get_single_auth_policy(pairing_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(pairing_policies[1], user=owner, realms=realms) # --------------------------------------------------------------- -- partition = self.getFromTokenInfo('partition') # FIXME: certificate usage pairing_url = generate_pairing_url(token_type='qr', partition=partition, serial=serial, callback_url=cb_url, callback_sms_number=cb_sms, otp_pin_length=otp_pin_length, hash_algorithm=hash_algorithm, use_cert=False) # --------------------------------------------------------------- -- self.addToInfo('pairing_url', pairing_url) response_detail['pairing_url'] = pairing_url # create response tabs response_detail['lse_qr_url'] = { 'description': _('QRToken Pairing Url'), 'img': create_img(pairing_url, width=250), 'order': 0, 'value': pairing_url } response_detail['lse_qr_cert'] = { 'description': _('QRToken Certificate'), 'img': create_img(pairing_url, width=250), 'order': 1, 'value': pairing_url } response_detail['serial'] = self.getSerial() # ------------------------------------------------------------------ -- else: # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type qr') # ------------------------------------------------------------------- -- self.change_state('pairing_url_sent') return response_detail
def update(self, params): param_keys = set(params.keys()) init_rollout_state_keys = set([ 'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login', 'description', 'user.realm', 'session', 'otplen', 'resConf', 'user', 'realm', 'qr', 'pin' ]) # ------------------------------------------------------------------- -- if not param_keys.issubset(init_rollout_state_keys): # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type qr') # if param keys are in {'type', 'hashlib'} the token is # initialized for the first time. this is e.g. done on the # manage web ui. since the token doesn't exist in the database # yet, its rollout state must be None (that is: they data for # the rollout state doesn't exist yet) self.ensure_state(None) # --------------------------------------------------------------- -- # we check if callback policies are set. this must be done here # because the token gets saved directly after the update method # in the TokenHandler _ = context['translate'] owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() pairing_policies = [ 'qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms' ] cb_url = get_single_auth_policy(pairing_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(pairing_policies[1], user=owner, realms=realms) if not cb_url and not cb_sms: raise Exception( _('Policy %s must have a value') % _(" or ").join(pairing_policies)) challenge_policies = [ 'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms' ] cb_url = get_single_auth_policy(challenge_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(challenge_policies[1], user=owner, realms=realms) if not cb_url and not cb_sms: raise Exception( _('Policy %s must have a value') % _(" or ").join(challenge_policies)) partition = get_partition(realms, owner) self.addToTokenInfo('partition', partition) # --------------------------------------------------------------- -- # we set the the active state of the token to False, because # it should not be allowed to use it for validation before the # pairing process is done self.token.LinOtpIsactive = False # --------------------------------------------------------------- -- if 'otplen' not in params: params['otplen'] = getFromConfig("QRTokenOtpLen", 8) # -------------------------------------------------------------- -- TokenClass.update(self, params, reset_failcount=True)
def createChallenge(self, transaction_id, options): """ create a challenge - either for pairing or challenges when the token is activated. we support re-activation by the means that if we are in the state 'pairing_challenge_sent' the activation challenge could be triggered again :param transaction_id: scope of the challenge, will become part of the challenge url code :param options: the request optional parameters :return: tuple with (True, challenge_url, data, {}) whereby the data is a dict with {'message': message, 'user_sig': user_sig} """ _ = context['translate'] valid_states = [ 'pairing_response_received', 'pairing_challenge_sent', 'pairing_complete' ] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- if self.current_state in [ 'pairing_challenge_sent', 'pairing_response_received' ]: content_type = CONTENT_TYPE_PAIRING reset_url = True elif self.current_state == 'pairing_complete': content_type_as_str = options.get('content_type') reset_url = False if content_type_as_str is None: content_type = None else: try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # ------------------------------------------------------------------- -- message = options.get('data') # ------------------------------------------------------------------- -- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policies = [ 'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms' ] callback_url = get_single_auth_policy(callback_policies[0], user=owner, realms=realms) callback_sms = get_single_auth_policy(callback_policies[1], user=owner, realms=realms) if not callback_url and not callback_sms: raise Exception( _('Policy %s must have a value') % _(" or ").join(callback_policies)) # TODO: get from policy/config compression = False # ------------------------------------------------------------------- -- challenge_url, user_sig = self.create_challenge_url( transaction_id, content_type, message, callback_url, callback_sms, compression, reset_url) data = {'message': message, 'user_sig': user_sig} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') return (True, challenge_url, data, {})
def createChallenge(self, transaction_id, options): """ entry hook for the challenge logic. when this function is called a challenge with an transaction was created. :param transaction_id: A unique transaction id used to identity the challenge object :param options: additional options as a dictionary :raises TokenStateError: If token state is not 'active' or 'pairing_response_received' :returns: A tuple (success, message, data, attributes) with success being a boolean indicating if the call to this method was successful, message being a string that is passed to the user, attributes being additional output data (unused in here) """ _ = context['translate'] valid_states = ['active', 'pairing_response_received', # we support re activation # as long as the token is not active 'pairing_challenge_sent', ] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- # inside the challenge url we sent a callback url for the client # which is defined by an authentication policy owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policy_name = 'pushtoken_challenge_callback_url' callback_url = get_single_auth_policy(callback_policy_name, user=owner, realms=realms) if not callback_url: raise Exception(_('Policy pushtoken_challenge_callback_url must ' 'have a value')) # ------------------------------------------------------------------- -- # load and configure provider # the realm logic was taken from the # provider loading in the smstoken class # TODO: refactor & centralize logic realm = None if realms: realm = realms[0] push_provider = loadProviderFromPolicy(provider_type='push', realm=realm, user=owner) # ------------------------------------------------------------------- -- if self.current_state in ['pairing_response_received', 'pairing_challenge_sent']: content_type = CONTENT_TYPE_PAIRING message = '' challenge_url, sig_base = self.create_challenge_url(transaction_id, content_type, callback_url) elif self.current_state in ['active']: content_type_as_str = options.get( 'content_type', CONTENT_TYPE_SIGNREQ) try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # --------------------------------------------------------------- -- if content_type == CONTENT_TYPE_SIGNREQ: message = options.get('data') challenge_url, sig_base = self.create_challenge_url( transaction_id, content_type, callback_url, message=message) # --------------------------------------------------------------- -- elif content_type == CONTENT_TYPE_LOGIN: message = options.get('data') login, __, host = message.partition('@') challenge_url, sig_base = self.create_challenge_url( transaction_id, content_type, callback_url, login=login, host=host) else: raise ValueError('Unrecognized content type: %s' % content_type) # ------------------------------------------------------------------- -- # send the challenge_url to the push notification proxy token_info = self.getTokenInfo() gda = token_info['gda'] log.debug("pushing notification: %r : %r", challenge_url, gda) success, response = push_provider.push_notification( challenge_url, gda, transaction_id) if not success: raise Exception('push mechanism failed. response was %r' % response) # ------------------------------------------------------------------- -- # we save sig_base in the challenge data, because we need it in # checkOtp to verify the signature b64_sig_base = b64encode(sig_base) data = {'sig_base': b64_sig_base} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') # ------------------------------------------------------------------- -- # don't pass the challenge_url as message to the user return (True, '', data, {})