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])
def cleanOldEmailsFromLog(*args, **kwargs): """ Start the QueryIter DeleteOldEmailsFromLog to remove old, successfully send emails from the queue """ qry = db.Query("viur-emails").filter("isSend =", True) \ .filter("creationDate <", utils.utcNow() - conf["viur.email.logRetention"]) DeleteEntitiesIter.startIterOnQuery(qry)
def validate(key: str, useSessionKey: bool) -> Union[bool, db.Entity]: """ Validates a security key. If useSessionKey is true, the key is expected to be the sessions current security key (or it's static security key). Otherwise it must be a key created with a duration (so it's not session dependent) :param key: The key to validate :param useSessionKey: If True, we validate against the session's skey, otherwise we'll lookup an unbound key :returns: False if the key was not valid for whatever reasons, the data (given during createSecurityKey) as dictionary or True if the dict is empty (or :param:useSessionKey was true). """ if useSessionKey: if key == "staticSessionKey": skeyHeaderValue = currentRequest.get().request.headers.get( "Sec-X-ViUR-StaticSKey") if skeyHeaderValue and currentSession.get( ).validateStaticSecurityKey(skeyHeaderValue): return True elif currentSession.get().validateSecurityKey(key): return True return False if not key: return False dbKey = db.Key(securityKeyKindName, key) dbObj = db.Get(dbKey) if dbObj: db.Delete(dbKey) until = dbObj["until"] if until < utcNow(): # This key has expired return False del dbObj["until"] if not dbObj: return True return dbObj return False
def wrapF(self, *args, **kwargs) -> Union[str, bytes]: currReq = currentRequest.get() if conf["viur.disableCache"] or currReq.disableCache: # Caching disabled if conf["viur.disableCache"]: logging.debug("Caching is disabled by config") return f(self, *args, **kwargs) # How many arguments are part of the way to the function called (and how many are just *args) offset = -len(currReq.args) or len(currReq.pathlist) path = "/" + "/".join(currReq.pathlist[:offset]) if not path in urls: # This path (possibly a sub-render) should not be cached logging.debug("Not caching for %s" % path) return f(self, *args, **kwargs) key = keyFromArgs(f, userSensitive, languageSensitive, evaluatedArgs, path, args, kwargs) if not key: # Something is wrong (possibly the parameter-count) # Let's call f, but we knew already that this will clash return f(self, *args, **kwargs) dbRes = db.Get(db.Key(viurCacheName, key)) if dbRes is not None: if not maxCacheTime \ or dbRes["creationtime"] > utils.utcNow() - timedelta(seconds=maxCacheTime): # We store it unlimited or the cache is fresh enough logging.debug("This request was served from cache.") currReq.response.headers['Content-Type'] = dbRes[ "content-type"] return dbRes["data"] # If we made it this far, the request wasn't cached or too old; we need to rebuild it oldAccessLog = db.startDataAccessLog() try: res = f(self, *args, **kwargs) finally: accessedEntries = db.endDataAccessLog(oldAccessLog) dbEntity = db.Entity(db.Key(viurCacheName, key)) dbEntity["data"] = res dbEntity["creationtime"] = utils.utcNow() dbEntity["path"] = path dbEntity["content-type"] = currReq.response.headers['Content-Type'] dbEntity["accessedEntries"] = list(accessedEntries) dbEntity.exclude_from_indexes = ["data", "content-type" ] # We can save 2 DB-Writs :) db.Put(dbEntity) logging.debug("This request was a cache-miss. Cache has been updated.") return res
def updateTxn(cacheKey): key = db.Key(self.rateLimitKind, cacheKey) obj = db.Get(key) if obj is None: obj = db.Entity(key) obj["value"] = 0 obj["value"] += 1 obj["expires"] = utils.utcNow() + timedelta(minutes=2 * self.minutes) db.Put(obj)
def _getCurrentTimeKey(self): """ :return: the current lockperiod used in second position of the memcache key """ dateTime = utils.utcNow() key = dateTime.strftime("%Y-%m-%d-%%s") secsinceMidnight = (dateTime - dateTime.replace( hour=0, minute=0, second=0, microsecond=0)).total_seconds() currentStep = int(secsinceMidnight / self.secondsPerStep) return key % currentStep
def secondFactorSucceeded(self, secondFactor, userKey): currSess = currentSession.get() logging.debug("Got SecondFactorSucceeded call from %s." % secondFactor) if currSess["_mayBeUserKey"] != userKey.id_or_name: raise errors.Forbidden() # Assert that the second factor verification finished in time if utils.utcNow( ) - currSess["_secondFactorStart"] > self.secondFactorTimeWindow: raise errors.RequestTimeout() return self.authenticateUser(userKey)
def ensureOwnModuleRootNode(self): """ Ensures, that general root-node for the current module exists. If no root-node exists yet, it will be created. :returns: The entity of the root-node. :rtype: :class:`server.db.Entity` """ key = "rep_module_repo" kindName = self.viewSkel("node").kindName return db.GetOrInsert(db.Key(kindName, key), creationdate=utils.utcNow(), rootNode=1)
def continueAuthenticationFlow(self, caller, userKey): currSess = currentSession.get() currSess["_mayBeUserKey"] = userKey.id_or_name currSess["_secondFactorStart"] = utils.utcNow() currSess.markChanged() for authProvider, secondFactor in self.validAuthenticationMethods: if isinstance(caller, authProvider): if secondFactor is None: # We allow sign-in without a second factor return self.authenticateUser(userKey) # This Auth-Request was issued from this authenticationProvider secondFactorProvider = self.secondFactorProviderByClass( secondFactor) if secondFactorProvider.canHandle(userKey): # We choose the first second factor provider which claims it can verify that user return secondFactorProvider.startProcessing(userKey) # Whoops.. This user logged in successfully - but we have no second factor provider willing to confirm it raise errors.NotAcceptable( "There are no more authentication methods to try") # Sorry...
def create(duration: Union[None, int] = None, **kwargs) -> str: """ Creates a new onetime Securitykey or returns the current sessions csrf-token. The custom data (given as keyword arguments) that can be stored with the key if :param:duration is set must be serializable by the datastore. :param duration: Make this key valid for a fixed timeframe (and independent of the current session) :returns: The new onetime key """ if not duration: return currentSession.get().getSecurityKey() key = generateRandomString() duration = int(duration) dbObj = db.Entity(db.Key(securityKeyKindName, key)) for k, v in kwargs.items(): dbObj[k] = v dbObj["until"] = utcNow() + timedelta(seconds=duration) db.Put(dbObj) return key
def sendEmailDeferred(emailKey: db.KeyClass): """ Callback from the Taskqueue to send the given Email :param emailKey: Database-Key of the email we should send """ logging.debug("Sending deferred email: %s" % str(emailKey)) queueEntity = db.Get(emailKey) assert queueEntity, "Email queue object went missing!" if queueEntity["isSend"]: return True elif queueEntity["errorCount"] > 3: raise ChildProcessError("Error-Count exceeded") transportClass = conf[ "viur.email.transportClass"] # First, ensure we're able to send email at all assert issubclass( transportClass, EmailTransport), "No or invalid email transportclass specified!" try: resultData = transportClass.deliverEmail( dests=queueEntity["dests"], sender=queueEntity["sender"], cc=queueEntity["cc"], bcc=queueEntity["bcc"], subject=queueEntity["subject"], body=queueEntity["body"], headers=queueEntity["headers"], attachments=queueEntity["attachments"]) except Exception: # Increase the errorCount and bail out queueEntity["errorCount"] += 1 db.Put(queueEntity) raise # If that transportFunction did not raise an error that email has been successfully send queueEntity["isSend"] = True queueEntity["sendDate"] = utils.utcNow() queueEntity["transportFuncResult"] = resultData queueEntity.exclude_from_indexes.add("transportFuncResult") db.Put(queueEntity) try: transportClass.transportSuccessfulCallback(queueEntity) except Exception as e: logging.exception(e)
def isQuotaAvailable(self): """ Checks if there's currently quota available for the current user/ip :return: True if there's quota available, False otherwise :rtype: bool """ endPoint = self._getEndpointKey() currentDateTime = utils.utcNow() secSinceMidnight = (currentDateTime - currentDateTime.replace( hour=0, minute=0, second=0, microsecond=0)).total_seconds() currentStep = int(secSinceMidnight / self.secondsPerStep) keyBase = currentDateTime.strftime("%Y-%m-%d-%%s") cacheKeys = [] for x in range(0, self.steps): cacheKeys.append( db.Key( self.rateLimitKind, "%s-%s-%s" % (self.resource, endPoint, keyBase % (currentStep - x)))) tmpRes = db.Get(cacheKeys) return sum([ x["value"] for x in tmpRes if currentDateTime < x["expires"] ]) <= self.maxRate
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)
def performMagic(self, valuesCache, name, isAdd): if (self.creationMagic and isAdd) or self.updateMagic: valuesCache[name] = utcNow()
def sendEMail(*, tpl: str = None, stringTemplate: str = None, skel: Union[None, Dict, "SkeletonInstance", List["SkeletonInstance"]] = None, sender: str = None, dests: Union[str, List[str]] = None, cc: Union[str, List[str]] = None, bcc: Union[str, List[str]] = None, headers: Dict[str, str] = None, attachments: List[Dict[str, Any]] = None, context: Union[db.DATASTORE_BASE_TYPES, List[db.DATASTORE_BASE_TYPES], db.Entity] = None, **kwargs) -> Any: """ General purpose function for sending e-mail. This function allows for sending e-mails, also with generated content using the Jinja2 template engine. Your have to implement a method which should be called to send the prepared email finally. For this you have to allocate *viur.email.transportClass* in conf. :param tpl: The name of a template from the deploy/emails directory. :param stringTemplate: This string is interpreted as the template contents. Alternative to load from template file. :param skel: The data made available to the template. In case of a Skeleton or SkelList, its parsed the usual way;\ Dictionaries are passed unchanged. :param sender: The address sending this mail. :param dests: A list of addresses to send this mail to. A bare string will be treated as a list with 1 address. :param cc: Carbon-copy recipients. A bare string will be treated as a list with 1 address. :param bcc: Blind carbon-copy recipients. A bare string will be treated as a list with 1 address. :param headers: Specify headers for this email. :param attachments: List of files to be sent within the mail as attachments. Each attachment must be a dictionary with these keys: filename (string): Name of the file that's attached. Always required content (bytes): Content of the attachment as bytes. Required for the send in blue API. mimetype (string): Mimetype of the file. Suggested parameter for other implementations (not used by SIB) gcsfile (string): Link to a GCS-File to include instead of content. Not supported by the current SIB implementation :param context: Arbitrary data that can be stored along the queue entry to be evaluated in transportSuccessfulCallback (useful for tracking delivery / opening events etc). .. Warning: As emails will be queued (and not send directly) you cannot exceed 1MB in total (for all text and attachments combined)! """ # First, ensure we're able to send email at all transportClass = conf[ "viur.email.transportClass"] # First, ensure we're able to send email at all assert issubclass( transportClass, EmailTransport), "No or invalid email transportclass specified!" # Ensure that all recipient parameters (dest, cc, bcc) are a list dests = normalizeToList(dests) cc = normalizeToList(cc) bcc = normalizeToList(bcc) assert dests or cc or bcc, "No destination address given" attachments = normalizeToList(attachments) if not (bool(stringTemplate) ^ bool(tpl)): raise ValueError( "You have to set the params 'tpl' xor a 'stringTemplate'.") if attachments: # Ensure each attachment has the filename key and rewrite each dict to db.Entity so we can exclude # it from being indexed for _ in range(0, len(attachments)): attachment = attachments.pop(0) assert "filename" in attachment entity = db.Entity() for k, v in attachment.items(): entity[k] = v entity.exclude_from_indexes.add(k) attachments.append(entity) assert all(["filename" in x for x in attachments ]), "Attachment is missing the filename key" # If conf["viur.email.recipientOverride"] is set we'll redirect any email to these address(es) if conf["viur.email.recipientOverride"]: logging.warning("Overriding destination %s with %s", dests, conf["viur.email.recipientOverride"]) oldDests = dests newDests = normalizeToList(conf["viur.email.recipientOverride"]) dests = [] for newDest in newDests: if newDest.startswith("@"): for oldDest in oldDests: dests.append( oldDest.replace(".", "_dot_").replace("@", "_at_") + newDest) else: dests.append(newDest) cc = bcc = [] elif conf["viur.email.recipientOverride"] is False: logging.warning( "Sending emails disabled by config[viur.email.recipientOverride]") return False if conf["viur.email.senderOverride"]: sender = conf["viur.email.senderOverride"] elif sender is None: sender = f"viur@{projectID}.appspotmail.com" subject, body = conf["viur.emailRenderer"](dests, tpl, stringTemplate, skel, **kwargs) # Push that email to the outgoing queue queueEntity = db.Entity(db.Key("viur-emails")) queueEntity["isSend"] = False queueEntity["errorCount"] = 0 queueEntity["creationDate"] = utils.utcNow() queueEntity["sender"] = sender queueEntity["dests"] = dests queueEntity["cc"] = cc queueEntity["bcc"] = bcc queueEntity["subject"] = subject queueEntity["body"] = body queueEntity["headers"] = headers queueEntity["attachments"] = attachments queueEntity["context"] = context queueEntity.exclude_from_indexes = ["body", "attachments", "context"] transportClass.validateQueueEntity( queueEntity) # Will raise an exception if the entity is not valid if utils.isLocalDevelopmentServer and not conf[ "viur.email.sendFromLocalDevelopmentServer"]: logging.info("Not sending email from local development server") logging.info("Subject: %s", queueEntity["subject"]) logging.info("Body: %s", queueEntity["body"]) logging.info("Recipients: %s", queueEntity["dests"]) return False db.Put(queueEntity) sendEmailDeferred(queueEntity.key, _queue="viur-emails") return True
def performMagic(self, valuesCache, name, isAdd): if (self.creationMagic and isAdd) or self.updateMagic: valuesCache[name] = utcNow().replace(microsecond=0).astimezone( self.guessTimeZone())
def singleValueFromClient(self, value, skel, name, origData): """ Reads a value from the client. If this value is valid for this bone, store this value and return None. Otherwise our previous value is left unchanged and an error-message is returned. :param name: Our name in the skeleton :type name: str :param data: *User-supplied* request-data :type data: dict :returns: str or None """ rawValue = value if str(rawValue).replace("-", "", 1).replace(".", "", 1).isdigit(): if int(rawValue) < -1 * (2**30) or int(rawValue) > (2**31) - 2: value = False # its invalid else: value = datetime.fromtimestamp(float(rawValue)) elif not self.date and self.time: try: if str(rawValue).count(":") > 1: (hour, minute, second) = [ int(x.strip()) for x in str(rawValue).split(":") ] value = time(hour=hour, minute=minute, second=second) elif str(rawValue).count(":") > 0: (hour, minute) = [ int(x.strip()) for x in str(rawValue).split(":") ] value = time(hour=hour, minute=minute) elif str(rawValue).replace("-", "", 1).isdigit(): value = time(second=int(rawValue)) else: value = False # its invalid except: value = False elif str(rawValue).lower().startswith("now"): tmpRes = utcNow().astimezone(self.guessTimeZone()) if len(str(rawValue)) > 4: try: tmpRes += timedelta(seconds=int(str(rawValue)[3:])) except: pass value = tmpRes else: try: timeZone = self.guessTimeZone() if " " in rawValue: # Date with time try: # Times with seconds if "-" in rawValue: # ISO Date value = datetime.strptime(str(rawValue), "%Y-%m-%d %H:%M:%S") elif "/" in rawValue: # Ami Date value = datetime.strptime(str(rawValue), "%m/%d/%Y %H:%M:%S") else: # European Date value = datetime.strptime(str(rawValue), "%d.%m.%Y %H:%M:%S") except: if "-" in rawValue: # ISO Date value = datetime.strptime(str(rawValue), "%Y-%m-%d %H:%M") elif "/" in rawValue: # Ami Date value = datetime.strptime(str(rawValue), "%m/%d/%Y %H:%M") else: # European Date value = datetime.strptime(str(rawValue), "%d.%m.%Y %H:%M") else: if "-" in rawValue: # ISO (Date only) value = datetime.strptime(str(rawValue), "%Y-%m-%d") elif "/" in rawValue: # Ami (Date only) value = datetime.strptime(str(rawValue), "%m/%d/%Y") else: # European (Date only) value = datetime.strptime(str(rawValue), "%d.%m.%Y") value = datetime(value.year, value.month, value.day, value.hour, value.minute, value.second, tzinfo=timeZone) except: value = False # its invalid if value is False: return self.getEmptyValue(), [ ReadFromClientError(ReadFromClientErrorSeverity.Invalid, name, "Invalid value entered") ] err = self.isInvalid(value) if err: return self.getEmptyValue(), [ ReadFromClientError(ReadFromClientErrorSeverity.Invalid, name, err) ] return value, None
def cleanOldRateLocks(*args, **kwargs): DeleteEntitiesIter.startIterOnQuery( db.Query(RateLimit.rateLimitKind).filter("expires <", utils.utcNow()))
def login(self, skey="", token="", *args, **kwargs): # FIXME: Check if already logged in if not conf.get("viur.user.google.clientID"): raise errors.PreconditionFailed( "Please configure 'viur.user.google.clientID' in your conf!") if not skey or not token: currentRequest.get().response.headers["Content-Type"] = "text/html" if currentRequest.get().response.headers.get( "cross-origin-opener-policy") == "same-origin": # We have to allow popups here currentRequest.get().response.headers[ "cross-origin-opener-policy"] = "same-origin-allow-popups" # Fixme: Render with Jinja2? tplStr = open( os.path.join(utils.coreBasePath, "viur/core/template/vi_user_google_login.html"), "r").read() tplStr = tplStr.replace("{{ clientID }}", conf["viur.user.google.clientID"]) extendCsp({ "script-src": ["sha256-JpzaUIxV/gVOQhKoDLerccwqDDIVsdn1JclA6kRNkLw="], "style-src": ["sha256-FQpGSicYMVC5jxKGS5sIEzrRjSJmkxKPaetUc7eamqc="] }) return tplStr if not securitykey.validate(skey, useSessionKey=True): raise errors.PreconditionFailed() userInfo = id_token.verify_oauth2_token( token, requests.Request(), conf["viur.user.google.clientID"]) if userInfo['iss'] not in { 'accounts.google.com', 'https://accounts.google.com' }: raise ValueError('Wrong issuer.') # Token looks valid :) uid = userInfo['sub'] email = userInfo['email'] addSkel = skeletonByKind(self.userModule.addSkel().kindName ) # Ensure that we have the full skeleton userSkel = addSkel().all().filter("uid =", uid).getSkel() if not userSkel: # We'll try again - checking if there's already an user with that email userSkel = addSkel().all().filter("name.idx =", email.lower()).getSkel() if not userSkel: # Still no luck - it's a completely new user if not self.registrationEnabled: if userInfo.get("hd") and userInfo["hd"] in conf[ "viur.user.google.gsuiteDomains"]: print("User is from domain - adding account") else: logging.warning("Denying registration of %s", email) raise errors.Forbidden( "Registration for new users is disabled") userSkel = addSkel() # We'll add a new user userSkel["uid"] = uid userSkel["name"] = email isAdd = True else: isAdd = False now = utils.utcNow() if isAdd or (now - userSkel["lastlogin"]) > datetime.timedelta(minutes=30): # Conserve DB-Writes: Update the user max once in 30 Minutes userSkel["lastlogin"] = now # if users.is_current_user_admin(): # if not userSkel["access"]: # userSkel["access"] = [] # if not "root" in userSkel["access"]: # userSkel["access"].append("root") # userSkel["gaeadmin"] = True # else: # userSkel["gaeadmin"] = False assert userSkel.toDB() return self.userModule.continueAuthenticationFlow( self, userSkel["key"])
def updateChangeDateTxn(key): obj = db.Get(key) obj["changedate"] = utcNow() db.Put(obj)
def cron(self, cronName="default", *args, **kwargs): global _callableTasks, _periodicTasks, _appengineServiceIPs req = currentRequest.get() if not req.isDevServer: if 'X-Appengine-Cron' not in req.request.headers: logging.critical( 'Detected an attempted XSRF attack. The header "X-AppEngine-Cron" was not set.' ) raise errors.Forbidden() if req.request.environ.get( "HTTP_X_APPENGINE_USER_IP") not in _appengineServiceIPs: logging.critical( 'Detected an attempted XSRF attack. This request did not originate from Cron.' ) raise errors.Forbidden() if cronName not in _periodicTasks: logging.warning( "Got Cron request '%s' which doesn't have any tasks" % cronName) for task, interval in _periodicTasks[cronName].items( ): # Call all periodic tasks bound to that queue periodicTaskName = task.periodicTaskName.lower() if interval: # Ensure this task doesn't get called to often lastCall = db.Get( db.Key("viur-task-interval", periodicTaskName)) if lastCall and utils.utcNow() - lastCall["date"] < timedelta( minutes=interval): logging.debug( "Skipping task %s - Has already run recently." % periodicTaskName) continue res = self.findBoundTask(task) try: if res: # Its bound, call it this way :) res[0]() else: task( ) # It seems it wasn't bound - call it as a static method except Exception as e: logging.error("Error calling periodic task %s", periodicTaskName) logging.exception(e) else: logging.debug("Successfully called task %s", periodicTaskName) if interval: # Update its last-call timestamp entry = db.Entity( db.Key("viur-task-interval", periodicTaskName)) entry["date"] = utils.utcNow() db.Put(entry) logging.debug("Periodic tasks complete") for currentTask in db.Query( "viur-queued-tasks").iter(): # Look for queued tasks db.Delete(currentTask.key()) if currentTask["taskid"] in _callableTasks: task = _callableTasks[currentTask["taskid"]]() tmpDict = {} for k in currentTask.keys(): if k == "taskid": continue tmpDict[k] = json.loads(currentTask[k]) try: task.execute(**tmpDict) except Exception as e: logging.error("Error executing Task") logging.exception(e) logging.debug("Scheduled tasks complete")
def mkDefered(func, self=__undefinedFlag_, *args, **kwargs): if not queueRegion: # Run tasks inline logging.info("Running inline: %s" % func) if self is __undefinedFlag_: task = lambda: func(*args, **kwargs) else: task = lambda: func(self, *args, **kwargs) req = currentRequest.get() if req: req.pendingTasks.append( task ) # < This property will be only exist on development server! else: # Warmup request or something - we have to call it now as we can't defer it :/ task() return # Ensure no result gets passed back from viur.core.utils import getCurrentUser try: req = currentRequest.get() except: # This will fail for warmup requests req = None if req is not None and req.request.headers.get("X-Appengine-Taskretrycount") \ and "DEFERED_TASK_CALLED" not in dir(req): # This is the deferred call req.DEFERED_TASK_CALLED = True # Defer recursive calls to an deferred function again. if self is __undefinedFlag_: return func(*args, **kwargs) else: return func(self, *args, **kwargs) else: try: if self.__class__.__name__ == "index": funcPath = func.__name__ else: funcPath = "%s/%s" % (self.modulePath, func.__name__) command = "rel" except: funcPath = "%s.%s" % (func.__name__, func.__module__) if self != __undefinedFlag_: args = ( self, ) + args # Reappend self to args, as this function is (hopefully) unbound command = "unb" taskargs = dict((x, kwargs.pop(("_%s" % x), None)) for x in ("countdown", "eta", "name", "target", "retry_options")) taskargs["url"] = "/_tasks/deferred" transactional = kwargs.pop("_transactional", False) taskargs["headers"] = {"Content-Type": "application/octet-stream"} queue = kwargs.pop("_queue", "default") # Try to preserve the important data from the current environment try: # We might get called inside a warmup request without session usr = currentSession.get().get("user") if "password" in usr: del usr["password"] except: usr = None env = {"user": usr} logging.error(env) try: env["lang"] = currentRequest.get().language except AttributeError: # This isn't originating from a normal request pass if db.IsInTransaction(): # We have to ensure transaction guarantees for that task also env["transactionMarker"] = db.acquireTransactionSuccessMarker() # We move that task at least 90 seconds into the future so the transaction has time to settle taskargs["countdown"] = max( 90, (taskargs.get("countdown") or 0)) # Countdown can be set to None if conf["viur.tasks.customEnvironmentHandler"]: # Check if this project relies on additional environmental variables and serialize them too assert isinstance(conf["viur.tasks.customEnvironmentHandler"], tuple) \ and len(conf["viur.tasks.customEnvironmentHandler"]) == 2 \ and callable(conf["viur.tasks.customEnvironmentHandler"][0]), \ "Your customEnvironmentHandler must be a tuple of two callable if set!" env["custom"] = conf["viur.tasks.customEnvironmentHandler"][0]( ) pickled = json.dumps((command, (funcPath, args, kwargs, env)), cls=JsonKeyEncoder).encode("UTF-8") project = utils.projectID location = queueRegion parent = taskClient.queue_path(project, location, queue) task = { 'app_engine_http_request': { # Specify the type of request. 'http_method': tasks_v2.HttpMethod.POST, 'relative_uri': '/_tasks/deferred' } } if taskargs.get("countdown"): # We must send a Timestamp Protobuf instead of a date-string timestamp = timestamp_pb2.Timestamp() timestamp.FromDatetime(utils.utcNow() + timedelta( seconds=taskargs["countdown"])) task['schedule_time'] = timestamp task['app_engine_http_request']['body'] = pickled # Use the client to build and send the task. response = taskClient.create_task(parent=parent, task=task) print('Created task {}'.format(response.name))