def extendAccessRights(self, skel): accessRights = skel.access.values.copy() for right in conf["viur.accessRights"]: accessRights[right] = translate( "server.modules.user.accessright.%s" % right, defaultText=right) skel.access.values = accessRights
def __init__( self, *, defaultValue: Union[None, Dict[str, Union[SelectBoneMultiple, SelectBoneValue]], SelectBoneMultiple] = None, values: Union[Dict, List, Tuple, Callable] = (), **kwargs ): """ Creates a new selectBone. :param defaultValue: key(s) which will be checked by default :param values: dict of key->value pairs from which the user can choose from. """ super().__init__(defaultValue=defaultValue, **kwargs) # handle list/tuple as dicts if isinstance(values, (list, tuple)): values = {i: translate(i) for i in values} assert isinstance(values, (dict, OrderedDict)) or callable(values) self._values = values
def strftime(self, format): """ Provides correct localized names for directives like %a which dont get translated on GAE properly This currently replaces %a, %A, %b, %B, %c, %x and %X. :param format: String containing the Format to apply. :type format: str :returns: str """ if "%c" in format: format = format.replace("%c", translate("const_datetimeformat")) if "%x" in format: format = format.replace("%x", translate("const_dateformat")) if "%X" in format: format = format.replace("%X", translate("const_timeformat")) if "%a" in format: format = format.replace( "%a", translate("const_day_%s_short" % int(super(ExtendedDateTime, self).strftime("%w")))) if "%A" in format: format = format.replace( "%A", translate("const_day_%s_long" % int(super(ExtendedDateTime, self).strftime("%w")))) if "%b" in format: format = format.replace( "%b", translate("const_month_%s_short" % int(super(ExtendedDateTime, self).strftime("%m")))) if "%B" in format: format = format.replace( "%B", translate("const_month_%s_long" % int(super(ExtendedDateTime, self).strftime("%m")))) return super(ExtendedDateTime, self).strftime(format)
class UserPassword(object): registrationEnabled = False registrationEmailVerificationRequired = True registrationAdminVerificationRequired = True verifySuccessTemplate = "user_verify_success" verifyEmailAddressMail = "user_verify_address" verifyFailedTemplate = "user_verify_failed" passwordRecoveryTemplate = "user_passwordrecover" passwordRecoveryMail = "user_password_recovery" passwordRecoveryAlreadySendTemplate = "user_passwordrecover_already_sent" passwordRecoverySuccessTemplate = "user_passwordrecover_success" passwordRecoveryInvalidTokenTemplate = "user_passwordrecover_invalid_token" passwordRecoveryInstuctionsSendTemplate = "user_passwordrecover_mail_sent" passwordRecoveryStep1Template = "user_passwordrecover_step1" passwordRecoveryStep2Template = "user_passwordrecover_step2" passwordRecoveryFailedTemplate = "user_passwordrecover_failed" # The default rate-limit for password recovery (10 tries each 15 minutes) passwordRecoveryRateLimit = RateLimit("user.passwordrecovery", 10, 15, "ip") # Default translations for password recovery passwordRecoveryKeyExpired = translate( key="viur.modules.user.passwordrecovery.keyexpired", defaultText="The key is expired. Please try again", hint="Shown when the user needs more than 10 minutes to paste the key") passwordRecoveryKeyInvalid = translate( key="viur.modules.user.passwordrecovery.keyinvalid", defaultText="The key is invalid. Please try again", hint="Shown when the user supplies an invalid key") passwordRecoveryUserNotFound = translate( key="viur.modules.user.passwordrecovery.usernotfound", defaultText="There is no account with this name", hint="We cant find an account with that name (Should never happen)") passwordRecoveryAccountLocked = translate( key="viur.modules.user.passwordrecovery.accountlocked", defaultText= "This account is currently locked. You cannot change it's password.", hint="Attempted password recovery on a locked account") def __init__(self, userModule, modulePath): super(UserPassword, self).__init__() self.userModule = userModule self.modulePath = modulePath @classmethod def getAuthMethodName(*args, **kwargs): return u"X-VIUR-AUTH-User-Password" class loginSkel(RelSkel): name = emailBone(descr="E-Mail", required=True, caseSensitive=False, indexed=True) password = passwordBone(descr="Password", indexed=True, params={"justinput": True}, required=True) class lostPasswordStep1Skel(RelSkel): name = emailBone(descr="Username", required=True) captcha = captchaBone(descr=u"Captcha", required=True) class lostPasswordStep2Skel(RelSkel): recoveryKey = stringBone(descr="Verification Code", required=True) password = passwordBone(descr="New Password", required=True) @exposed @forceSSL def login(self, name=None, password=None, skey="", *args, **kwargs): if self.userModule.getCurrentUser(): # Were already logged in return self.userModule.render.loginSucceeded() if not name or not password or not securitykey.validate( skey, useSessionKey=True): return self.userModule.render.login(self.loginSkel()) name = name.lower().strip() query = db.Query(self.userModule.viewSkel().kindName) res = query.filter("name.idx >=", name).getEntry() if res is None: res = { "password": { "pwhash": "-invalid-", "salt": "-invalid" }, "status": 0, "name": {} } passwd = pbkdf2(password[:conf["viur.maxPasswordLength"]], (res.get("password", None) or {}).get("salt", "")) isOkay = True # We do this exactly that way to avoid timing attacks # Check if the username matches storedUserName = (res.get("name") or {}).get("idx", "") if len(storedUserName) != len(name): isOkay = False else: for x, y in zip(storedUserName, name): if x != y: isOkay = False # Check if the password matches storedPasswordHash = (res.get("password", None) or {}).get("pwhash", "-invalid-") if len(storedPasswordHash) != len(passwd): isOkay = False else: for x, y in zip(storedPasswordHash, passwd): if x != y: isOkay = False accountStatus: Optional[int] = None # Verify that this account isn't blocked if res["status"] < 10: if isOkay: # The username and password is valid, in this case we can inform that user about his account status # (ie account locked or email verification pending) accountStatus = res["status"] isOkay = False if not isOkay: skel = self.loginSkel() return self.userModule.render.login(skel, loginFailed=True, accountStatus=accountStatus) else: return self.userModule.continueAuthenticationFlow(self, res.key) @exposed def pwrecover(self, *args, **kwargs): """ This implements the password recovery process which let them set a new password for their account after validating a code send to them by email. The process is as following: - The user enters his email adress - We'll generate a random code, store it in his session and call sendUserPasswordRecoveryCode - sendUserPasswordRecoveryCode will run in the background, check if we have a user with that name and send the code. It runs as a deferredTask so we don't leak the information if a user account exists. - If the user received his code, he can paste the code and set a new password for his account. To prevent automated attacks, the fist step is guarded by a captcha and we limited calls to this function to 10 actions per 15 minutes. (One complete recovery process consists of two calls). """ if not self.passwordRecoveryRateLimit.isQuotaAvailable(): raise errors.Forbidden() # Quota exhausted, bail out session = currentSession.get() request = currentRequest.get() recoverStep = session.get("user.auth_userpassword.pwrecover") if not recoverStep: # This is the first step, where we ask for the username of the account we'll going to reset the password on skel = self.lostPasswordStep1Skel() if not request.isPostRequest or not skel.fromClient(kwargs): return self.userModule.render.edit( skel, self.passwordRecoveryStep1Template) if not securitykey.validate(kwargs.get("skey"), useSessionKey=True): raise errors.PreconditionFailed() self.passwordRecoveryRateLimit.decrementQuota() recoveryKey = utils.generateRandomString( 13) # This is the key the user will have to Copy&Paste self.sendUserPasswordRecoveryCode( skel["name"].lower(), recoveryKey) # Send the code in the background session["user.auth_userpassword.pwrecover"] = { "name": skel["name"].lower(), "recoveryKey": recoveryKey, "creationdate": utcNow(), "errorCount": 0 } del recoveryKey return self.pwrecover( ) # Fall through to the second step as that key in the session is now set else: if request.isPostRequest and kwargs.get("abort") == "1" \ and securitykey.validate(kwargs.get("skey"), useSessionKey=True): # Allow a user to abort the process if a wrong email has been used session["user.auth_userpassword.pwrecover"] = None return self.pwrecover() # We're in the second step - the code has been send and is waiting for confirmation from the user if utcNow() - session["user.auth_userpassword.pwrecover"][ "creationdate"] > datetime.timedelta(minutes=15): # This recovery-process is expired; reset the session and start over session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryKeyExpired) skel = self.lostPasswordStep2Skel() if not skel.fromClient(kwargs) or not request.isPostRequest: return self.userModule.render.edit( skel, self.passwordRecoveryStep2Template) if not securitykey.validate(kwargs.get("skey"), useSessionKey=True): raise errors.PreconditionFailed() self.passwordRecoveryRateLimit.decrementQuota() if not hmac.compare_digest( session["user.auth_userpassword.pwrecover"]["recoveryKey"], skel["recoveryKey"]): # The key was invalid, increase error-count or abort this recovery process altogether session["user.auth_userpassword.pwrecover"]["errorCount"] += 1 if session["user.auth_userpassword.pwrecover"][ "errorCount"] > 3: session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryKeyInvalid) return self.userModule.render.edit( skel, self.passwordRecoveryStep2Template) # Let's try again # If we made it here, the key was correct, so we'd hopefully have a valid user for this uSkel = userSkel().all().filter( "name.idx =", session["user.auth_userpassword.pwrecover"]["name"]).getSkel() if not uSkel: # This *should* never happen - if we don't have a matching account we'll not send the key. session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryUserNotFound) if uSkel["status"] != 10: # The account is locked or not yet validated. Abort the process session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryAccountLocked) # Update the password, save the user, reset his session and show the success-template uSkel["password"] = skel["password"] uSkel.toDB() session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( None, self.passwordRecoverySuccessTemplate) @callDeferred def sendUserPasswordRecoveryCode(self, userName: str, recoveryKey: str) -> None: """ Sends the given recovery code to the user given in userName. This function runs deferred so there's no timing sidechannel that leaks if this user exists. Per default, we'll send the code by email (assuming we have working email delivery), but this can be overridden to send it by SMS or other means. We'll also update the changedate for this user, so no more than one code can be send to any given user in four hours. """ def updateChangeDateTxn(key): obj = db.Get(key) obj["changedate"] = utcNow() db.Put(obj) user = db.Query("user").filter("name.idx =", userName).getEntry() if user: if user.get("changedate") and user["changedate"] > utcNow( ) - datetime.timedelta(hours=4): # There is a changedate and the user has been modified in the last 4 hours - abort return # Update the changedate so no more than one email is send per 4 hours db.RunInTransaction(updateChangeDateTxn, user.key) email.sendEMail(tpl=self.passwordRecoveryMail, skel={"recoveryKey": recoveryKey}, dests=[userName]) @exposed def verify(self, skey, *args, **kwargs): data = securitykey.validate(skey, useSessionKey=False) skel = self.userModule.editSkel() if not data or not isinstance( data, dict) or "userKey" not in data or not skel.fromDB( data["userKey"].id_or_name): return self.userModule.render.view(None, self.verifyFailedTemplate) if self.registrationAdminVerificationRequired: skel["status"] = 2 else: skel["status"] = 10 skel.toDB() return self.userModule.render.view(skel, self.verifySuccessTemplate) def canAdd(self): return self.registrationEnabled def addSkel(self): """ Prepare the add-Skel for rendering. Currently only calls self.userModule.addSkel() and sets skel["status"].value depening on self.registrationEmailVerificationRequired and self.registrationAdminVerificationRequired :return: server.skeleton.Skeleton """ skel = self.userModule.addSkel() if self.registrationEmailVerificationRequired: defaultStatusValue = 1 elif self.registrationAdminVerificationRequired: defaultStatusValue = 2 else: # No further verification required defaultStatusValue = 10 skel.status.readOnly = True skel["status"] = defaultStatusValue skel.password.required = True # The user will have to set a password for his account return skel @forceSSL @exposed def add(self, *args, **kwargs): """ Allows guests to register a new account if self.registrationEnabled is set to true .. seealso:: :func:`addSkel`, :func:`onAdded`, :func:`canAdd` :returns: The rendered, added object of the entry, eventually with error hints. :raises: :exc:`server.errors.Unauthorized`, if the current user does not have the required permissions. :raises: :exc:`server.errors.PreconditionFailed`, if the *skey* could not be verified. """ if "skey" in kwargs: skey = kwargs["skey"] else: skey = "" if not self.canAdd(): raise errors.Unauthorized() skel = self.addSkel() if (len(kwargs) == 0 # no data supplied or skey == "" # no skey supplied or not currentRequest.get(). isPostRequest # bail out if not using POST-method or not skel.fromClient( kwargs) # failure on reading into the bones or ("bounce" in kwargs and kwargs["bounce"] == "1")): # review before adding # render the skeleton in the version it could as far as it could be read. return self.userModule.render.add(skel) if not securitykey.validate(skey, useSessionKey=True): raise errors.PreconditionFailed() skel.toDB() if self.registrationEmailVerificationRequired and str( skel["status"]) == "1": # The user will have to verify his email-address. Create an skey and send it to his address skey = securitykey.create(duration=60 * 60 * 24 * 7, userKey=utils.normalizeKey(skel["key"]), name=skel["name"]) skel.skey = baseBone(descr="Skey") skel["skey"] = skey email.sendEMail(dests=[skel["name"]], tpl=self.userModule.verifyEmailAddressMail, skel=skel) self.userModule.onAdded(skel) # Call onAdded on our parent user module return self.userModule.render.addSuccess(skel)
class userSkel(Skeleton): kindName = "user" # Properties required by google and custom auth name = emailBone(descr=u"E-Mail", required=True, readOnly=True, caseSensitive=False, searchable=True, indexed=True, unique=UniqueValue(UniqueLockMethod.SameValue, True, "Username already taken")) # Properties required by custom auth password = passwordBone(descr=u"Password", required=False, readOnly=True, visible=False) # Properties required by google auth uid = stringBone(descr=u"Google's UserID", indexed=True, required=False, readOnly=True, unique=UniqueValue(UniqueLockMethod.SameValue, False, "UID already in use")) gaeadmin = booleanBone(descr=u"Is GAE Admin", defaultValue=False, readOnly=True) # Generic properties access = selectBone( descr=u"Access rights", values=lambda: { right: translate("server.modules.user.accessright.%s" % right, defaultText=right) for right in sorted(conf["viur.accessRights"]) }, indexed=True, multiple=True) status = selectBone(descr=u"Account status", values={ 1: u"Waiting for email verification", 2: u"Waiting for verification through admin", 5: u"Account disabled", 10: u"Active" }, defaultValue=10, required=True, indexed=True) lastlogin = dateBone(descr=u"Last Login", readOnly=True, indexed=True) # One-Time Password Verification otpid = stringBone(descr=u"OTP serial", required=False, indexed=True, searchable=True) otpkey = credentialBone(descr=u"OTP hex key", required=False, indexed=True) otptimedrift = numericBone(descr=u"OTP time drift", readOnly=True, defaultValue=0)
class passwordBone(stringBone): """ A bone holding passwords. This is always empty if read from the database. If its saved, its ignored if its values is still empty. If its value is not empty, its hashed (with salt) and only the resulting hash will be written to the database """ type = "password" saltLength = 13 minPasswordLength = 8 passwordTests = [ lambda val: val.lower() != val, # Do we have upper-case characters? lambda val: val.upper() != val, # Do we have lower-case characters? lambda val: any([x in val for x in "0123456789"]), # Do we have any digits? lambda val: any([ x not in (string.ascii_lowercase + string.ascii_uppercase + string.digits) for x in val ]), # Special characters? ] passwordTestThreshold = 3 tooShortMessage = translate( "server.bones.passwordBone.tooShortMessage", defaultText= "The entered password is to short - it requires at least {{length}} characters." ) tooWeakMessage = translate("server.bones.passwordBone.tooWeakMessage", defaultText="The entered password is too weak.") def isInvalid(self, value): if not value: return False if len(value) < self.minPasswordLength: return self.tooShortMessage.translate( length=self.minPasswordLength) # Run our password test suite testResults = [] for test in self.passwordTests: testResults.append(test(value)) if sum(testResults) < self.passwordTestThreshold: return str(self.tooWeakMessage) return False def fromClient(self, skel: 'SkeletonInstance', name: str, data: dict) -> Union[None, List[ReadFromClientError]]: if not name in data: return [ ReadFromClientError(ReadFromClientErrorSeverity.NotSet, "Field not submitted") ] value = data.get(name) if not value: # Password-Bone is special: As it cannot be read don't set back no None if no value is given # This means an once set password can only be changed - but never deleted. return [ ReadFromClientError(ReadFromClientErrorSeverity.Empty, "No value entered") ] err = self.isInvalid(value) if err: return [ ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err) ] # As we don't escape passwords and allow most special characters we'll hash it early on so we don't open # an XSS attack vector if a password is echoed back to the client (which should not happen) salt = utils.generateRandomString(self.saltLength) passwd = pbkdf2(value[:conf["viur.maxPasswordLength"]], salt) skel[name] = {"pwhash": passwd, "salt": salt} def serialize(self, skel: 'SkeletonInstance', name: str, parentIndexed: bool) -> bool: if name in skel.accessedValues and skel.accessedValues[name]: value = skel.accessedValues[name] if isinstance( value, dict): # It is a pre-hashed value (probably fromClient) skel.dbEntity[name] = value else: # This has been set by skel["password"] = "******", we'll still have to hash it salt = utils.generateRandomString(self.saltLength) passwd = pbkdf2(value[:conf["viur.maxPasswordLength"]], salt) skel.dbEntity[name] = {"pwhash": passwd, "salt": salt} # Ensure our indexed flag is up2date indexed = self.indexed and parentIndexed if indexed and name in skel.dbEntity.exclude_from_indexes: skel.dbEntity.exclude_from_indexes.discard(name) elif not indexed and name not in skel.dbEntity.exclude_from_indexes: skel.dbEntity.exclude_from_indexes.add(name) return True return False def unserialize(self, skeletonValues, name): return False
class passwordBone(stringBone): """ A bone holding passwords. This is always empty if read from the database. If its saved, its ignored if its values is still empty. If its value is not empty, its hashed (with salt) and only the resulting hash will be written to the database """ type = "password" saltLength = 13 minPasswordLength = 8 passwordTests = [ lambda val: val.lower() != val, # Do we have upper-case characters? lambda val: val.upper() != val, # Do we have lower-case characters? lambda val: any([x in val for x in "0123456789"]), # Do we have any digits? lambda val: any([ x not in (string.ascii_lowercase + string.ascii_uppercase + string.digits) for x in val ]), # Special characters? ] passwordTestThreshold = 3 tooShortMessage = translate( "server.bones.passwordBone.tooShortMessage", defaultText= "The entered password is to short - it requires at least {{length}} characters." ) tooWeakMessage = translate("server.bones.passwordBone.tooWeakMessage", defaultText="The entered password is too weak.") def isInvalid(self, value): if not value: return False if len(value) < self.minPasswordLength: return self.tooShortMessage.translate( length=self.minPasswordLength) # Run our password test suite testResults = [] for test in self.passwordTests: testResults.append(test(value)) if sum(testResults) < self.passwordTestThreshold: return str(self.tooWeakMessage) return False def fromClient(self, valuesCache, name, data): if not name in data: return [ ReadFromClientError(ReadFromClientErrorSeverity.NotSet, name, "Field not submitted") ] value = data.get(name) if not value: return [ ReadFromClientError(ReadFromClientErrorSeverity.Empty, name, "No value entered") ] err = self.isInvalid(value) if err: return [ ReadFromClientError(ReadFromClientErrorSeverity.Invalid, name, err) ] valuesCache[name] = value def serialize(self, skeletonValues, name): if name in skeletonValues.accessedValues and skeletonValues.accessedValues[ name]: salt = utils.generateRandomString(self.saltLength) passwd = pbkdf2( skeletonValues.accessedValues[name] [:conf["viur.maxPasswordLength"]], salt) skeletonValues.entity[name] = {"pwhash": passwd, "salt": salt} return True return False def unserialize(self, skeletonValues, name): return False