def get_mobile_number(self, user=None): ''' get the mobile number - from the token info or - if the policy allowes it, from the user info ''' if not user: return self.get_phone() pol = get_client_policy(context['Client'], scope="authentication", user=user, action="voice_dynamic_mobile_number") if not pol: return self.get_phone() get_dynamic = get_action_value(pol, scope='authentication', action="voice_dynamic_mobile_number", default=False) if not get_dynamic: return self.get_phone() user_detail = getUserDetail(user) return user_detail.get('mobile', self.get_phone())
def get_mobile_number(self, user=None): """ get the mobile number - from the token info or - if the policy allowes it, from the user info """ if not user: return self._getPhone() pol = get_client_policy( context["Client"], scope="authentication", user=user, action="sms_dynamic_mobile_number", ) get_dynamic = get_action_value( pol, scope="authentication", action="sms_dynamic_mobile_number", default=False, ) if not get_dynamic: return self._getPhone() user_detail = getUserDetail(user) return user_detail.get("mobile", self._getPhone())
def _lookup_provider_policies(provider_type): """ helper, to prevent deleting a provider while it is still used in a policy :param provider_type: the type of provider: sms or email :return: a dictionary with provider names as key and list of policy names """ provider_policies = {} # lookup the policy action name provider_action_name = Policy_action_name.get(provider_type) if not provider_action_name: raise Exception('unknown provider_type for policy lookup! %r' % provider_type) # now have a look at all authentication policies policies = linotp.lib.policy.getPolicy({ 'scope': 'authentication', "action": provider_action_name, }) for policy in policies: provider_name = get_action_value(policy, scope='authentication', action=provider_action_name, default='') if provider_name not in provider_policies: provider_policies[provider_name] = [] provider_policies[provider_name].append(policy) return provider_policies
def _get_email_address(self, user=None): ''' get the email address - from the token info or - if the policy allowes it, from the user info ''' if not user: return self._email_address pol = get_client_policy(context['Client'], scope="authentication", user=user, action="dynamic_email_address") if not pol: return self._email_address get_dynamic = get_action_value(pol, scope="authentication", action="dynamic_email_address", default='') if not get_dynamic: return self._email_address user_detail = getUserDetail(user) return user_detail.get('email', self._email_address)
def is_email_editable(user=""): """ this function checks the policy scope=selfservice, action=edit_email This is a int policy, while the '0' is a deny """ realm = user.realm login = user.login policies = get_client_policy( client=context["Client"], scope="selfservice", action="edit_email", realm=realm, user=login, ) edit_email = get_action_value(policies, scope="selfservice", action="edit_email", default=1) if edit_email == 0: return False return True
def is_phone_editable(user=""): """ this function checks the policy scope=selfservice, action=edit_sms This is a int policy, while the '0' is a deny """ # the default string is the OTP value ret = True realm = user.realm login = user.login policies = getPolicy( { "scope": "selfservice", "realm": realm, "action": "edit_sms", "user": login, } ) edit_sms = get_action_value( policies, scope="selfservice", action="edit_sms", default=1 ) if edit_sms == 0: return False return True
def _getEmailSubject(self, user=""): """ Could be used to implement some more complex logic similar to the SMS token where the SMS text is read from a policy. :return: The message that is sent to the user. It should contain at least the placeholder <otp> :rtype: string """ subject = '' if not user: return subject realm = user.realm login = user.login policies = get_client_policy(context['Client'], scope="authentication", realm=realm, user=login, action="emailsubject") subject = get_action_value(policies, scope="authentication", action="emailsubject", default=subject) return subject
def get_voice_message(user="", realm=""): """ This function returns the voice message as defined in the policy authentication/voice_message. If no such policy is defined, the function returns the fallback message "{otp}" :return: string """ voice_text = "{otp}" pol = get_client_policy( context["Client"], scope="authentication", realm=realm, user=user, action="voice_message", ) if len(pol) > 0: voice_text = get_action_value(pol, scope="authentication", action="voice_message", default="") log.debug("[get_voice_message] got the voice_message = %s", voice_text) return voice_text
def is_phone_editable(user=""): ''' this function checks the policy scope=selfservice, action=edit_sms This is a int policy, while the '0' is a deny ''' # the default string is the OTP value ret = True realm = user.realm login = user.login policies = getPolicy({ 'scope': 'selfservice', 'realm': realm, "action": "edit_sms", "user": login }) edit_sms = get_action_value(policies, scope='selfservice', action="edit_sms", default=1) if edit_sms == 0: return False return True
def _getEmailMessage(self, user=""): """ Could be used to implement some more complex logic similar to the SMS token where the SMS text is read from a policy. :return: The message that is sent to the user. It should contain at least the placeholder <otp> :rtype: string """ message = DEFAULT_MESSAGE if not user: return message realm = user.realm login = user.login policies = get_client_policy( context["Client"], scope="authentication", realm=realm, user=login, action="emailtext", ) message = get_action_value( policies, scope="authentication", action="emailtext", default=message, ) return message
def get_voice_language(user="", realm=""): """ This function returns the voice language as defined in the policy authentication/voice_language. If no such policy is defined, the function returns the fallback message "en" :return: string """ voice_language = "en" pol = get_client_policy(context['Client'], scope="authentication", realm=realm, user=user, action="voice_language") voice_language = get_action_value(pol, scope='authentication', action="voice_language", default='') log.debug("[get_voice_language] got the voice_language = %s", voice_language) return voice_language
def check_maxtoken_for_user_by_type(user, type_of_token): ''' This internal function checks the number of assigned tokens to a user restricted by the policies: "scope = enrollment", action = "maxtokenTOKENTYPE = <number>" :param user: to whom the token should belong :param type_of_token: which type of token should be enrolled or assigned :raises PolicyException: if maxtoken policy would be violated ''' _ = context['translate'] if not user or not user.login: return client = _get_client() user_realms = _getUserRealms(user) log.debug("checking the already assigned tokens for user %r, realms %s" % (user, user_realms)) # ------------------------------------------------------------------ -- # check the maxtokenTOKENTYPE policy typed_tokens = linotp.lib.token.getTokens4UserOrSerial( user, token_type=type_of_token) for user_realm in user_realms: policies = get_client_policy(client, action="maxtoken%s" % type_of_token.upper(), scope='enrollment', realm=user_realm, user=user.login, userObj=user) if not policies: continue # compare the tokens of the user with the max numbers of the policy total_maxtoken = get_action_value( policies, scope='enrollment', action="maxtoken%s" % type_of_token.upper(), default=-1) if total_maxtoken == -1 or isinstance(total_maxtoken, bool): continue if len(typed_tokens) + 1 > total_maxtoken: error_msg = _("The maximum number of allowed tokens of type %s " "per user is exceeded. Check the policies " "scope=enrollment, action=maxtoken%s" % (type_of_token, type_of_token.upper())) raise linotp.lib.policy.MaxTokenTypeUserPolicyException(error_msg)
def get_provider_from_policy(provider_type, realm=None, user=None, scope='authentication', action=None): """ interface for the provider user like email token or sms token :param provider_type: 'push', 'email' or 'sms :param user: the user, who should receive the message, used for the policy lookup :return: the list of all identified providers by name """ # check if the provider is defined in a policy provider_name = None # lookup the policy action name provider_action_name = Policy_action_name.get(provider_type) if not provider_action_name: raise Exception('unknown provider_type for policy lookup! %r' % provider_type) if user is None: raise Exception('unknown user for policy lookup! %r' % user) if user and user.login: realm = user.realm if not action: action = provider_action_name policies = linotp.lib.policy.get_client_policy(request_context['Client'], scope=scope, action=action, realm=realm, user=user.login) if not policies: default_provider = _get_default_provider_name(provider_type) if default_provider: return [default_provider] return [] provider_names = get_action_value(policies, scope=scope, action=action, default='') providers = [] for entry in [x.strip() for x in provider_names.split(' ')]: if entry: providers.append(entry) return providers
def check_maxtoken_for_user(user): ''' This internal function checks the number of assigned tokens to a user restricted by the policies: "scope = enrollment", action = "maxtoken = <number>" :param user: to whom the token should belong :raises PolicyException: if maxtoken policy would be violated ''' _ = context['translate'] if not user or not user.login: return client = _get_client() user_realms = _getUserRealms(user) log.debug("checking the already assigned tokens for user %r, realms %s" % (user, user_realms)) # ----------------------------------------------------------------------- -- # check the maxtoken policy action = "maxtoken" tokens = linotp.lib.token.getTokens4UserOrSerial(user, "") for user_realm in user_realms: policies = get_client_policy(client, scope='enrollment', action=action, realm=user_realm, user=user.login, userObj=user) if not policies: continue total_maxtoken = get_action_value(policies, scope='enrollment', action=action, default=-1) if total_maxtoken == -1 or isinstance(total_maxtoken, bool): continue if len(tokens) + 1 > total_maxtoken: error_msg = _("The maximum number of allowed tokens " "per user is exceeded. Check the " "policies scope=enrollment, " "action=maxtoken") raise linotp.lib.policy.MaxTokenUserPolicyException(error_msg)
def notify_user(user, action, info, required=False): """ notify user via email, sms or other method (http/whatsapp...) :param user: the user who should be notified :param action: action is currently the notification action like enrollment, setPin, which are defined in the notification policies :param info: generic dict which is action specific :param required: if True an exception is raised if no notification could be send eg if no provider is defined or could be found :return: boolean - true if notification is enabled """ policies = linotp.lib.policy.get_client_policy( request_context["Client"], scope="notification", action=action, realm=user.realm, user=user.login, ) provider_specs = get_action_value( policies, scope="notification", action=action, default="" ) if not isinstance(provider_specs, list): provider_specs = [provider_specs] # TODO: use the ResouceSchduler to handle failover for provider_spec in provider_specs: provider_type, _sep, provider_name = provider_spec.partition("::") if provider_type == "email": notify_user_by_email(provider_name, user, action, info) return True # elif provider_type == 'sms': # notify_user_by_email(provider_name, user, action, info) log.info("Failed to notify user %r", user) if required: raise NotificationException( "No notification has been sent - %r provider defined?" % action ) return False
def _is_valid_facet(self, origin): """ check if origin is in the valid facets if the u2f_valid_facets policy is set. Otherwise check if the origin matches the previously saved origin :return: boolean - True if supported, False if unsupported """ is_valid = False # Get the valid facets as specified in the enrollment policy 'u2f_valid_facets' # for the specific realm valid_facets_action_value = "" realms = self.token.getRealmNames() if len(realms) > 0: get_policy_params = { "action": "u2f_valid_facets", "scope": "enrollment", "realm": realms[0], } policies = getPolicy(get_policy_params) valid_facets_action_value = get_action_value( policies, scope="enrollment", action="u2f_valid_facets", default="", ) if valid_facets_action_value != "": # 'u2f_valid_facets' policy is set - check if origin is in valid facets list valid_facets = valid_facets_action_value.split(";") for facet in valid_facets: facet = facet.strip() if origin in valid_facets: is_valid = True else: # 'u2f_valid_facets' policy is empty or not set # check if origin matches the origin stored in the token info or save it if no origin # is stored yet appId = self._get_app_id() if appId == origin: is_valid = True return is_valid
def valid_facets(self, realm=None): """ Show the JSON output for the valid facets configured by the enrollment policy 'u2f_valid_facets'. The form of the JSON output is specified by the FIDO Alliance. """ if realm is None: realm = getDefaultRealm() # Get the valid facets as specified in the enrollment policy 'u2f_valid_facets' # for the specific realm get_policy_params = { "action": "u2f_valid_facets", "scope": "enrollment", "realm": realm, } valid_facets_action_value = get_action_value( getPolicy(get_policy_params), scope="enrollment", action="u2f_valid_facets", default="", ) # the action value contains the semicolon-separated list of valid # facets valid_facets = valid_facets_action_value.split(";") # Prepare the response response.content_type = ( "application/fido.trusted-apps+json" # as specified by FIDO ) response_dict = { "trustedFacets": [{ "version": { "major": 1, "minor": 0 }, "ids": [] }] } for facet in valid_facets: facet = facet.strip() response_dict["trustedFacets"][0]["ids"].append(facet) return json.dumps(response_dict)
def loadProviderFromPolicy(provider_type, realm=None, user=None): """ interface for the provider user like email token or sms token :param provider_type: 'push', 'email' or 'sms :param user: the user, who should receive the message, used for the policy lookup :return: the instantiated provider with already loaded config """ # check if the provider is defined in a policy provider_name = None # lookup the policy action name provider_action_name = Policy_action_name.get(provider_type) if not provider_action_name: raise Exception("unknown provider_type for policy lookup! %r" % provider_type) if user is None: raise Exception("unknown user for policy lookup! %r" % user) if user and user.login: realm = user.realm policies = linotp.lib.policy.get_client_policy( request_context["Client"], scope="authentication", action=provider_action_name, realm=realm, user=user.login, ) provider_name = get_action_value( policies, scope="authentication", action=provider_action_name, default="", ) return loadProvider(provider_type, provider_name)
def get_auth_smstext(user="", realm=""): ''' this function checks the policy scope=authentication, action=smstext This is a string policy The function returns the tuple (bool, string), bool: If a policy is defined string: the string to use ''' pol = get_client_policy(context['Client'], scope="authentication", realm=realm, user=user, action="smstext") smstext = get_action_value(pol, scope='authentication', action="smstext", default="<otp>") log.debug("[get_auth_smstext] got the smstext = %s" % smstext) return (smstext != "<otp>"), smstext
def valid_facets(self, realm=None): """ Show the JSON output for the valid facets configured by the enrollment policy 'u2f_valid_facets'. The form of the JSON output is specified by the FIDO Alliance. """ if realm is None: realm = getDefaultRealm() # Get the valid facets as specified in the enrollment policy 'u2f_valid_facets' # for the specific realm get_policy_params = { 'action': 'u2f_valid_facets', 'scope': 'enrollment', 'realm': realm } valid_facets_action_value = get_action_value( getPolicy(get_policy_params), scope='enrollment', action='u2f_valid_facets', default='') # the action value contains the semicolon-separated list of valid facets valid_facets = valid_facets_action_value.split(';') # Prepare the response response.content_type = 'application/fido.trusted-apps+json' # as specified by FIDO response_dict = { "trustedFacets": [{ "version": { "major": 1, "minor": 0 }, "ids": [] }] } for facet in valid_facets: facet = facet.strip() response_dict['trustedFacets'][0]['ids'].append(facet) return json.dumps(response_dict)
def enforce_smstext(user="", realm=""): ''' this function checks the boolean policy scope=authentication, action=enforce_smstext The function returns true if the smstext should be used instead of the challenge data :return: bool ''' pol = get_client_policy(context['Client'], scope="authentication", realm=realm, user=user, action="enforce_smstext") enforce_smstext = get_action_value(pol, scope='authentication', action="enforce_smstext", default=False) log.debug("got enforce_smstext = %r" % enforce_smstext) return enforce_smstext
def getInitDetail(self, params, user=None): """ to complete the token normalisation, the response of the initialisation should be built by the token specific method, the getInitDetails """ response_detail = {} info = self.getInfo() response_detail.update(info) response_detail['serial'] = self.getSerial() # get requested phase try: requested_phase = params["phase"] except KeyError: raise ParameterError("Missing parameter: 'phase'") if requested_phase == "registration1": # We are in registration phase 1 # We create a 32 bytes otp key (from urandom) # which is used as the registration challenge challenge = base64.urlsafe_b64encode( binascii.unhexlify(self._genOtpKey_(32))) self.addToTokenInfo('challenge', challenge.decode('ascii')) # save the appId to the TokenInfo # An appId passed as parameter is preferred over an appId defined in a policy appId = '' if 'appid' in params: appId = params.get('appid') else: # No appId passed as parameter - fall back to the policy # Get the appId as specified in the enrollment policy 'u2f_app_id' # for the specific realm # If the token has multiple realms, the appIds are checked for conflicts. # It could be discussed whether the token should use the appId of the default # realm, when the token is not attached to any realms realms = self.token.getRealmNames() for realm in realms: get_policy_params = { 'action': 'u2f_app_id', 'scope': 'enrollment', 'realm': realm } policies = getPolicy(get_policy_params) policy_value = get_action_value(policies, scope='enrollment', action='u2f_app_id', default='') # Check for appId conflicts if appId and policy_value: if appId != policy_value: raise Exception( "Conflicting appId values in u2f policies.") appId = policy_value if not appId: raise Exception("No appId defined.") self.addToTokenInfo('appId', appId) # create U2F RegisterRequest object and append it to the response as 'message' appId = self._get_app_id() register_request = { 'challenge': challenge.decode('ascii'), 'version': 'U2F_V2', 'appId': appId } response_detail['registerrequest'] = register_request elif requested_phase == "registration2": # We are in registration phase 2 # process the data generated by the u2f compatible token device registerResponse = "" otpkey = None if 'otpkey' in params: otpkey = params.get('otpkey') if otpkey is not None: # otpkey holds the JSON RegisterResponse object as specified by the FIDO Alliance try: registerResponse = json.loads(otpkey) except ValueError as ex: raise Exception('Invalid JSON format') self._handle_client_errors(registerResponse) try: registrationData = registerResponse['registrationData'] clientData = registerResponse['clientData'] except AttributeError as ex: raise Exception("Couldn't find keyword in JSON object") # registrationData and clientData are urlsafe base64 encoded # correct padding errors (length should be multiples of 4) # fill up the registrationData with '=' to the correct padding registrationData = registrationData + \ ('=' * (4 - (len(registrationData) % 4))) clientData = clientData + ('=' * (4 - (len(clientData) % 4))) registrationData = base64.urlsafe_b64decode( registrationData.encode('ascii')) clientData = base64.urlsafe_b64decode( clientData.encode('ascii')) # parse the raw registrationData according to the specification (userPublicKey, keyHandle, x509cert, signature) = \ self._parseRegistrationData(registrationData) # check the received clientData object if not self._checkClientData( clientData, 'registration', self.getFromTokenInfo('challenge', None)): raise ValueError( "Received invalid clientData object. Aborting...") # prepare the applicationParameter and challengeParameter needed for # verification of the registration signature appId = self._get_app_id() applicationParameter = sha256(appId.encode('utf-8')).digest() challengeParameter = sha256(clientData).digest() # verify the registration signature self._validateRegistrationSignature(applicationParameter, challengeParameter, keyHandle, userPublicKey, x509cert, signature) # save the key handle and the user public key in the Tokeninfo field for # future use self.addToTokenInfo( 'keyHandle', base64.urlsafe_b64encode(keyHandle).decode('ascii')) self.addToTokenInfo( 'publicKey', base64.urlsafe_b64encode(userPublicKey).decode('ascii')) self.addToTokenInfo('counter', '0') self.addToTokenInfo('phase', 'authentication') # remove the registration challenge from the token info self.removeFromTokenInfo('challenge') # Activate the token self.token.LinOtpIsactive = True else: raise ValueError("No otpkey set") else: raise Exception("Unsupported phase: %s", requested_phase) return response_detail