def getUnverifiedUserByVeriCode(userId, veriCode): "Helper. Returns non-deactivated, unverified user." user = userMod.getUser(userId) if not user: return bu.abort("No such user or invitation.") if user.isVerified: return bu.abort("Already joined. Please log in.") if user.isDeactivated: return bu.abort("Account deactivated.") if not utils.checkPw(veriCode, user.hVeriCode): print("veriCode = %r" % veriCode) print("hVeriCode = %r" % user.hVeriCode) return bu.abort("Verification failed.") # ==> Valid veriCode. return user
def getSesh (strict=True, validateCsrf=None, req=None): "Get sesh (session-like) object with current 'user' property."; req = req or bu.request; # Defaults to (global) bu.request. if validateCsrf is None: validateCsrf = bool(req.method != "GET"); # Default behavior: false for GET, else true. # Check the 'userId' cookie: userId = bu.getCookie("userId", strict=strict, secret=K.AUTH_COOKIE_SECRET, req=req, ); if not userId: assert not strict; return dotsi.fy({"user": None}); # ==> Cookie found, signature valid. assert userId; if validateCsrf: # Ref: https://laravel.com/docs/5.8/csrf#csrf-x-csrf-token xCsrfToken = req.headers.get("X-Csrf-Token"); assert validateXCsrfToken(xCsrfToken, userId); # ==> CSRF TOKEN IS VALID. # ==> CSRF PREVENTED, if applicable. user = userMod.getUser(userId); assert user and user.isVerified; # User shouldn't be able to log-in if not .isVerified. Asserting here. if user.isDeactivated: # XXX:Note: Below 'log out' should force CLI logout. return bu.abort("ACCOUNT DEACTIVATED\n\n" + "Your account has been deactivated by your admin." + "You shall now proceed to log out." #+ ); # ==> User exists, is verified, non-deactivated. return dotsi.fy({"user": user});
def validateXCsrfToken (xCsrfToken, userId): try: #print("xCsrfToken = ", repr(xCsrfToken)); #print(bu.getCookie("userId")); unwrappedUserId = bu.hasher.signUnwrap( xCsrfToken, secret = K.ANTI_CSRF_SECRET, # <-- Even though we use `bu.hasher`, we specify an overriding secret. maxExpiryInDays = K.REMEMBER_ME_DAY_COUNT, # <-- Note: Can set to 0 or 0.0001 to see XSRF validation being enforced. # ^ This talks about signature-expiry, not cookie expiry. ); except bu.hasher.SignatureInvalidError as e: return bu.abort("1. X-CSRF validation failed. Please log out and then log back in."); # ==> No unwrapping error. if unwrappedUserId != userId: return bu.abort("2. X-CSRF validation failed. Please log out and then log back in."); # ==> Unwrapped data is as expected. return True;
def post_userCon_toggleUser_isDeactivated(): jdata = bu.get_jdata(ensure="thatUserId, preToggle_isDeactivated") sesh = auth.getSesh() assert auth.validateAccessLevel("admin", sesh.user) thatUser = userMod.getUser({ "_id": jdata.thatUserId, "isDeactivated": jdata. preToggle_isDeactivated, # This helps ensure that we don't accidently perform the opposite op. # No "isVerified" condition. # <-- This is as unverified (invited/pre-joined) users can be deactivated. }) if not thatUser: # ==> Couldn't find user to be de/reactivated. return bu.abort( "No such user. Can't de/reactivate. Please refresh (Ctrl+R) and retry." ) # ==> User to be de/reactivated found. if thatUser._id == sesh.user._id: assert not thatUser.isDeactivated # Assert active. If deactivated, this line must be unreachable. return bu.abort("One can't deactivate their own account.") # ==> Not trying to deactivate own account. if thatUser.isRootAdmin: assert not thatUser.isDeactivated # Assert active. Root-admin user cannot be deactivated. return bu.abort("Can't de/reactivate root admin.") # ==> `thatUser` is NOT the root admin. assert (thatUser and thatUser._id != sesh.user._id and # Like w/ db-query above, no 'isVerified' related assertion. thatUser.isDeactivated == jdata.preToggle_isDeactivated # and ) thatUser.update({ "isDeactivated": not thatUser.isDeactivated, }) assert userMod.validateUser(thatUser) userMod.replaceUser(thatUser) return { "user": userMod.snipUser(thatUser) }
def get_kb_article (articleId): article = articleMod.getArticle({ "_id": articleId, "status": "published_externally", }); if not article: raise bu.abort("Artile not found. " + "It may have been moved or deleted." #+ ); # ==> Found externally published article. return bu.render("extern-kb-article.html", **({ "article": article, "bleachUp": bleachUp, }));
def post_userCon_inviteUser(): jdata = bu.get_jdata(ensure=""" invitee_fname, invitee_lname, invitee_email, invitee_accessLevel, """) sesh = auth.getSesh() assert auth.validateAccessLevel("admin", sesh.user) inviter = sesh.user invitee = userMod.getUserByEmail(jdata.invitee_email) newVeriCode = userMod.genVeriCode() if not invitee: # ==> Invitee doesn't already exists. (fresh invite) invitee = userMod.buildUser( email=jdata.invitee_email, fname=jdata.invitee_fname, lname=jdata.invitee_lname, inviterId=inviter._id, veriCode=newVeriCode, accessLevel=jdata.invitee_accessLevel, ) else: # ==> Invitee already exists. (re-invite) if invitee.isVerified: return bu.abort( "That email address is already associated with a confirmed user." ) # ==> __NOT__ already verified. invitee.update({ "fname": jdata.invitee_fname, # Allow re-inviting w/ updated name. "lname": jdata.invitee_lname, "inviterId": inviter._id, # <-- Here, we're updating to latest inviter's id. "hVeriCode": utils.hashPw( newVeriCode ), # <-- On reinvite, new veriCode is gen'd, prev one expires. "accessLevel": jdata.invitee_accessLevel, }) assert invitee.email == jdata.invitee_email # <-- fname/lname/inviterId/hVeriCode can change, but not email. # ==> `invitee` object is now available. userMod.upsertUser(invitee) if emailer.checkSendingEnabled(): return_inviteLink = None sendInviteEmail(invitee, newVeriCode) else: return_inviteLink = genInviteLink(invitee, newVeriCode) return { "user": userMod.snipUser(invitee), "inviteLink": return_inviteLink, }
def post_userCon_loginDo(): email, pw = bu.unpack_jdata("email pw") # TODO: 'rememberMe' user = userMod.getUserByEmail(email) #print("user="******"User not found. Please retry or contact your admin for help.") # ==> User found. if not user.isVerified: return bu.abort( "Unconfirmed account. Please contact your admin for help.") # ==> User is verified. if user.isDeactivated: return bu.abort( "Account deactivated. Please contact your admin for help.") # ==> User is non-deactivated. if not utils.checkPw(pw, user.hpw): return bu.abort( "Login failed due to email/password mismatch. Please retry.") # ==> SUCCESS. User can log in. return auth.sendAuthSuccessResponse(user)
def post_categoryCon_deleteCategory(): jdata = bu.get_jdata(ensure="categoryId") sesh = auth.getSesh() category = categoryMod.getCategory(jdata.categoryId) assert category assert auth.validateCategoryDeletable(category, sesh.user) articleCount = articleMod.getArticleCount({ "categoryId": category._id, }) childCategoryCount = categoryMod.getCategoryCount({ "parentId": category._id, # i.e. child.parentId == category._id; }) if not (articleCount == 0 == childCategoryCount): return bu.abort("Can't delete non-empty categories.") assert articleCount == 0 == childCategoryCount categoryMod.deleteCategory(category) return { "deletedCategoryId": category._id }
def post_userCon_setupFirstUser(): jdata = bu.get_jdata(ensure="email fname lname pw") email, fname, lname, pw = utils.unpack(jdata, "email fname lname pw") print(email, fname, lname, pw) userCount = userMod.getUserCount() if userCount >= 1: return bu.abort( "Setup already completed. Please visite /login to log in.") user = userMod.buildUser( email=email, fname=fname, lname=lname, pw=pw, isRootAdmin=True, isVerified=True, accessLevel=auth.alm.getMaxLevel(), ) userMod.insertUser(user) return auth.sendAuthSuccessResponse(user)
def validateNonCircularParentage (unsavedCategory): if checkCircularParentage(unsavedCategory): raise bu.abort("Circular parentage detected."); # ==> Non-circular, i.e. valid. return True;
def validateArticleEditable (article, user): if not checkArticleEditable(article, user): raise bu.abort("Access level insufficient."); # otherwise ... return True;
def validateAccessLevel(reqdAccessLevel, user): ok = alm.contains(user.accessLevel, reqdAccessLevel); if not ok: raise bu.abort("Access level insufficient."); # otherwise ... => ok return True;