def save_pin_change(request, response, serial=None): """ This policy function checks if the next_pin_change date should be stored in the tokeninfo table. 1. Check scope:enrollment and ACTION.CHANGE_PIN_FIRST_USE. This action is used, when the administrator enrolls a token or sets a PIN 2. Check scope:enrollment and ACTION.CHANGE_PIN_EVERY is used, if the user changes the PIN. This function decorates /token/init and /token/setpin. The parameter "pin" and "otppin" is investigated. :param request: :param action: :return: """ policy_object = g.policy_object serial = serial or request.all_data.get("serial") if not serial: # No serial in request, so we look into the response serial = response.json.get("detail", {}).get("serial") if not serial: log.error("Can not determine serial number. Have no idea of any " "realm!") else: # Determine the realm by the serial realm = get_realms_of_token(serial, only_first_realm=True) realm = realm or get_default_realm() if g.logged_in_user.get("role") == ROLE.ADMIN: pinpol = Match.realm(g, scope=SCOPE.ENROLL, action=ACTION.CHANGE_PIN_FIRST_USE, realm=realm).policies() if pinpol: token = get_one_token(serial=serial) token.set_next_pin_change(diff="0d") elif g.logged_in_user.get("role") == ROLE.USER: # Check for parameter "pin" or "otppin". otppin = request.all_data.get("otppin") pin = request.all_data.get("pin") # The user sets a pin or enrolls a token. -> delete the pin_change if otppin or pin: token = get_one_token(serial=serial) token.del_tokeninfo("next_pin_change") # If there is a change_pin_every policy, we need to set the PIN # anew. pinpol = Match.realm(g, scope=SCOPE.ENROLL, action=ACTION.CHANGE_PIN_EVERY, realm=realm).action_values(unique=True) if pinpol: token = get_one_token(serial=serial) token.set_next_pin_change(diff=list(pinpol)[0]) # we do not modify the response! return response
def get_default_settings(cls, g, params): """ This method returns a dictionary with default settings for token enrollment. These default settings are defined in SCOPE.USER or SCOPE.ADMIN and are totp_hashlib, totp_timestep and totp_otplen. If these are set, the user or admin will only be able to enroll tokens with these values. The returned dictionary is added to the parameters of the API call. :param g: context object, see documentation of ``Match`` :param params: The call parameters :type params: dict :return: default parameters """ ret = {} if not g.logged_in_user: return ret (role, username, userrealm, adminuser, adminrealm) = determine_logged_in_userparams(g.logged_in_user, params) hashlib_pol = Match.generic( g, scope=role, action="totp_hashlib", user=username, realm=userrealm, adminuser=adminuser, adminrealm=adminrealm).action_values(unique=True) if hashlib_pol: ret["hashlib"] = list(hashlib_pol)[0] timestep_pol = Match.generic( g, scope=role, action="totp_timestep", user=username, realm=userrealm, adminuser=adminuser, adminrealm=adminrealm).action_values(unique=True) if timestep_pol: ret["timeStep"] = list(timestep_pol)[0] otplen_pol = Match.generic( g, scope=role, action="totp_otplen", user=username, realm=userrealm, adminuser=adminuser, adminrealm=adminrealm).action_values(unique=True) if otplen_pol: ret["otplen"] = list(otplen_pol)[0] return ret
def mangle_challenge_response(request, response): """ This policy decorator is used in the AUTH scope to decorate the /validate/check endpoint. It can modify the contents of the response "detail"->"message" to allow a better readability for a challenge response text. :param request: :param response: :return: """ if not response.is_json: # This can happen with the validate/radiuscheck endpoint return response content = response.json user_obj = request.User header_pol = Match.user(g, scope=SCOPE.AUTH, action=ACTION.CHALLENGETEXT_HEADER, user_object=user_obj).action_values( unique=True, allow_white_space_in_action=True) footer_pol = Match.user(g, scope=SCOPE.AUTH, action=ACTION.CHALLENGETEXT_FOOTER, user_object=user_obj).action_values( unique=True, allow_white_space_in_action=True) if header_pol: multi_challenge = content.get("detail", {}).get("multi_challenge") if multi_challenge: message = list(header_pol)[0] footer = "" if footer_pol: footer = list(footer_pol)[0] # We actually have challenge response messages = content.get("detail", {}).get("messages") or [] messages = sorted(set(messages)) if message[-4:].lower() in ["<ol>", "<ul>"]: for m in messages: message += u"<li>{0!s}</li>\n".format(m) else: message += "\n" message += ", ".join(messages) message += "\n" # Add the footer message += footer content["detail"]["message"] = message response.set_data(json.dumps(content)) return response
def auth_user_does_not_exist(wrapped_function, user_object, passw, options=None): """ This decorator checks, if the user does exist at all. If the user does exist, the wrapped function is called. The wrapped function is usually token.check_user_pass, which takes the arguments (user, passw, options={}) :param wrapped_function: :param user_object: :param passw: :param options: Dict containing values for "g" and "clientip" :return: Tuple of True/False and reply-dictionary """ options = options or {} g = options.get("g") if g: pass_no_user = Match.user(g, scope=SCOPE.AUTH, action=ACTION.PASSNOUSER, user_object=user_object).policies(write_to_audit_log=False) if pass_no_user: # Check if user object exists if not user_object.exist(): g.audit_object.add_policy([p.get("name") for p in pass_no_user]) return True, {"message": u"user does not exist, accepted due to '{!s}'".format( pass_no_user[0].get("name"))} # If nothing else returned, we return the wrapped function return wrapped_function(user_object, passw, options)
def auth_user_has_no_token(wrapped_function, user_object, passw, options=None): """ This decorator checks if the user has a token at all. If the user has a token, the wrapped function is called. The wrapped function is usually token.check_user_pass, which takes the arguments (user, passw, options={}) :param wrapped_function: :param user_object: :param passw: :param options: Dict containing values for "g" and "clientip" :return: Tuple of True/False and reply-dictionary """ from privacyidea.lib.token import get_tokens options = options or {} g = options.get("g") if g: pass_no_token = Match.user(g, scope=SCOPE.AUTH, action=ACTION.PASSNOTOKEN, user_object=user_object).policies(write_to_audit_log=False) if pass_no_token: # Now we need to check, if the user really has no token. tokencount = get_tokens(user=user_object, count=True) if tokencount == 0: g.audit_object.add_policy([p.get("name") for p in pass_no_token]) return True, {"message": u"user has no token, accepted due to '{!s}'".format( pass_no_token[0].get("name"))} # If nothing else returned, we return the wrapped function return wrapped_function(user_object, passw, options)
def login_mode(wrapped_function, *args, **kwds): """ Decorator to decorate the lib.auth.check_webui_user function. Depending on ACTION.LOGINMODE it sets the check_otp parameter, to signal that the authentication should be performed against privacyIDEA. :param wrapped_function: Usually the function check_webui_user :param `*args`: arguments user_obj and password :param `**kwds`: keyword arguments like options and !check_otp! kwds["options"] contains the flask g :return: calls the original function with the modified "check_otp" argument """ # if tokenclass.check_pin is called in any other way, options may be None # or it might have no element "g". options = kwds.get("options") or {} g = options.get("g") if g: # We need the user but we do not need the password user_object = args[0] # get the policy login_mode_dict = Match.user(g, scope=SCOPE.WEBUI, action=ACTION.LOGINMODE, user_object=user_object).action_values(unique=True) if login_mode_dict: # There is a login mode policy if list(login_mode_dict)[0] == LOGINMODE.PRIVACYIDEA: # The original function should check against privacyidea! kwds["check_otp"] = True if list(login_mode_dict)[0] == LOGINMODE.DISABLE: # The login to the webui is disabled raise PolicyError("The login for this user is disabled.") return wrapped_function(*args, **kwds)
def no_detail_on_fail(request, response): """ This policy function is used with the AUTHZ scope. If the boolean value no_detail_on_fail is set, the details will be stripped if the authentication request failed. :param request: :param response: :return: """ content = response.json # get the serials from a policy definition detailPol = Match.action_only( g, scope=SCOPE.AUTHZ, action=ACTION.NODETAILFAIL).policies(write_to_audit_log=False) if detailPol and content.get("result", {}).get("value") is False: # The policy was set, we need to strip the details, if the # authentication was successful. (value=true) del content["detail"] response.set_data(json.dumps(content)) g.audit_object.add_policy([p.get("name") for p in detailPol]) return response
def get_default_settings(cls, g, params): """ This method returns a dictionary with additional settings for token enrollment. The settings that are evaluated are SCOPE.ADMIN|SCOPE.USER, action=trusted_Assertion_CA_path It sets a list of configured paths. The returned dictionary is added to the parameters of the API call. :param g: context object, see documentation of ``Match`` :param params: The call parameters :type params: dict :return: default parameters """ ret = {ACTION.TRUSTED_CA_PATH: DEFAULT_CA_PATH} (role, username, userrealm, adminuser, adminrealm) = determine_logged_in_userparams(g.logged_in_user, params) # Now we fetch CA-pathes from the policies paths = Match.generic(g, scope=role, action=ACTION.TRUSTED_CA_PATH, user=username, realm=userrealm, adminuser=adminuser, adminrealm=adminrealm).action_values( unique=False, allow_white_space_in_action=True) if paths: ret[ACTION.TRUSTED_CA_PATH] = list(paths) return ret
def challenge_response_wrapper(*args, **kwds): options = kwds.get("options", {}) g = options.get("g") token = args[0] user_object = kwds.get("user") or User() if g: allowed_tokentypes_dict = Match.user(g, scope=SCOPE.AUTH, action=ACTION.CHALLENGERESPONSE, user_object=user_object)\ .action_values(unique=False, write_to_audit_log=False) log.debug("Found these allowed tokentypes: {0!s}".format( list(allowed_tokentypes_dict))) # allowed_tokentypes_dict.keys() is a list of actions from several policies. I # could look like this: # ["tiqr hotp totp", "tiqr motp"] chal_resp_found = False for toks in allowed_tokentypes_dict: if token.get_tokentype().upper() in [ x.upper() for x in toks.split(" ") ]: # This token is allowed to to chal resp chal_resp_found = True g.audit_object.add_policy( allowed_tokentypes_dict.get(toks)) if not chal_resp_found: # No policy to allow this token to do challenge response return False f_result = func(*args, **kwds) return f_result
def _get_sms_text(options): """ This returns the SMSTEXT from the policy "smstext" options contains data like clientip, g, user and also the Request parameters like "challenge" or "pass". :param options: contains user and g object. :type options: dict :return: Message template :rtype: basestring """ message = "<otp>" g = options.get("g") user_object = options.get("user") if g: messages = Match.user(g, scope=SCOPE.AUTH, action=SMSACTION.SMSTEXT, user_object=user_object if user_object else None).action_values(unique=True) if len(messages) == 1: message = list(messages)[0] # Replace the {challenge}: message = message.format(challenge=options.get("challenge")) return message
def reset_all_user_tokens(wrapped_function, *args, **kwds): """ Resets all tokens if the corresponding policy is set. :param token: The successful token, the tokenowner is used to find policies. :param tokenobject_list: The list of all the tokens of the user :param options: options dictionary containing g. :return: None """ tokenobject_list = args[0] options = kwds.get("options") or {} g = options.get("g") allow_reset = kwds.get("allow_reset_all_tokens") r = wrapped_function(*args, **kwds) # A successful authentication was done if r[0] and g and allow_reset: token_owner = tokenobject_list[0].user reset_all = Match.user( g, scope=SCOPE.AUTH, action=ACTION.RESETALLTOKENS, user_object=token_owner if token_owner else None).policies() if reset_all: log.debug( "Reset failcounter of all tokens of {0!s}".format(token_owner)) for tok_obj_reset in tokenobject_list: try: tok_obj_reset.reset() except Exception: log.debug("registration token does not exist anymore and " "cannot be reset.") return r
def check_tokentype(request, response): """ This policy function is to be used in a decorator of an API function. It checks, if the token, that was used in the API call is of a type that is allowed to be used. If not, a PolicyException is raised. :param response: The response of the decorated function :type response: Response object :return: A new (maybe modified) response """ tokentype = response.json.get("detail", {}).get("type") user_object = request.User allowed_tokentypes = Match.user( g, scope=SCOPE.AUTHZ, action=ACTION.TOKENTYPE, user_object=user_object).action_values(unique=False) if tokentype and allowed_tokentypes and tokentype not in allowed_tokentypes: # If we have tokentype policies, but # the tokentype is not allowed, we raise an exception g.audit_object.log({ "success": False, 'action_detail': "Tokentype {0!r} not allowed for " "authentication".format(tokentype) }) raise PolicyError("Tokentype not allowed for authentication!") return response
def check_serial(request, response): """ This policy function is to be used in a decorator of an API function. It checks, if the token, that was used in the API call has a serial number that is allowed to be used. If not, a PolicyException is raised. :param response: The response of the decorated function :type response: Response object :return: A new (maybe modified) response """ serial = response.json.get("detail", {}).get("serial") # get the serials from a policy definition allowed_serials = Match.action_only(g, scope=SCOPE.AUTHZ, action=ACTION.SERIAL).action_values(unique=False) # If we can compare a serial and if we do serial matching! if serial and allowed_serials: serial_matches = False for allowed_serial in allowed_serials: if re.search(allowed_serial, serial): serial_matches = True break if serial_matches is False: g.audit_object.log({"action_detail": "Serial is not allowed for " "authentication!"}) raise PolicyError("Serial is not allowed for authentication!") return response
def no_detail_on_success(request, response): """ This policy function is used with the AUTHZ scope. If the boolean value no_detail_on_success is set, the details will be stripped if the authentication request was successful. :param request: :param response: :return: """ content = response.json # get the serials from a policy definition detailPol = Match.action_only(g, scope=SCOPE.AUTHZ, action=ACTION.NODETAILSUCCESS)\ .policies(write_to_audit_log=False) if detailPol and content.get("result", {}).get("value"): # The policy was set, we need to strip the details, if the # authentication was successful. (value=true) # None assures that we do not get an error, if "detail" does not exist. content.pop("detail", None) response.set_data(json.dumps(content)) g.audit_object.add_policy([p.get("name") for p in detailPol]) return response
def is_authorized(request, response): """ This policy decorator is used in the AUTHZ scope to decorate the /validate/check and /validate/triggerchallenge endpoint. I will cause authentication to fail, if the policy authorized=deny_access is set. :param request: :param response: :return: """ if not response.is_json: # This can happen with the validate/radiuscheck endpoint return response authorized_pol = Match.user(g, scope=SCOPE.AUTHZ, action=ACTION.AUTHORIZED, user_object=request.User).action_values( unique=True, allow_white_space_in_action=True) if authorized_pol: if list(authorized_pol)[0] == AUTHORIZED.DENY: raise ValidateError( "User is not authorized to authenticate under these conditions." ) return response
def api_endpoint(cls, request, g): """ This provides a function to be plugged into the API endpoint /ttype/u2f The u2f token can return the facet list at this URL. :param request: The Flask request :param g: The Flask global object g :return: Flask Response or text """ configured_app_id = get_from_config("u2f.appId") if configured_app_id is None: raise ParameterError("u2f is not configured") app_id = configured_app_id.strip("/") # Read the facets from the policies pol_facets = Match.action_only( g, scope=SCOPE.AUTH, action=U2FACTION.FACETS).action_values(unique=False) facet_list = ["https://{0!s}".format(x) for x in pol_facets] facet_list.append(app_id) log.debug("Sending facets lists for appId {0!s}: {1!s}".format( app_id, facet_list)) res = { "trustedFacets": [{ "version": { "major": 1, "minor": 0 }, "ids": facet_list }] } return "fido.trusted-apps+json", res
def config_lost_token(wrapped_function, *args, **kwds): """ Decorator to decorate the lib.token.lost_token function. Depending on ACTION.LOSTTOKENVALID, ACTION.LOSTTOKENPWCONTENTS, ACTION.LOSTTOKENPWLEN it sets the check_otp parameter, to signal how the lostToken should be generated. :param wrapped_function: Usually the function lost_token() :param args: argument "serial" as the old serial number :param kwds: keyword arguments like "validity", "contents", "pw_len" kwds["options"] contains the flask g :return: calls the original function with the modified "validity", "contents" and "pw_len" argument """ # if called in any other way, options may be None # or it might have no element "g". from privacyidea.lib.token import get_tokens options = kwds.get("options") or {} g = options.get("g") if g: # We need the old serial number, to determine the user - if it exist. serial = args[0] toks = get_tokens(serial=serial) if len(toks) == 1: user_object = toks[0].user # get the policy contents_dict = Match.user(g, scope=SCOPE.ENROLL, action=ACTION.LOSTTOKENPWCONTENTS, user_object=user_object if user_object else None)\ .action_values(unique=True) validity_dict = Match.user(g, scope=SCOPE.ENROLL, action=ACTION.LOSTTOKENVALID, user_object=user_object if user_object else None)\ .action_values(unique=True) pw_len_dict = Match.user(g, scope=SCOPE.ENROLL, action=ACTION.LOSTTOKENPWLEN, user_object=user_object if user_object else None)\ .action_values(unique=True) if contents_dict: kwds["contents"] = list(contents_dict)[0] if validity_dict: kwds["validity"] = int(list(validity_dict)[0]) if pw_len_dict: kwds["pw_len"] = int(list(pw_len_dict)[0]) return wrapped_function(*args, **kwds)
def auth_lastauth(wrapped_function, user_or_serial, passw, options=None): """ This decorator checks the policy settings of ACTION.LASTAUTH If the last authentication stored in tokeninfo last_auth_success of a token is exceeded, the authentication is denied. The wrapped function is usually token.check_user_pass, which takes the arguments (user, passw, options={}) OR token.check_serial_pass with the arguments (user, passw, options={}) :param wrapped_function: either check_user_pass or check_serial_pass :param user_or_serial: either the User user_or_serial or a serial :param passw: :param options: Dict containing values for "g" and "clientip" :return: Tuple of True/False and reply-dictionary """ # First we call the wrapped function res, reply_dict = wrapped_function(user_or_serial, passw, options) options = options or {} g = options.get("g") if g and res: # in case of a serial: if isinstance(user_or_serial, User): user_object = user_or_serial serial = reply_dict.get("serial") else: # in case of a serial: user_object = None serial = user_or_serial # In case of a passthru policy we have no serial in the response # So we may only continue, if we have a serial. if serial: from privacyidea.lib.token import get_tokens try: token = get_tokens(serial=serial)[0] except IndexError: # In the special case of a registration token, # the token does not exist anymore. So we immediately return return res, reply_dict last_auth_dict = Match.user(g, scope=SCOPE.AUTHZ, action=ACTION.LASTAUTH, user_object=user_object).action_values(unique=True, write_to_audit_log=False) if len(last_auth_dict) == 1: res = token.check_last_auth_newer(list(last_auth_dict)[0]) if not res: reply_dict["message"] = "The last successful " \ "authentication was %s. " \ "It is to long ago." % \ token.get_tokeninfo(ACTION.LASTAUTH) g.audit_object.add_policy(next(iter(last_auth_dict.values()))) # set the last successful authentication, if res still true if res: token.add_tokeninfo(ACTION.LASTAUTH, datetime.datetime.now(tzlocal())) return res, reply_dict
def auth_cache(wrapped_function, user_object, passw, options=None): """ Decorate lib.token:check_user_pass. Verify, if the authentication can be found in the auth_cache. :param wrapped_function: usually "check_user_pass" :param user_object: User who tries to authenticate :param passw: The PIN and OTP :param options: Dict containing values for "g" and "clientip". :return: Tuple of True/False and reply-dictionary """ options = options or {} g = options.get("g") auth_cache_dict = None if g: auth_cache_dict = Match.user(g, scope=SCOPE.AUTH, action=ACTION.AUTH_CACHE, user_object=user_object).action_values( unique=True, write_to_audit_log=False) if auth_cache_dict: auth_times = list(auth_cache_dict)[0].split("/") # determine first_auth from policy! first_offset = parse_timedelta(auth_times[0]) first_auth = datetime.datetime.utcnow() - first_offset last_auth = first_auth # Default if no last auth exists max_auths = 0 # Default value, 0 has no effect on verification # Use auth cache when number of allowed authentications is defined if len(auth_times) == 2: if re.match(r"^\d+$", auth_times[1]): max_auths = int(auth_times[1]) else: # Determine last_auth delta from policy last_offset = parse_timedelta(auth_times[1]) last_auth = datetime.datetime.utcnow() - last_offset result = verify_in_cache(user_object.login, user_object.realm, user_object.resolver, passw, first_auth=first_auth, last_auth=last_auth, max_auths=max_auths) if result: g.audit_object.add_policy(next(iter(auth_cache_dict.values()))) return True, {"message": "Authenticated by AuthCache."} # If nothing else returned, call the wrapped function res, reply_dict = wrapped_function(user_object, passw, options) if auth_cache_dict and res: # If authentication is successful, we store the password in auth_cache add_to_cache(user_object.login, user_object.realm, user_object.resolver, passw) return res, reply_dict
def auth_otppin(wrapped_function, *args, **kwds): """ Decorator to decorate the tokenclass.check_pin function. Depending on the ACTION.OTPPIN it * either simply accepts an empty pin * checks the pin against the userstore * or passes the request to the wrapped_function :param wrapped_function: In this case the wrapped function should be :py:func:`privacyidea.lib.tokenclass.TokenClass.check_pin` :param `*args`: args[1] is the pin :param `**kwds`: kwds["options"] contains the flask g :return: True or False """ # if tokenclass.check_pin is called in any other way, options may be None # or it might have no element "g". options = kwds.get("options") or {} g = options.get("g") if g: token = args[0] pin = args[1] clientip = options.get("clientip") user_object = kwds.get("user") if not user_object: # No user in the parameters, so we need to determine the owner of # the token user_object = token.user realms = token.get_realms() if not user_object and len(realms): # if the token has not owner, we take a realm. user_object = User("", realm=realms[0]) if not user_object: # If we still have no user and no tokenrealm, we create an empty # user object. user_object = User("", realm="") # get the policy otppin_dict = Match.user( g, scope=SCOPE.AUTH, action=ACTION.OTPPIN, user_object=user_object).action_values(unique=True) if otppin_dict: if list(otppin_dict)[0] == ACTIONVALUE.NONE: if pin == "": # No PIN checking, we expect an empty PIN! return True else: return False if list(otppin_dict)[0] == ACTIONVALUE.USERSTORE: rv = user_object.check_password(pin) return rv is not None # call and return the original check_pin function return wrapped_function(*args, **kwds)
def register_status(): """ This endpoint returns the information if registration is allowed or not. This is used by the UI to either display the registration button or not. :return: JSON with value=True or value=False """ resolvername = Match.action_only(g, scope=SCOPE.REGISTER, action=ACTION.RESOLVER)\ .action_values(unique=True) result = bool(resolvername) g.audit_object.log({"info": result, "success": True}) return send_result(result)
def check_verify_enrollment(request, response): """ This policy decorator is used in the ENROLL scope to decorate the /token/init If will check for action=verify_enrollment and ask the user in a 2nd step to provide information to verify, that the token was successfully enrolled. :param request: :param response: :return: """ serial = response.json.get("detail").get("serial") verify = request.all_data.get("verify") if verify: # In case we are in a 2nd step verification, we must early exit return response tokenobj_list = get_tokens(serial=serial) if len(tokenobj_list) == 1: tokenobj = tokenobj_list[0] # check if this token type can do verify enrollment if tokenobj.can_verify_enrollment: # Get policies verify_pol_dict = Match.user( g, scope=SCOPE.ENROLL, action=ACTION.VERIFY_ENROLLMENT, user_object=request.User).action_values( unique=False, allow_white_space_in_action=True, write_to_audit_log=False) # verify_pol_dict.keys() is a list of actions from several policies. It # could look like this: # ["hotp totp", "hotp email"] do_verify_enrollment = False for toks in verify_pol_dict: if tokenobj.get_tokentype().upper() in [ x.upper() for x in toks.split(" ") ]: # This token is supposed to do verify enrollment do_verify_enrollment = True g.audit_object.add_policy(verify_pol_dict.get(toks)) if do_verify_enrollment: content = response.json content["detail"][ "verify"] = tokenobj.prepare_verify_enrollment() content["detail"]["rollout_state"] = ROLLOUTSTATE.VERIFYPENDING tokenobj.token.rollout_state = ROLLOUTSTATE.VERIFYPENDING tokenobj.token.save() response.set_data(json.dumps(content)) else: log.warning("No distinct token object found in enrollment response!") return response
def add_user_detail_to_response(request, response): """ This policy decorated is used in the AUTHZ scope. If the boolean value add_user_in_response is set, the details will contain a dictionary "user" with all user details. :param request: :param response: :return: """ content = response.json # Check for ADD USER IN RESPONSE detail_pol = Match.user(g, scope=SCOPE.AUTHZ, action=ACTION.ADDUSERINRESPONSE, user_object=request.User)\ .policies(write_to_audit_log=False) if detail_pol and content.get("result", {}).get("value") and request.User: # The policy was set, we need to add the user # details ui = request.User.info.copy() ui["password"] = "" for key, value in ui.items(): if type(value) == datetime.datetime: ui[key] = str(value) content.setdefault("detail", {})["user"] = ui g.audit_object.add_policy([p.get("name") for p in detail_pol]) # Check for ADD RESOLVER IN RESPONSE detail_pol = Match.user(g, scope=SCOPE.AUTHZ, action=ACTION.ADDRESOLVERINRESPONSE, user_object=request.User)\ .policies(write_to_audit_log=False) if detail_pol and content.get("result", {}).get("value") and request.User: # The policy was set, we need to add the resolver and the realm content.setdefault("detail", {})["user-resolver"] = request.User.resolver content["detail"]["user-realm"] = request.User.realm g.audit_object.add_policy([p.get("name") for p in detail_pol]) response.set_data(json.dumps(content)) return response
def is_password_reset(g): """ Check if password reset is allowed. We need to check, if a user policy with password_reset exists AND if an editable resolver exists. Otherwise password_reset does not make any sense. :return: True or False """ rlist = get_resolver_list(editable=True) log.debug("Number of editable resolvers: {0!s}".format(len(rlist))) pwreset = Match.generic( g, scope=SCOPE.USER, action=ACTION.PASSWORDRESET).allowed(write_to_audit_log=False) log.debug("Password reset allowed via policies: {0!s}".format(pwreset)) return bool(rlist and pwreset)
def _get_auto_email(options): """ This returns the AUTOEMAIL setting. :param options: contains user and g object. :optins type: dict :return: True if an SMS should be sent automatically :rtype: bool """ autosms = False g = options.get("g") user_object = options.get("user") if g: autoemailpol = Match.user(g, scope=SCOPE.AUTH, action=EMAILACTION.EMAILAUTO, user_object=user_object).policies() autosms = len(autoemailpol) >= 1 return autosms
def check_tokeninfo(request, response): """ This policy function is used as a decorator for the validate API. It checks after a successful authentication if the token has a matching tokeninfo field. If it does not match, authorization is denied. Then a PolicyException is raised. :param response: The response of the decorated function :type response: Response object :return: A new modified response """ serial = response.json.get("detail", {}).get("serial") if serial: tokeninfos_pol = Match.action_only(g, scope=SCOPE.AUTHZ, action=ACTION.TOKENINFO)\ .action_values(unique=False, allow_white_space_in_action=True) if tokeninfos_pol: tokens = get_tokens(serial=serial) if len(tokens) == 1: token_obj = tokens[0] for tokeninfo_pol in tokeninfos_pol: try: key, regex, _r = tokeninfo_pol.split("/") value = token_obj.get_tokeninfo(key, "") if re.search(regex, value): log.debug( u"Regular expression {0!s} " u"matches the tokeninfo field {1!s}.".format( regex, key)) else: log.info( u"Tokeninfo field {0!s} with contents {1!s} " u"does not match {2!s}".format( key, value, regex)) raise PolicyError( "Tokeninfo field {0!s} with contents does not" " match regular expression.".format(key)) except ValueError: log.warning(u"invalid tokeinfo policy: {0!s}".format( tokeninfo_pol)) return response
def generic_challenge_response_resync(wrapped_function, *args, **kwds): """ Check if the authentication request results in an autosync Conditions: To do so we check for "otp1c" in the tokeninfo data. Policies: A policy defines that the token resync should be allowed this way. Note: The general config "autoresync" needs to be set anyways. args are: :param tokenobject_list: The list of all the tokens of the user, that will be checked :param passw: The password presented in the authentication. kwds are: :param options: options dictionary containing g :param user: The user_obj """ success, reply_dict = wrapped_function(*args, **kwds) options = kwds.get("options") or {} # After a failed authentication, we check if the token has an otp1c if not success and reply_dict.get("serial"): # FIXME: Only works if the one token has a unique PIN serial = reply_dict.get("serial") g = options.get("g") # The tokenlist can contain more than one token. So we get the matching token object token_obj = next(t for t in args[0] if t.token.serial == serial) if token_obj and token_obj.get_tokeninfo("otp1c"): # We have an entry for resync if g and Match.token(g, scope=SCOPE.AUTH, action=ACTION.RESYNC_VIA_MULTICHALLENGE, token_obj=token_obj).any(): reply_dict = _create_challenge( token_obj, CHALLENGE_TYPE.RESYNC, _("To resync your token, please enter the next OTP value")) return success, reply_dict
def _get_email_text_or_subject(options, action=EMAILACTION.EMAILTEXT, default="<otp>"): """ This returns the EMAILTEXT or EMAILSUBJECT from the policy "emailtext" or "emailsubject :param options: contains user and g object. :type options: dict :param action: The action - either emailtext or emailsubject :param default: If no policy can be found, this is the default text :return: Message template, MIME type (one of "plain", "html") :rtype: (basestring, basestring) """ message = default mimetype = "plain" g = options.get("g") user_object = options.get("user") if g: messages = Match.user(g, scope=SCOPE.AUTH, action=action, user_object=user_object if user_object else None)\ .action_values(unique=True, allow_white_space_in_action=True) if len(messages) == 1: message = list(messages)[0] message = message.format(challenge=options.get("challenge")) if message.startswith("file:"): # We read the template from the file. try: with open(message[5:], "r") as f: message = f.read() mimetype = "html" except Exception as e: # pragma: no cover message = default log.warning(u"Failed to read email template: {0!r}".format(e)) log.debug(u"{0!s}".format(traceback.format_exc())) return message, mimetype
def autoassign(request, response): """ This decorator decorates the function /validate/check. Depending on ACTION.AUTOASSIGN it checks if the user has no token and if the given OTP-value matches a token in the users realm, that is not yet assigned to any user. If a token can be found, it assigns the token to the user also taking into account ACTION.MAXTOKENUSER and ACTION.MAXTOKENREALM. :return: """ content = response.json # check, if the authentication was successful, then we need to do nothing if content.get("result").get("value") is False: user_obj = request.User #user_obj = get_user_from_param(request.all_data) password = request.all_data.get("pass", "") if user_obj.login and user_obj.realm: # If there is no user in the request (because it is a serial # authentication request) we immediately bail out # check if the policy is defined autoassign_values = Match.user(g, scope=SCOPE.ENROLL, action=ACTION.AUTOASSIGN, user_object=user_obj).action_values(unique=True, write_to_audit_log=False) # check if the user has no token if autoassign_values and get_tokens(user=user_obj, count=True) == 0: # Check if the token would match # get all unassigned tokens in the realm and look for # a matching OTP: realm_tokens = get_tokens(realm=user_obj.realm, assigned=False) for token_obj in realm_tokens: (res, pin, otp) = token_obj.split_pin_pass(password) if res: pin_check = True if list(autoassign_values)[0] == \ AUTOASSIGNVALUE.USERSTORE: # If the autoassign policy is set to userstore, # we need to check against the userstore. pin_check = user_obj.check_password(pin) if pin_check: otp_check = token_obj.check_otp(otp) if otp_check >= 0: # we found a matching token # check MAXTOKENUSER and MAXTOKENREALM check_max_token_user(request=request) check_max_token_realm(request=request) # Assign token assign_token(serial=token_obj.token.serial, user=user_obj, pin=pin) # Set the response to true content.get("result")["value"] = True # Set the serial number detail = content.setdefault("detail", {}) detail["serial"] = token_obj.token.serial detail["otplen"] = token_obj.token.otplen detail["type"] = token_obj.type detail["message"] = "Token assigned to user via Autoassignment" response.set_data(json.dumps(content)) g.audit_object.log( {"success": True, "info": "Token assigned via auto assignment", "serial": token_obj.token.serial}) # The token was assigned by autoassign. We save the first policy name g.audit_object.add_policy(next(iter(autoassign_values.values()))) break return response
def get_webui_settings(request, response): """ This decorator is used in the /auth API to add configuration information like the logout_time or the policy_template_url to the response. :param request: flask request object :param response: flask response object :return: the response """ content = response.json # check, if the authentication was successful, then we need to do nothing if content.get("result").get("status") is True: role = content.get("result").get("value").get("role") loginname = content.get("result").get("value").get("username") realm = content.get("result").get("value").get("realm") or get_default_realm() # At this point the logged in user is not necessarily a user object. It can # also be a local admin. logout_time_pol = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.LOGOUTTIME, user=loginname, realm=realm).action_values(unique=True) timeout_action_pol = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.TIMEOUT_ACTION, user=loginname, realm=realm).action_values(unique=True) token_page_size_pol = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.TOKENPAGESIZE, user=loginname, realm=realm).action_values(unique=True) user_page_size_pol = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.USERPAGESIZE, user=loginname, realm=realm).action_values(unique=True) token_wizard_2nd = bool(role == ROLE.USER and Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.TOKENWIZARD2ND, user=loginname, realm=realm).policies()) admin_dashboard = (role == ROLE.ADMIN and Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.ADMIN_DASHBOARD, user=loginname, realm=realm).any()) token_wizard = False dialog_no_token = False if role == ROLE.USER: user_obj = User(loginname, realm) user_token_num = get_tokens(user=user_obj, count=True) token_wizard_pol = Match.user(g, scope=SCOPE.WEBUI, action=ACTION.TOKENWIZARD, user_object=user_obj).any() # We also need to check, if the user has not tokens assigned. # If the user has no tokens, we run the wizard. If the user # already has tokens, we do not run the wizard. token_wizard = token_wizard_pol and (user_token_num == 0) dialog_no_token_pol = Match.user(g, scope=SCOPE.WEBUI, action=ACTION.DIALOG_NO_TOKEN, user_object=user_obj).any() dialog_no_token = dialog_no_token_pol and (user_token_num == 0) user_details_pol = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.USERDETAILS, user=loginname, realm=realm).policies() search_on_enter = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SEARCH_ON_ENTER, user=loginname, realm=realm).policies() hide_welcome = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.HIDE_WELCOME, user=loginname, realm=realm).any() hide_buttons = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.HIDE_BUTTONS, user=loginname, realm=realm).any() default_tokentype_pol = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.DEFAULT_TOKENTYPE, user=loginname, realm=realm).action_values(unique=True) show_seed = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SHOW_SEED, user=loginname, realm=realm).any() show_node = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SHOW_NODE, realm=realm).any() qr_ios_authenticator = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SHOW_IOS_AUTHENTICATOR, user=loginname, realm=realm).any() qr_android_authenticator = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SHOW_ANDROID_AUTHENTICATOR, user=loginname, realm=realm).any() qr_custom_authenticator_url = Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SHOW_CUSTOM_AUTHENTICATOR, user=loginname, realm=realm).action_values(unique=True) qr_image_android = create_img(DEFAULT_ANDROID_APP_URL) if qr_android_authenticator else None qr_image_ios = create_img(DEFAULT_IOS_APP_URL) if qr_ios_authenticator else None qr_image_custom = create_img(list(qr_custom_authenticator_url)[0]) if qr_custom_authenticator_url else None token_page_size = DEFAULT_PAGE_SIZE user_page_size = DEFAULT_PAGE_SIZE default_tokentype = DEFAULT_TOKENTYPE if len(token_page_size_pol) == 1: token_page_size = int(list(token_page_size_pol)[0]) if len(user_page_size_pol) == 1: user_page_size = int(list(user_page_size_pol)[0]) if len(default_tokentype_pol) == 1: default_tokentype = list(default_tokentype_pol)[0] logout_time = DEFAULT_LOGOUT_TIME if len(logout_time_pol) == 1: logout_time = int(list(logout_time_pol)[0]) timeout_action = DEFAULT_TIMEOUT_ACTION if len(timeout_action_pol) == 1: timeout_action = list(timeout_action_pol)[0] policy_template_url_pol = Match.action_only(g, scope=SCOPE.WEBUI, action=ACTION.POLICYTEMPLATEURL).action_values(unique=True) policy_template_url = DEFAULT_POLICY_TEMPLATE_URL if len(policy_template_url_pol) == 1: policy_template_url = list(policy_template_url_pol)[0] indexed_preset_attribute = Match.realm(g, scope=SCOPE.WEBUI, action="indexedsecret_preset_attribute", realm=realm).action_values(unique=True) if len(indexed_preset_attribute) == 1: content["result"]["value"]["indexedsecret_preset_attribute"] = list(indexed_preset_attribute)[0] # This only works for users, because the value of the policy does not change while logged in. if role == ROLE.USER and \ Match.user(g, SCOPE.USER, "indexedsecret_force_attribute", user_obj).action_values(unique=False): content["result"]["value"]["indexedsecret_force_attribute"] = 1 content["result"]["value"]["logout_time"] = logout_time content["result"]["value"]["token_page_size"] = token_page_size content["result"]["value"]["user_page_size"] = user_page_size content["result"]["value"]["policy_template_url"] = policy_template_url content["result"]["value"]["default_tokentype"] = default_tokentype content["result"]["value"]["user_details"] = len(user_details_pol) > 0 content["result"]["value"]["token_wizard"] = token_wizard content["result"]["value"]["token_wizard_2nd"] = token_wizard_2nd content["result"]["value"]["admin_dashboard"] = admin_dashboard content["result"]["value"]["dialog_no_token"] = dialog_no_token content["result"]["value"]["search_on_enter"] = len(search_on_enter) > 0 content["result"]["value"]["timeout_action"] = timeout_action content["result"]["value"]["hide_welcome"] = hide_welcome content["result"]["value"]["hide_buttons"] = hide_buttons content["result"]["value"]["show_seed"] = show_seed content["result"]["value"]["show_node"] = get_privacyidea_node() if show_node else "" content["result"]["value"]["subscription_status"] = subscription_status() content["result"]["value"]["qr_image_android"] = qr_image_android content["result"]["value"]["qr_image_ios"] = qr_image_ios content["result"]["value"]["qr_image_custom"] = qr_image_custom response.set_data(json.dumps(content)) return response