diff --git a/bones/bone.py b/bones/bone.py index 7d617ae..7c3fb57 100644 --- a/bones/bone.py +++ b/bones/bone.py @@ -366,16 +366,27 @@ def getUniquePropertyIndexValue( self, valuesCache, name ): """ Returns an hash for our current value, used to store in the uniqueProptertyValue index. """ - if valuesCache[name] is None: - return( None ) - h = hashlib.sha256() - h.update( unicode( valuesCache[name] ).encode("UTF-8") ) - res = h.hexdigest() - if isinstance( valuesCache[name], int ) or isinstance( valuesCache[name], float ) or isinstance( valuesCache[name], long ): - return("I-%s" % res ) - elif isinstance( valuesCache[name], str ) or isinstance( valuesCache[name], unicode ): - return("S-%s" % res ) - raise NotImplementedError("Type %s can't be safely used in an uniquePropertyIndex" % type(valuesCache[name]) ) + values = valuesCache[name] + if values is None: + return None + res = [] + # Make value a list so we can loop over it in any case + if not isinstance(values, list): + values = [values] + for item in values: + h = hashlib.sha256() + h.update( unicode( item ).encode("UTF-8") ) + # res = h.hexdigest() + if isinstance( item, int ) or isinstance( item, float ) or isinstance( item, long ): + res.append("I-%s" % h.hexdigest() ) + elif isinstance( item, str ) or isinstance( item, unicode ): + res.append("S-%s" % h.hexdigest() ) + else: + raise NotImplementedError("Type %s can't be safely used in an uniquePropertyIndex" % type(item) ) + if isinstance(valuesCache[name], list): + return res + # Return back to original non-list + return res[0] def getReferencedBlobs( self, valuesCache, name ): """ diff --git a/bones/stringBone.py b/bones/stringBone.py index a2b10b0..9b20ca2 100644 --- a/bones/stringBone.py +++ b/bones/stringBone.py @@ -382,5 +382,5 @@ def getUniquePropertyIndexValue(self, valuesCache, name): """ if not valuesCache[name] and not self.required: #Dont enforce a unique property on an empty string if we are required=False return( None ) - return( super( stringBone, self).getUniquePropertyIndexValue(valuesCache, name)) + return super( stringBone, self).getUniquePropertyIndexValue(valuesCache, name) diff --git a/modules/user.py b/modules/user.py index 3dc55a0..60d3bad 100644 --- a/modules/user.py +++ b/modules/user.py @@ -153,6 +153,8 @@ def login(self, name=None, password=None, skey="", *args, **kwargs): # Check if the username matches storedUserName = res.get("name.idx", "") + if isinstance(storedUserName, list): # Multiple emails: only use main one (first in list) + storedUserName = storedUserName[0] if len(storedUserName) != len(name.lower()): isOkay = False else: @@ -176,7 +178,7 @@ def login(self, name=None, password=None, skey="", *args, **kwargs): if not isOkay: skel=self.loginSkel() skel.fromClient({"name": name, "nomissing": "1"}) - return self.userModule.render.login(skel, loginFailed=True) + return self.userModule.render.login(skel, params={"loginFailed": True}) else: if not "password_salt" in res: #Update the password to the new, more secure format res[ "password_salt" ] = utils.generateRandomString( 13 ) @@ -202,7 +204,10 @@ def pwrecover( self, authtoken=None, skey=None, *args, **kwargs ): skel = self.lostPasswordSkel() if len(kwargs)==0 or not skel.fromClient(kwargs) or not securitykey.validate(skey): return self.userModule.render.passwdRecover(skel, tpl=self.passwordRecoveryTemplate) - user = self.userModule.viewSkel().all().filter("name.idx =", skel["name"].lower()).get() + skel_name = skel["name"] + if isinstance(skel_name, list): # Multiple emails: only use main one (first in list) + skel_name = skel_name[0] + user = self.userModule.viewSkel().all().filter("name.idx =", skel_name.lower()).get() if not user or user["status"]<10: # Unknown user or locked account skel.errors["name"] = _("Unknown user") @@ -366,6 +371,14 @@ def canHandle(self, userKey): user = db.Get(userKey) return all([(x in user and (x=="otptimedrift" or bool(user[x]))) for x in ["otpid", "otpkey", "otptimedrift"]]) + class otpSkel( RelSkel ): + otptoken = stringBone(descr="Token", required=True, caseSensitive=False, indexed=True) + + def render(self, **params): + return self.userModule.render.edit(self.otpSkel(), action="otp", + tpl=self.userModule.loginSecondFactorTemplate, + params=params) + def startProcessing(self, userKey): user = db.Get(userKey) if all([(x in user and user[x]) for x in ["otpid", "otpkey"]]): @@ -377,13 +390,10 @@ def startProcessing(self, userKey): "timestamp": time(), "failures": 0} session.current.markChanged() - return self.userModule.render.loginSucceeded(msg="X-VIUR-2FACTOR-TimeBasedOTP") + return self.render() return None - class otpSkel( RelSkel ): - otptoken = stringBone(descr="Token", required=True, caseSensitive=False, indexed=True) - def generateOtps(self, secret, timeDrift): """ Generates all valid tokens for the given secret @@ -414,12 +424,13 @@ def asBytes(valIn): @exposed @forceSSL - def otp(self, otptoken = None, skey = None, *args, **kwargs ): + def otp(self, otptoken = None, skey = None, *args, **kwargs): token = session.current.get("_otp_user") if not token: raise errors.Forbidden() if otptoken is None: - self.userModule.render.edit(self.otpSkel()) + return self.render() + if not securitykey.validate(skey): raise errors.PreconditionFailed() if token["failures"] > 3: @@ -431,7 +442,12 @@ def otp(self, otptoken = None, skey = None, *args, **kwargs ): otptoken = int(otptoken) except: # We got a non-numeric token - this cant be correct - self.userModule.render.edit(self.otpSkel(), tpl=self.otpTemplate) + + return self.render() + + #logging.debug(otptoken) + #logging.debug(validTokens) + #logging.debug(otptoken in validTokens) if otptoken in validTokens: userKey = session.current["_otp_user"]["uid"] @@ -451,7 +467,8 @@ def otp(self, otptoken = None, skey = None, *args, **kwargs ): token["failures"] += 1 session.current["_otp_user"] = token session.current.markChanged() - return self.userModule.render.edit(self.otpSkel(), loginFailed=True, tpl=self.otpTemplate) + + return self.render(secondFactorFailed=True) def updateTimeDrift(self, userKey, idx): """ @@ -477,6 +494,7 @@ class User(List): lostPasswordTemplate = "user_lostpassword" verifyEmailAddressMail = "user_verify_address" passwordRecoveryMail = "user_password_recovery" + loginSecondFactorTemplate = "user_login_secondfactor" authenticationProviders = [UserPassword, GoogleAccount] secondFactorProviders = [TimeBasedOTP] @@ -729,6 +747,7 @@ def createNewUserIfNotExists(): """ Create a new Admin user, if the userDB is empty """ + logging.warning("createNewUserIfNotExists") userMod = getattr(conf["viur.mainApp"], "user", None) if (userMod # We have a user module and isinstance(userMod, User) diff --git a/render/html/user.py b/render/html/user.py index 1aeaf3c..9d79c09 100644 --- a/render/html/user.py +++ b/render/html/user.py @@ -4,30 +4,30 @@ import default from server.skeleton import Skeleton -class Render( default.Render ): #Render user-data to xml +class Render(default.Render): #Render user-data to HTML loginTemplate = "user_login" - logoutSuccessTemplate = "user_logout_success" loginSuccessTemplate = "user_login_success" + logoutSuccessTemplate = "user_logout_success" verifySuccessTemplate = "user_verify_success" verifyFailedTemplate = "user_verify_failed" passwdRecoverInfoTemplate = "user_passwdrecover_info" - def login(self, authMethods, tpl=None, **kwargs): + def login(self, skel, tpl=None, **kwargs): if "loginTemplate" in dir(self.parent): tpl = tpl or self.parent.loginTemplate else: tpl = tpl or self.loginTemplate - template = self.getEnv().get_template(self.getTemplateFileName(tpl)) - return template.render(authMethods=authMethods, **kwargs) + return self.edit(skel, tpl=tpl, **kwargs) - def loginSucceeded( self, tpl=None, **kwargs ): - if "loginSuccessTemplate" in dir( self.parent ): + def loginSucceeded(self, tpl=None, **kwargs): + if "loginSuccessTemplate" in dir(self.parent): tpl = tpl or self.parent.loginSuccessTemplate else: tpl = tpl or self.loginSuccessTemplate - template= self.getEnv().get_template( self.getTemplateFileName( tpl ) ) - return( template.render( **kwargs ) ) + + template = self.getEnv().get_template(self.getTemplateFileName(tpl)) + return template.render(**kwargs) def logoutSuccess(self, tpl=None, **kwargs ): if "logoutSuccessTemplate" in dir( self.parent ): diff --git a/render/json/user.py b/render/json/user.py index 12ac59d..0c147df 100644 --- a/render/json/user.py +++ b/render/json/user.py @@ -11,8 +11,8 @@ def login(self, skel, **kwargs): return self.edit(skel, **kwargs) - def loginSucceeded(self, msg = "OKAY", **kwargs): - return json.dumps(msg) + def loginSucceeded(self, **kwargs): + return json.dumps("OKAY") def logoutSuccess(self, **kwargs): return json.dumps("OKAY") diff --git a/skeleton.py b/skeleton.py index e3d73b1..324f0de 100644 --- a/skeleton.py +++ b/skeleton.py @@ -394,17 +394,21 @@ def fromClient( self, data ): if boneInstance.unique: newVal = boneInstance.getUniquePropertyIndexValue(self.valuesCache, boneName) if newVal is not None: - try: - dbObj = db.Get(db.Key.from_path("%s_%s_uniquePropertyIndex" % (self.kindName, boneName), newVal)) - if dbObj["references"] != self["key"]: #This valus is taken (sadly, not by us) - complete = False - if isinstance(boneInstance.unique, unicode): - errorMsg = _(boneInstance.unique) - else: - errorMsg = _("This value is not available") - self.errors[boneName] = errorMsg - except db.EntityNotFoundError: - pass + if not isinstance(newVal, list): + # UniquePropertyIndexValue may be a list. Make all a list so we can treat all the same... + newVal = [newVal] + for item in newVal: + try: + dbObj = db.Get(db.Key.from_path("%s_%s_uniquePropertyIndex" % (self.kindName, boneName), item)) + if dbObj["references"] != self["key"]: #This valus is taken (sadly, not by us) + complete = False + if isinstance(boneInstance.unique, unicode): + errorMsg = _(boneInstance.unique) + else: + errorMsg = _("This value is not available") + self.errors[boneName] = errorMsg + except db.EntityNotFoundError: + pass if( len(data) == 0 or (len(data) == 1 and "key" in data) @@ -607,6 +611,9 @@ def txnUpdate(key, mergeFrom, clearUpdateTag): if bone.unique: if "%s.uniqueIndexValue" % key in dbObj: oldUniqueValues[key] = dbObj["%s.uniqueIndexValue" % key] + # oldUniqeValues[key] may be a list. Enforce list type for easy usage in this func + if oldUniqueValues[key] is not None and not isinstance(oldUniqueValues[key], list): + oldUniqueValues[key] = [oldUniqueValues[key]] # Merge the values from mergeFrom in if key in mergeFrom: @@ -641,22 +648,27 @@ def txnUpdate(key, mergeFrom, clearUpdateTag): # Check if the property is really unique newUniqueValues[key] = bone.getUniquePropertyIndexValue(self.valuesCache, key) + # newUniqueValues[key] may be a list: enforce list type for easy usage in this func if newUniqueValues[key] is not None: - try: - lockObj = db.Get(db.Key.from_path( - "%s_%s_uniquePropertyIndex" % (skel.kindName, key), - newUniqueValues[key])) - - if lockObj["references"] != ourKey: - # This value has been claimed, and that not by us - - raise ValueError( - "The unique value '%s' of bone '%s' has been recently claimed!" % - (self.valuesCache[key], key)) - - except db.EntityNotFoundError: # No lockObj found for that value, we can use that - pass - dbObj["%s.uniqueIndexValue" % key] = newUniqueValues[key] + if not isinstance(newUniqueValues[key], list): + newUniqueValues[key] = [newUniqueValues[key]] + for item in newUniqueValues[key]: + try: + lockObj = db.Get(db.Key.from_path( + "%s_%s_uniquePropertyIndex" % (skel.kindName, key), + item)) + + if lockObj["references"] != ourKey: + # This value has been claimed, and that not by us + + raise ValueError( + "One of the unique values '%s' of bone '%s' has been recently claimed!" % + (self.valuesCache[key], key)) + + except db.EntityNotFoundError: # No lockObj found for that value, we can use that + pass + dbObj["%s.uniqueIndexValue" % key] = newUniqueValues[key] \ + if bone.multiple else newUniqueValues[key][0] else: if "%s.uniqueIndexValue" % key in dbObj: @@ -715,36 +727,51 @@ def txnUpdate(key, mergeFrom, clearUpdateTag): for key, bone in skel.items(): if bone.unique: # Update/create/delete missing lock-objects - if key in oldUniqueValues and oldUniqueValues[key] != newUniqueValues[key]: - - # We had an old lock and its value changed - try: - # Try to delete the old lock - oldLockObj = db.Get(db.Key.from_path( - "%s_%s_uniquePropertyIndex" % (skel.kindName, key), - oldUniqueValues[key])) - if oldLockObj["references"] != ourKey: - # We've been supposed to have that lock - but we don't. - # Don't remove that lock as it now belongs to a different entry - logging.critical( - "Detected Database corruption! A Value-Lock had been reassigned!") - else: - # It's our lock which we don't need anymore - db.Delete(db.Key.from_path( - "%s_%s_uniquePropertyIndex" % ( - skel.kindName, key), - oldUniqueValues[key])) - except db.EntityNotFoundError as e: - logging.critical( - "Detected Database corruption! Could not delete stale lock-object!") - + # oldUniqueValues and newUniqueValues are lists; newUniqueValues[key] may be None! + # logging.error("delete0") + # logging.error(key) + # logging.error(oldUniqueValues) + # logging.error(newUniqueValues) + if key in oldUniqueValues and\ + oldUniqueValues[key] is not None and\ + ( + newUniqueValues[key] is None + or (oldUniqueValues[key]) != (newUniqueValues[key]) + ): + for item in oldUniqueValues[key]: + if newUniqueValues[key] is None or item not in newUniqueValues[key]: + # We had an old lock and its value changed + try: + # Try to delete the old lock + # Loop over old lockObjs and delete those not equal to new locks + oldLockObjKey = db.Key.from_path( + "%s_%s_uniquePropertyIndex" % (skel.kindName, key), + item) + oldLockObj = db.Get(oldLockObjKey) + # logging.error("delete1") + # logging.error(dbObj.keys()) + if oldLockObj["references"] != ourKey: + # We've been supposed to have that lock - but we don't. + # Don't remove that lock as it now belongs to a different entry + logging.critical( + "Detected Database corruption! A Value-Lock had been reassigned!") + else: + # It's our lock which we don't need anymore + # logging.error("delete2") + # logging.error(oldLockObjKey) + db.Delete(oldLockObjKey) + except db.EntityNotFoundError as e: + logging.critical( + "Detected Database corruption! Could not delete stale lock-object!") if newUniqueValues[key] is not None: - # Lock the new value - newLockObj = db.Entity( - "%s_%s_uniquePropertyIndex" % (skel.kindName, key), - name=newUniqueValues[key]) - newLockObj["references"] = str(dbObj.key()) - db.Put(newLockObj) + for item in newUniqueValues[key]: + # Lock the new value + # TODO: We could reduce write-ops if we won't write items existent in oldUniqeValues + newLockObj = db.Entity( + "%s_%s_uniquePropertyIndex" % (skel.kindName, key), + name=item) + newLockObj["references"] = str(dbObj.key()) + db.Put(newLockObj) return str(dbObj.key()), dbObj, skel @@ -844,10 +871,18 @@ def txnDelete(key, skel): # Ensure that we delete any value-lock objects remaining for this entry if bone.unique: try: + # dbObj["%s.uniqueIndexValue" % boneName] may now be a list: loop over all items + # and delete each s_uniquePropertyIndex db entry if "%s.uniqueIndexValue" % boneName in dbObj: - db.Delete(db.Key.from_path( - "%s_%s_uniquePropertyIndex" % (skel.kindName, boneName), - dbObj["%s.uniqueIndexValue" % boneName])) + oldUniqueValues = dbObj["%s.uniqueIndexValue" % boneName] + if oldUniqueValues is not None and not isinstance(oldUniqueValues, list): + oldUniqueValues = [oldUniqueValues] + logging.error("x3") + logging.error(oldUniqueValues) + for item in oldUniqueValues: + db.Delete(db.Key.from_path( + "%s_%s_uniquePropertyIndex" % (skel.kindName, boneName), + item)) except db.EntityNotFoundError: raise