Example #1
0
 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
Example #2
0
	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
Example #3
0
    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)
Example #4
0
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)
Example #5
0
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)
Example #6
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
Example #7
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, 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