From 17c104c29f7b146e2137acd8bc19394cfa5472f9 Mon Sep 17 00:00:00 2001 From: Damirkhon Aloev Date: Mon, 5 Dec 2022 19:29:45 +0500 Subject: [PATCH 1/3] fix: added displayEmail field with AES_KEY encryption into user model --- girderformindlogger/models/user.py | 645 ++++++++++++++++------------- 1 file changed, 357 insertions(+), 288 deletions(-) diff --git a/girderformindlogger/models/user.py b/girderformindlogger/models/user.py index 0efa11dbd..e7c84d9ae 100644 --- a/girderformindlogger/models/user.py +++ b/girderformindlogger/models/user.py @@ -9,7 +9,13 @@ import six from girderformindlogger import events -from girderformindlogger.constants import AccessType, CoreEventHandler, TokenScope, USER_ROLES, ServerMode +from girderformindlogger.constants import ( + AccessType, + CoreEventHandler, + TokenScope, + USER_ROLES, + ServerMode, +) from girderformindlogger.exceptions import AccessException, ValidationException from girderformindlogger.models.aes_encrypt import AESEncryption, AccessControlledModel from girderformindlogger.models.setting import Setting @@ -26,21 +32,52 @@ class User(AESEncryption): """ def initialize(self): - self.name = 'user' - self.ensureIndices(['login', 'email', 'groupInvites.groupId', 'size', - 'created', 'deviceId', 'timezone', 'accountId']) + self.name = "user" + self.ensureIndices( + [ + "login", + "email", + "groupInvites.groupId", + "size", + "created", + "deviceId", + "timezone", + "accountId", + ] + ) self.prefixSearchFields = ( - 'login', ('firstName', 'i'), ('displayName', 'i'), 'email') - self.ensureTextIndex({ - 'login': 1, - 'displayName': 1, - 'email': 1, - }, language='none') - self.exposeFields(level=AccessType.READ, fields=( - '_id', 'login', 'public', 'displayName', 'firstName', 'lastName', - 'admin', 'email', 'created')) - self.exposeFields(level=AccessType.ADMIN, fields=( - 'size', 'status', 'emailVerified', 'creatorId')) + "login", + ("firstName", "i"), + ("displayName", "i"), + "email", + ) + self.ensureTextIndex( + { + "login": 1, + "displayName": 1, + "email": 1, + }, + language="none", + ) + self.exposeFields( + level=AccessType.READ, + fields=( + "_id", + "login", + "public", + "displayName", + "firstName", + "lastName", + "admin", + "email", + "created", + "displayEmail", + ), + ) + self.exposeFields( + level=AccessType.ADMIN, + fields=("size", "status", "emailVerified", "creatorId"), + ) # To ensure compatibility with authenticator apps, other defaults shouldn't be changed self._TotpFactory = TOTP.using( @@ -48,18 +85,22 @@ def initialize(self): wallet=None ) - self._cryptContext = CryptContext( - schemes=['bcrypt'] - ) + self._cryptContext = CryptContext(schemes=["bcrypt"]) - self.initAES([ - ('firstName', 64), - ('lastName', 64), - ('displayName', 64) - ]) + self.initAES( + [ + ("firstName", 64), + ("lastName", 64), + ("displayName", 64), + ("displayEmail", 64), + ] + ) - events.bind('model.user.save.created', - CoreEventHandler.USER_SELF_ACCESS, self._grantSelfAccess) + events.bind( + "model.user.save.created", + CoreEventHandler.USER_SELF_ACCESS, + self._grantSelfAccess, + ) # events.bind('model.user.save.created', # CoreEventHandler.USER_DEFAULT_FOLDERS, # self._addDefaultFolders) @@ -68,103 +109,114 @@ def validate(self, doc): """ Validate the user every time it is stored in the database. """ - for s in ['email', 'displayName', 'firstName']: + for s in ["email", "displayName", "firstName"]: if s in doc and doc[s] is None: - doc[s] = '' - doc['login'] = doc.get('login', '').lower().strip() - if not 'email_encrypted' in doc: - doc['email_encrypted'] = False - if not doc['email_encrypted']: - doc['email'] = doc.get('email', '').lower().strip() - doc['displayName'] = doc.get( - 'displayName', - doc.get('firstName', '') - ).strip() - doc['firstName'] = doc.get('firstName', '').strip() - doc['status'] = doc.get('status', 'enabled') - doc['deviceId'] = doc.get('deviceId', '') - doc['timezone'] = doc.get('timezone', 0) - - if 'salt' not in doc: + doc[s] = "" + doc["login"] = doc.get("login", "").lower().strip() + if not "email_encrypted" in doc: + doc["email_encrypted"] = False + if not doc["email_encrypted"]: + doc["email"] = doc.get("email", "").lower().strip() + doc["displayName"] = doc.get("displayName", doc.get("firstName", "")).strip() + doc["firstName"] = doc.get("firstName", "").strip() + doc["status"] = doc.get("status", "enabled") + doc["deviceId"] = doc.get("deviceId", "") + doc["timezone"] = doc.get("timezone", 0) + + if "salt" not in doc: # Internal error, this should not happen - raise Exception('Tried to save user document with no salt.') + raise Exception("Tried to save user document with no salt.") - if not doc['displayName']: - raise ValidationException('Display name must not be empty.', - 'displayName') + if not doc["displayName"]: + raise ValidationException("Display name must not be empty.", "displayName") - if doc['status'] not in ('pending', 'enabled', 'disabled'): + if doc["status"] not in ("pending", "enabled", "disabled"): raise ValidationException( - 'Status must be pending, enabled, or disabled.', 'status') + "Status must be pending, enabled, or disabled.", "status" + ) - if 'hashAlg' in doc: + if "hashAlg" in doc: # This is a legacy field; hash algorithms are now inline with the password hash - del doc['hashAlg'] + del doc["hashAlg"] - if not doc['email_encrypted'] and len(doc['email']) and not mail_utils.validateEmailAddress( - doc['email'] + if ( + not doc["email_encrypted"] + and len(doc["email"]) + and not mail_utils.validateEmailAddress(doc["email"]) ): - raise ValidationException('Invalid email address.', 'email') + raise ValidationException("Invalid email address.", "email") - if len(doc['email']): - q = {'email': doc['email']} - if '_id' in doc: - q['_id'] = {'$ne': doc['_id']} + if len(doc["email"]): + q = {"email": doc["email"]} + if "_id" in doc: + q["_id"] = {"$ne": doc["_id"]} existing = self.findOne(q) if existing is not None: - raise ValidationException('That email is already registered in the system.', ) + raise ValidationException( + "That email is already registered in the system.", + ) # Ensure unique logins - if len(doc['login']): - self._validateLogin(doc['login']) + if len(doc["login"]): + self._validateLogin(doc["login"]) - q = {'login': doc['login']} - if '_id' in doc: - q['_id'] = {'$ne': doc['_id']} + q = {"login": doc["login"]} + if "_id" in doc: + q["_id"] = {"$ne": doc["_id"]} existing = self.findOne(q) if existing is not None: - raise ValidationException('That login is already registered.', - 'login') + raise ValidationException("That login is already registered.", "login") # If this is the first user being created, make it an admin existing = self.findOne({}) if existing is None and config.getServerMode() == ServerMode.DEVELOPMENT: - doc['admin'] = True + doc["admin"] = True # Ensure settings don't stop this user from logging in - doc['emailVerified'] = True - doc['status'] = 'enabled' + doc["emailVerified"] = True + doc["status"] = "enabled" return doc def _validateLogin(self, login): - if '@' in login: + if "@" in login: # Hard-code this constraint so we can always easily distinguish # an email address from a login - raise ValidationException('Login may not contain "@".', 'login') + raise ValidationException('Login may not contain "@".', "login") - if not re.match(r'^[a-z][\da-z\-\.]{3,}$', login): + if not re.match(r"^[a-z][\da-z\-\.]{3,}$", login): raise ValidationException( - 'Login must be at least 4 characters, start with a letter, and may only contain ' - 'letters, numbers, dashes, and dots.', 'login') + "Login must be at least 4 characters, start with a letter, and may only contain " + "letters, numbers, dashes, and dots.", + "login", + ) def filter(self, doc, user, additionalKeys=None): filteredDoc = super(User, self).filter(doc, user, additionalKeys) level = self.getAccessLevel(doc, user) if level >= AccessType.ADMIN: - filteredDoc['otp'] = doc.get('otp', {}) - filteredDoc['otp'] = filteredDoc['otp'].get( - 'enabled', - False - ) if isinstance(filteredDoc['otp'], dict) else False + filteredDoc["otp"] = doc.get("otp", {}) + filteredDoc["otp"] = ( + filteredDoc["otp"].get("enabled", False) + if isinstance(filteredDoc["otp"], dict) + else False + ) return filteredDoc def hash(self, data): - x = hashlib.sha224(data.encode('utf-8')).hexdigest() + x = hashlib.sha224(data.encode("utf-8")).hexdigest() return x - def authenticate(self, login, password, otpToken=None, deviceId=None, timezone=0, loginAsEmail = False): + def authenticate( + self, + login, + password, + otpToken=None, + deviceId=None, + timezone=0, + loginAsEmail=False, + ): """ Validate a user login via username and password. If authentication fails, an ``AccessException`` is raised. @@ -181,38 +233,37 @@ def authenticate(self, login, password, otpToken=None, deviceId=None, timezone=0 :rtype: dict """ user = None - event = events.trigger('model.user.authenticate', { - 'login': login, - 'password': password - }) + event = events.trigger( + "model.user.authenticate", {"login": login, "password": password} + ) if event.defaultPrevented and len(event.responses): return event.responses[-1] login = login.lower().strip() - loginField = 'email' if loginAsEmail else 'login' + loginField = "email" if loginAsEmail else "login" - user = self.findOne({loginField: self.hash(login), 'email_encrypted': True}) + user = self.findOne({loginField: self.hash(login), "email_encrypted": True}) - if user is None and loginField == 'email': - user = self.findOne({loginField: login, 'email_encrypted': {'$ne': True}}) + if user is None and loginField == "email": + user = self.findOne({loginField: login, "email_encrypted": {"$ne": True}}) if user is None: - raise AccessException('Login failed. User not found.') + raise AccessException("Login failed. User not found.") # Handle users with no password if not self.hasPassword(user): - e = events.trigger('no_password_login_attempt', { - 'user': user, - 'password': password - }) + e = events.trigger( + "no_password_login_attempt", {"user": user, "password": password} + ) if len(e.responses): return e.responses[-1] raise ValidationException( - 'This user does not have a password. You must log in with an ' - 'external service, or reset your password.') + "This user does not have a password. You must log in with an " + "external service, or reset your password." + ) # Handle OTP token concatenation if otpToken is True and self.hasOtpEnabled(user): @@ -228,24 +279,23 @@ def authenticate(self, login, password, otpToken=None, deviceId=None, timezone=0 if self.hasOtpEnabled(user): if otpToken is None: raise AccessException( - 'User authentication must include a one-time password ' - '(typically in the "Girder-OTP" header).') + "User authentication must include a one-time password " + '(typically in the "Girder-OTP" header).' + ) self.verifyOtp(user, otpToken) elif isinstance(otpToken, six.string_types): - raise AccessException( - 'The user has not enabled one-time passwords.' - ) + raise AccessException("The user has not enabled one-time passwords.") # This has the same behavior as User.canLogin, but returns more # detailed error messages - if user.get('status', 'enabled') == 'disabled': - return { 'exception' : 'Account is disabled.' } + if user.get("status", "enabled") == "disabled": + return {"exception": "Account is disabled."} if self.emailVerificationRequired(user): - return { 'exception' : 'Email verification is required.' } + return {"exception": "Email verification is required."} if self.adminApprovalRequired(user): - return { 'exception' : 'Admin approval required' } + return {"exception": "Admin approval required"} return user @@ -263,27 +313,23 @@ def remove(self, user, progress=None, **kwargs): from girderformindlogger.models.token import Token # Delete all authentication tokens owned by this user - Token().removeWithQuery({'userId': user['_id']}) + Token().removeWithQuery({"userId": user["_id"]}) # Delete all pending group invites for this user - Group().update( - {'requests': user['_id']}, - {'$pull': {'requests': user['_id']}} - ) + Group().update({"requests": user["_id"]}, {"$pull": {"requests": user["_id"]}}) # Delete all of the folders under this user folderModel = Folder() - folders = folderModel.find({ - 'parentId': user['_id'], - 'parentCollection': 'user' - }) + folders = folderModel.find( + {"parentId": user["_id"], "parentCollection": "user"} + ) for folder in folders: folderModel.remove(folder, progress=progress, **kwargs) # Finally, delete the user document itself AccessControlledModel.remove(self, user) if progress: - progress.update(increment=1, message='Deleted user ' + user['login']) + progress.update(increment=1, message="Deleted user " + user["login"]) def getAdmins(self): """ @@ -291,7 +337,7 @@ def getAdmins(self): admins is assumed to be small enough that we will not need to page the results for now. """ - return self.find({'admin': True}) + return self.find({"admin": True}) def search(self, text=None, user=None, limit=0, offset=0, sort=None): """ @@ -314,8 +360,8 @@ def search(self, text=None, user=None, limit=0, offset=0, sort=None): cursor = self.find({}, sort=sort) return self.filterResultsByPermission( - cursor=cursor, user=user, level=AccessType.READ, limit=limit, - offset=offset) + cursor=cursor, user=user, level=AccessType.READ, limit=limit, offset=offset + ) def setUserName(self, user, userName, save=True): """ @@ -325,17 +371,16 @@ def setUserName(self, user, userName, save=True): :param userName: the new userName to be stored """ - oldUserName = user['login'] + oldUserName = user["login"] if len(userName) > 0: - user['login'] = userName + user["login"] = userName else: - raise Exception('username can\'t be empty') + raise Exception("username can't be empty") self.save(user) return oldUserName - def hasPassword(self, user): """ Returns whether or not the given user has a password stored in the @@ -346,7 +391,7 @@ def hasPassword(self, user): :type user: dict :returns: bool """ - return user['salt'] is not None + return user["salt"] is not None def setPassword(self, user, password, save=True): """ @@ -359,43 +404,56 @@ def setPassword(self, user, password, save=True): authenticating the user. """ if password is None: - user['salt'] = None + user["salt"] = None else: cur_config = config.getConfig() # Normally this would go in validate() but password is a special case. - if not re.match(cur_config['users']['password_regex'], password): - raise ValidationException(cur_config['users']['password_description'], 'password') + if not re.match(cur_config["users"]["password_regex"], password): + raise ValidationException( + cur_config["users"]["password_description"], "password" + ) - user['salt'] = self._cryptContext.hash(password) + user["salt"] = self._cryptContext.hash(password) if save: self.save(user) def getEncryptions(self, user, email, password): from girderformindlogger.models.applet import Applet as AppletModel - accounts = AccountProfile().getAccounts(user['_id']) + + accounts = AccountProfile().getAccounts(user["_id"]) applet_ids = [] for account in accounts: - for applet in account.get('applets', {}).get('user', []): + for applet in account.get("applets", {}).get("user", []): applet_ids.append(applet) - applets = [AppletModel().load(ObjectId(applet_id), AccessType.READ) for applet_id in applet_ids] + applets = [ + AppletModel().load(ObjectId(applet_id), AccessType.READ) + for applet_id in applet_ids + ] - privateKey = self.getPrivateKey(user['_id'], email, password) + privateKey = self.getPrivateKey(user["_id"], email, password) keys = {} for applet in applets: - encryption = applet['meta'].get('encryption') + encryption = applet["meta"].get("encryption") if encryption: - publicKey = self.getPublicKey(privateKey, encryption['appletPrime'], encryption['base']) - aesKey = self.getAESKey(privateKey, encryption['appletPublicKey'], encryption['appletPrime'], encryption['base']) - - keys[str(applet['_id'])] = { - 'userPublicKey': publicKey, - 'AESKey': aesKey + publicKey = self.getPublicKey( + privateKey, encryption["appletPrime"], encryption["base"] + ) + aesKey = self.getAESKey( + privateKey, + encryption["appletPublicKey"], + encryption["appletPrime"], + encryption["base"], + ) + + keys[str(applet["_id"])] = { + "userPublicKey": publicKey, + "AESKey": aesKey, } return (privateKey, keys) @@ -412,44 +470,43 @@ def initializeOtp(self, user): """ totp = self._TotpFactory.new() - user['otp'] = { - 'enabled': False, - 'totp': totp.to_dict() - } + user["otp"] = {"enabled": False, "totp": totp.to_dict()} # Use the brand name as the OTP issuer if it's non-default (since that's prettier and more # meaningful for users), but fallback to the site hostname if the brand name isn't set # (to disambiguate otherwise identical "Girder" issuers) # Prevent circular import from girderformindlogger.api.rest import getUrlParts + brandName = Setting().get(SettingKey.BRAND_NAME) defaultBrandName = Setting().getDefault(SettingKey.BRAND_NAME) # OTP URIs ( https://github.com/google/google-authenticator/wiki/Key-Uri-Format ) do not # allow colons, so use only the hostname component - serverHostname = getUrlParts().netloc.partition(':')[0] + serverHostname = getUrlParts().netloc.partition(":")[0] # Normally, the issuer would be set when "self._TotpFactory" is instantiated, but that # happens during model initialization, when there's no current request, so the server # hostname is not known then otpIssuer = brandName if brandName != defaultBrandName else serverHostname - return { - 'totpUri': totp.to_uri(label=user['login'], issuer=otpIssuer) - } + return {"totpUri": totp.to_uri(label=user["login"], issuer=otpIssuer)} def hasOtpEnabled(self, user): - return 'otp' in user and user['otp']['enabled'] + return "otp" in user and user["otp"]["enabled"] def verifyOtp(self, user, otpToken): - lastCounterKey = 'girderformindlogger.models.user.%s.otp.totp.counter' % user['_id'] + lastCounterKey = ( + "girderformindlogger.models.user.%s.otp.totp.counter" % user["_id"] + ) # The last successfully-authenticated key (which is blacklisted from reuse) lastCounter = rateLimitBuffer.get(lastCounterKey) or None try: totpMatch = self._TotpFactory.verify( - otpToken, user['otp']['totp'], last_counter=lastCounter) + otpToken, user["otp"]["totp"], last_counter=lastCounter + ) except TokenError as e: - raise AccessException('One-time password validation failed: %s' % e) + raise AccessException("One-time password validation failed: %s" % e) # The totpMatch.cache_seconds tells us prospectively how long the counter needs to be cached # for, but dogpile.cache expiration times work retrospectively (on "get"), so there's no @@ -457,9 +514,19 @@ def verifyOtp(self, user, otpToken): # "totp.verify" security) rateLimitBuffer.set(lastCounterKey, totpMatch.counter) - def createUser(self, login, password, displayName="", email="", - admin=False, public=False, currentUser=None, - firstName="", lastName="", encryptEmail=False): + def createUser( + self, + login, + password, + displayName="", + email="", + admin=False, + public=False, + currentUser=None, + firstName="", + lastName="", + encryptEmail=False, + ): """ Create a new user with the given information. @@ -472,49 +539,56 @@ def createUser(self, login, password, displayName="", email="", from girderformindlogger.models.group import Group from girderformindlogger.models.setting import Setting from girderformindlogger.models.account_profile import AccountProfile - requireApproval = Setting( - ).get(SettingKey.REGISTRATION_POLICY) == 'approve' + + requireApproval = Setting().get(SettingKey.REGISTRATION_POLICY) == "approve" email = "" if not email else email login = login.lower().strip() email = email.lower().strip() - if self.findOne({'email': email, 'email_encrypted': {'$ne': True}}) or self.findOne({'email': self.hash(email), 'email_encrypted': True}): - raise ValidationException('That email is already registered in the system.', ) + if self.findOne( + {"email": email, "email_encrypted": {"$ne": True}} + ) or self.findOne({"email": self.hash(email), "email_encrypted": True}): + raise ValidationException( + "That email is already registered in the system.", + ) if admin: requireApproval = False encryptEmail = False user = { - 'login': login, - 'email': email, - 'displayName': displayName if len( - displayName - ) else firstName if firstName is not None else "", - 'firstName': firstName, - 'lastName': lastName, - 'created': datetime.datetime.utcnow(), - 'emailVerified': False, - 'status': 'pending' if requireApproval else 'enabled', - 'admin': admin, - 'size': 0, - 'deviceId': '', - 'timezone': 0, - 'groups': [], - 'groupInvites': [ - { - "groupId": gi.get('_id'), - "level": 0 - } for gi in list(Group().find(query={"queue": email})) - ] if len(email) else [], - 'email_encrypted': encryptEmail, - 'accountName': '' + "login": login, + "email": email, + "displayName": displayName + if len(displayName) + else firstName + if firstName is not None + else "", + "firstName": firstName, + "lastName": lastName, + "created": datetime.datetime.utcnow(), + "emailVerified": False, + "status": "pending" if requireApproval else "enabled", + "admin": admin, + "size": 0, + "deviceId": "", + "timezone": 0, + "groups": [], + "groupInvites": [ + {"groupId": gi.get("_id"), "level": 0} + for gi in list(Group().find(query={"queue": email})) + ] + if len(email) + else [], + "email_encrypted": encryptEmail, + "accountName": "", + "displayEmail": email, } if encryptEmail: if len(email) == 0 or not mail_utils.validateEmailAddress(email): - raise ValidationException('Invalid email address.', 'email') + raise ValidationException("Invalid email address.", "email") - user['email'] = self.hash(user['email']) + user["email"] = self.hash(user["email"]) self.setPassword(user, password, save=False) self.setPublic(user, public, save=False) @@ -523,7 +597,7 @@ def createUser(self, login, password, displayName="", email="", self.setUserAccess( user, user=currentUser, level=AccessType.WRITE, save=False ) - user['creatorId'] = currentUser['_id'] + user["creatorId"] = currentUser["_id"] user = self.save(user) @@ -532,62 +606,59 @@ def createUser(self, login, password, displayName="", email="", doc=currentUser, user=user, level=AccessType.READ, save=True ) else: - user['creatorId'] = user['_id'] + user["creatorId"] = user["_id"] user = self.save(user) - verifyEmail = Setting().get(SettingKey.EMAIL_VERIFICATION) != 'disabled' + verifyEmail = Setting().get(SettingKey.EMAIL_VERIFICATION) != "disabled" if verifyEmail: self._sendVerificationEmail(user, email) if requireApproval: self._sendApprovalEmail(user) Group().update( - query={"queue": user['email']}, - update={"$pull": {"queue": user['email']}}, - multi=True + query={"queue": user["email"]}, + update={"$pull": {"queue": user["email"]}}, + multi=True, ) account = AccountProfile().createOwner(user) - user['accountId'] = account['_id'] - self.update({'_id': user['_id']}, {'$set': {'accountId': user['accountId']}}) + user["accountId"] = account["_id"] + self.update({"_id": user["_id"]}, {"$set": {"accountId": user["accountId"]}}) # self.createTemplatesFolder(user) user = self._getGroupInvitesFromProtoUser(user) self._deleteProtoUser(user) - return(user) + return user def createTemplatesFolder(self, user): from girderformindlogger.models.folder import Folder - existing = Folder().findOne({ - 'accountId': user['accountId'], - 'meta.contentType': 'templates' - }) + existing = Folder().findOne( + {"accountId": user["accountId"], "meta.contentType": "templates"} + ) if existing: return existing templatesFolder = Folder().createFolder( parent=user, - parentType='user', - name='templates folder for {} account'.format(user['firstName']), + parentType="user", + name="templates folder for {} account".format(user["firstName"]), creator=user, reuseExisting=True, allowRename=True, public=False, - accountId=user['accountId'] + accountId=user["accountId"], ) - return Folder().setMetadata(templatesFolder, { - 'contentType': 'templates' - }) + return Folder().setMetadata(templatesFolder, {"contentType": "templates"}) def canLogin(self, user): """ Returns True if the user is allowed to login, e.g. email verification is not needed and admin approval is not needed. """ - if user.get('status', 'enabled') == 'disabled': + if user.get("status", "enabled") == "disabled": return False if self.emailVerificationRequired(user): return False @@ -601,8 +672,11 @@ def emailVerificationRequired(self, user): yet verified their email address. """ from girderformindlogger.models.setting import Setting - return (not user['emailVerified']) and \ - (Setting().get(SettingKey.EMAIL_VERIFICATION) == 'required' or Setting().get(SettingKey.EMAIL_VERIFICATION) == 'enabled') + + return (not user["emailVerified"]) and ( + Setting().get(SettingKey.EMAIL_VERIFICATION) == "required" + or Setting().get(SettingKey.EMAIL_VERIFICATION) == "enabled" + ) def adminApprovalRequired(self, user): """ @@ -610,44 +684,37 @@ def adminApprovalRequired(self, user): this user is pending approval. """ from girderformindlogger.models.setting import Setting - return user.get('status', 'enabled') == 'pending' and \ - Setting().get(SettingKey.REGISTRATION_POLICY) == 'approve' + + return ( + user.get("status", "enabled") == "pending" + and Setting().get(SettingKey.REGISTRATION_POLICY) == "approve" + ) def _sendApprovalEmail(self, user): - url = '%s#user/%s' % ( - mail_utils.getEmailUrlPrefix(), str(user['_id'])) - text = mail_utils.renderTemplate('accountApproval.mako', { - 'user': user, - 'url': url - }) - mail_utils.sendMailToAdmins( - 'Girder: Account pending approval', - text) + url = "%s#user/%s" % (mail_utils.getEmailUrlPrefix(), str(user["_id"])) + text = mail_utils.renderTemplate( + "accountApproval.mako", {"user": user, "url": url} + ) + mail_utils.sendMailToAdmins("Girder: Account pending approval", text) def _sendApprovedEmail(self, user, email): - text = mail_utils.renderTemplate('accountApproved.mako', { - 'user': user, - 'url': mail_utils.getEmailUrlPrefix() - }) - mail_utils.sendMail( - 'Girder: Account approved', - text, - [email]) + text = mail_utils.renderTemplate( + "accountApproved.mako", + {"user": user, "url": mail_utils.getEmailUrlPrefix()}, + ) + mail_utils.sendMail("Girder: Account approved", text, [email]) def _sendVerificationEmail(self, user, email): from girderformindlogger.models.token import Token - token = Token().createToken( - user, days=1, scope=TokenScope.EMAIL_VERIFICATION) - url = '%s#useraccount/%s/verification/%s' % ( - mail_utils.getEmailUrlPrefix(), str(user['_id']), str(token['_id'])) - text = mail_utils.renderTemplate('emailVerification.mako', { - 'url': url - }) - mail_utils.sendMail( - 'Girder: Email verification', - text, - [email]) + token = Token().createToken(user, days=1, scope=TokenScope.EMAIL_VERIFICATION) + url = "%s#useraccount/%s/verification/%s" % ( + mail_utils.getEmailUrlPrefix(), + str(user["_id"]), + str(token["_id"]), + ) + text = mail_utils.renderTemplate("emailVerification.mako", {"url": url}) + mail_utils.sendMail("Girder: Email verification", text, [email]) def _grantSelfAccess(self, event): """ @@ -671,18 +738,22 @@ def _addDefaultFolders(self, event): from girderformindlogger.models.folder import Folder from girderformindlogger.models.setting import Setting - if Setting().get(SettingKey.USER_DEFAULT_FOLDERS) == 'public_private': + if Setting().get(SettingKey.USER_DEFAULT_FOLDERS) == "public_private": user = event.info publicFolder = Folder().createFolder( - user, 'Public', parentType='user', public=True, creator=user) + user, "Public", parentType="user", public=True, creator=user + ) privateFolder = Folder().createFolder( - user, 'Private', parentType='user', public=False, creator=user) + user, "Private", parentType="user", public=False, creator=user + ) # Give the user admin access to their own folders Folder().setUserAccess(publicFolder, user, AccessType.ADMIN, save=True) Folder().setUserAccess(privateFolder, user, AccessType.ADMIN, save=True) - def fileList(self, doc, user=None, path='', includeMetadata=False, subpath=True, data=True): + def fileList( + self, doc, user=None, path="", includeMetadata=False, subpath=True, data=True + ): """ This function generates a list of 2-tuples whose first element is the relative path to the file from the user's folders root and whose second @@ -707,16 +778,21 @@ def fileList(self, doc, user=None, path='', includeMetadata=False, subpath=True, from girderformindlogger.models.folder import Folder if subpath: - path = os.path.join(path, doc['login']) + path = os.path.join(path, doc["login"]) folderModel = Folder() # Eagerly evaluate this list, as the MongoDB cursor can time out on long requests - childFolders = list(folderModel.childFolders( - parentType='user', parent=doc, user=user, - fields=['name'] + (['meta'] if includeMetadata else []) - )) + childFolders = list( + folderModel.childFolders( + parentType="user", + parent=doc, + user=user, + fields=["name"] + (["meta"] if includeMetadata else []), + ) + ) for folder in childFolders: for (filepath, file) in folderModel.fileList( - folder, user, path, includeMetadata, subpath=True, data=data): + folder, user, path, includeMetadata, subpath=True, data=data + ): yield (filepath, file) def subtreeCount(self, doc, includeItems=True, user=None, level=None): @@ -735,14 +811,19 @@ def subtreeCount(self, doc, includeItems=True, user=None, level=None): count = 1 folderModel = Folder() - folders = folderModel.findWithPermissions({ - 'parentId': doc['_id'], - 'parentCollection': 'user' - }, fields='access', user=user, level=level) - - count += sum(folderModel.subtreeCount( - folder, includeItems=includeItems, user=user, level=level) - for folder in folders) + folders = folderModel.findWithPermissions( + {"parentId": doc["_id"], "parentCollection": "user"}, + fields="access", + user=user, + level=level, + ) + + count += sum( + folderModel.subtreeCount( + folder, includeItems=includeItems, user=user, level=level + ) + for folder in folders + ) return count def countFolders(self, user, filterUser=None, level=None): @@ -760,13 +841,15 @@ def countFolders(self, user, filterUser=None, level=None): """ from girderformindlogger.models.folder import Folder - fields = () if level is None else ('access', 'public') + fields = () if level is None else ("access", "public") folderModel = Folder() - folders = folderModel.findWithPermissions({ - 'parentId': user['_id'], - 'parentCollection': 'user' - }, fields=fields, user=filterUser, level=level) + folders = folderModel.findWithPermissions( + {"parentId": user["_id"], "parentCollection": "user"}, + fields=fields, + user=filterUser, + level=level, + ) return folders.count() @@ -783,67 +866,53 @@ def updateSize(self, doc): size = 0 fixes = 0 folderModel = Folder() - folders = folderModel.find({ - 'parentId': doc['_id'], - 'parentCollection': 'user' - }) + folders = folderModel.find({"parentId": doc["_id"], "parentCollection": "user"}) for folder in folders: # fix folder size if needed _, f = folderModel.updateSize(folder) fixes += f # get total recursive folder size - folder = folderModel.load(folder['_id'], force=True) + folder = folderModel.load(folder["_id"], force=True) size += folderModel.getSizeRecursive(folder) # fix value if incorrect - if size != doc.get('size'): - self.update({'_id': doc['_id']}, update={'$set': {'size': size}}) + if size != doc.get("size"): + self.update({"_id": doc["_id"]}, update={"$set": {"size": size}}) fixes += 1 return size, fixes def _getGroupInvitesFromProtoUser(self, doc): - """ - - """ + """ """ from girderformindlogger.models.protoUser import ProtoUser # Ensure unique emails - q = {'email': doc['email']} - if '_id' in doc: - q['_id'] = {'$ne': doc['_id']} + q = {"email": doc["email"]} + if "_id" in doc: + q["_id"] = {"$ne": doc["_id"]} existing = ProtoUser().findOne(q) if existing is not None: - doc['groupInvites'] = existing['groupInvites'] - return(doc) + doc["groupInvites"] = existing["groupInvites"] + return doc def _deleteProtoUser(self, doc): - """ - - """ + """ """ from girderformindlogger.models.protoUser import ProtoUser # Ensure unique emails - q = {'email': doc['email']} - if '_id' in doc: - q['_id'] = {'$ne': doc['_id']} + q = {"email": doc["email"]} + if "_id" in doc: + q["_id"] = {"$ne": doc["_id"]} existing = ProtoUser().findOne(q) if existing is not None: ProtoUser().remove(existing) def _verify_password(self, password, user): # Verify password - if not self._cryptContext.verify(password, user['salt']): - raise AccessException('Login failed.') + if not self._cryptContext.verify(password, user["salt"]): + raise AccessException("Login failed.") else: - return(True) + return True def get_users_by_ids(self, user_ids): return self.find( - query={ - '_id': { - '$in': user_ids - } - }, - fields=[ - 'timezone', 'deviceId' - ] + query={"_id": {"$in": user_ids}}, fields=["timezone", "deviceId"] ) From 775519bc94a37cd6f0b76c359839c11fca61d2e6 Mon Sep 17 00:00:00 2001 From: Damirkhon Aloev Date: Tue, 6 Dec 2022 13:59:35 +0500 Subject: [PATCH 2/3] reverting user model --- girderformindlogger/models/user.py | 645 +++++++++++++---------------- 1 file changed, 288 insertions(+), 357 deletions(-) diff --git a/girderformindlogger/models/user.py b/girderformindlogger/models/user.py index e7c84d9ae..0efa11dbd 100644 --- a/girderformindlogger/models/user.py +++ b/girderformindlogger/models/user.py @@ -9,13 +9,7 @@ import six from girderformindlogger import events -from girderformindlogger.constants import ( - AccessType, - CoreEventHandler, - TokenScope, - USER_ROLES, - ServerMode, -) +from girderformindlogger.constants import AccessType, CoreEventHandler, TokenScope, USER_ROLES, ServerMode from girderformindlogger.exceptions import AccessException, ValidationException from girderformindlogger.models.aes_encrypt import AESEncryption, AccessControlledModel from girderformindlogger.models.setting import Setting @@ -32,52 +26,21 @@ class User(AESEncryption): """ def initialize(self): - self.name = "user" - self.ensureIndices( - [ - "login", - "email", - "groupInvites.groupId", - "size", - "created", - "deviceId", - "timezone", - "accountId", - ] - ) + self.name = 'user' + self.ensureIndices(['login', 'email', 'groupInvites.groupId', 'size', + 'created', 'deviceId', 'timezone', 'accountId']) self.prefixSearchFields = ( - "login", - ("firstName", "i"), - ("displayName", "i"), - "email", - ) - self.ensureTextIndex( - { - "login": 1, - "displayName": 1, - "email": 1, - }, - language="none", - ) - self.exposeFields( - level=AccessType.READ, - fields=( - "_id", - "login", - "public", - "displayName", - "firstName", - "lastName", - "admin", - "email", - "created", - "displayEmail", - ), - ) - self.exposeFields( - level=AccessType.ADMIN, - fields=("size", "status", "emailVerified", "creatorId"), - ) + 'login', ('firstName', 'i'), ('displayName', 'i'), 'email') + self.ensureTextIndex({ + 'login': 1, + 'displayName': 1, + 'email': 1, + }, language='none') + self.exposeFields(level=AccessType.READ, fields=( + '_id', 'login', 'public', 'displayName', 'firstName', 'lastName', + 'admin', 'email', 'created')) + self.exposeFields(level=AccessType.ADMIN, fields=( + 'size', 'status', 'emailVerified', 'creatorId')) # To ensure compatibility with authenticator apps, other defaults shouldn't be changed self._TotpFactory = TOTP.using( @@ -85,22 +48,18 @@ def initialize(self): wallet=None ) - self._cryptContext = CryptContext(schemes=["bcrypt"]) - - self.initAES( - [ - ("firstName", 64), - ("lastName", 64), - ("displayName", 64), - ("displayEmail", 64), - ] + self._cryptContext = CryptContext( + schemes=['bcrypt'] ) - events.bind( - "model.user.save.created", - CoreEventHandler.USER_SELF_ACCESS, - self._grantSelfAccess, - ) + self.initAES([ + ('firstName', 64), + ('lastName', 64), + ('displayName', 64) + ]) + + events.bind('model.user.save.created', + CoreEventHandler.USER_SELF_ACCESS, self._grantSelfAccess) # events.bind('model.user.save.created', # CoreEventHandler.USER_DEFAULT_FOLDERS, # self._addDefaultFolders) @@ -109,114 +68,103 @@ def validate(self, doc): """ Validate the user every time it is stored in the database. """ - for s in ["email", "displayName", "firstName"]: + for s in ['email', 'displayName', 'firstName']: if s in doc and doc[s] is None: - doc[s] = "" - doc["login"] = doc.get("login", "").lower().strip() - if not "email_encrypted" in doc: - doc["email_encrypted"] = False - if not doc["email_encrypted"]: - doc["email"] = doc.get("email", "").lower().strip() - doc["displayName"] = doc.get("displayName", doc.get("firstName", "")).strip() - doc["firstName"] = doc.get("firstName", "").strip() - doc["status"] = doc.get("status", "enabled") - doc["deviceId"] = doc.get("deviceId", "") - doc["timezone"] = doc.get("timezone", 0) - - if "salt" not in doc: + doc[s] = '' + doc['login'] = doc.get('login', '').lower().strip() + if not 'email_encrypted' in doc: + doc['email_encrypted'] = False + if not doc['email_encrypted']: + doc['email'] = doc.get('email', '').lower().strip() + doc['displayName'] = doc.get( + 'displayName', + doc.get('firstName', '') + ).strip() + doc['firstName'] = doc.get('firstName', '').strip() + doc['status'] = doc.get('status', 'enabled') + doc['deviceId'] = doc.get('deviceId', '') + doc['timezone'] = doc.get('timezone', 0) + + if 'salt' not in doc: # Internal error, this should not happen - raise Exception("Tried to save user document with no salt.") + raise Exception('Tried to save user document with no salt.') - if not doc["displayName"]: - raise ValidationException("Display name must not be empty.", "displayName") + if not doc['displayName']: + raise ValidationException('Display name must not be empty.', + 'displayName') - if doc["status"] not in ("pending", "enabled", "disabled"): + if doc['status'] not in ('pending', 'enabled', 'disabled'): raise ValidationException( - "Status must be pending, enabled, or disabled.", "status" - ) + 'Status must be pending, enabled, or disabled.', 'status') - if "hashAlg" in doc: + if 'hashAlg' in doc: # This is a legacy field; hash algorithms are now inline with the password hash - del doc["hashAlg"] + del doc['hashAlg'] - if ( - not doc["email_encrypted"] - and len(doc["email"]) - and not mail_utils.validateEmailAddress(doc["email"]) + if not doc['email_encrypted'] and len(doc['email']) and not mail_utils.validateEmailAddress( + doc['email'] ): - raise ValidationException("Invalid email address.", "email") + raise ValidationException('Invalid email address.', 'email') - if len(doc["email"]): - q = {"email": doc["email"]} - if "_id" in doc: - q["_id"] = {"$ne": doc["_id"]} + if len(doc['email']): + q = {'email': doc['email']} + if '_id' in doc: + q['_id'] = {'$ne': doc['_id']} existing = self.findOne(q) if existing is not None: - raise ValidationException( - "That email is already registered in the system.", - ) + raise ValidationException('That email is already registered in the system.', ) # Ensure unique logins - if len(doc["login"]): - self._validateLogin(doc["login"]) + if len(doc['login']): + self._validateLogin(doc['login']) - q = {"login": doc["login"]} - if "_id" in doc: - q["_id"] = {"$ne": doc["_id"]} + q = {'login': doc['login']} + if '_id' in doc: + q['_id'] = {'$ne': doc['_id']} existing = self.findOne(q) if existing is not None: - raise ValidationException("That login is already registered.", "login") + raise ValidationException('That login is already registered.', + 'login') # If this is the first user being created, make it an admin existing = self.findOne({}) if existing is None and config.getServerMode() == ServerMode.DEVELOPMENT: - doc["admin"] = True + doc['admin'] = True # Ensure settings don't stop this user from logging in - doc["emailVerified"] = True - doc["status"] = "enabled" + doc['emailVerified'] = True + doc['status'] = 'enabled' return doc def _validateLogin(self, login): - if "@" in login: + if '@' in login: # Hard-code this constraint so we can always easily distinguish # an email address from a login - raise ValidationException('Login may not contain "@".', "login") + raise ValidationException('Login may not contain "@".', 'login') - if not re.match(r"^[a-z][\da-z\-\.]{3,}$", login): + if not re.match(r'^[a-z][\da-z\-\.]{3,}$', login): raise ValidationException( - "Login must be at least 4 characters, start with a letter, and may only contain " - "letters, numbers, dashes, and dots.", - "login", - ) + 'Login must be at least 4 characters, start with a letter, and may only contain ' + 'letters, numbers, dashes, and dots.', 'login') def filter(self, doc, user, additionalKeys=None): filteredDoc = super(User, self).filter(doc, user, additionalKeys) level = self.getAccessLevel(doc, user) if level >= AccessType.ADMIN: - filteredDoc["otp"] = doc.get("otp", {}) - filteredDoc["otp"] = ( - filteredDoc["otp"].get("enabled", False) - if isinstance(filteredDoc["otp"], dict) - else False - ) + filteredDoc['otp'] = doc.get('otp', {}) + filteredDoc['otp'] = filteredDoc['otp'].get( + 'enabled', + False + ) if isinstance(filteredDoc['otp'], dict) else False return filteredDoc def hash(self, data): - x = hashlib.sha224(data.encode("utf-8")).hexdigest() + x = hashlib.sha224(data.encode('utf-8')).hexdigest() return x - def authenticate( - self, - login, - password, - otpToken=None, - deviceId=None, - timezone=0, - loginAsEmail=False, - ): + def authenticate(self, login, password, otpToken=None, deviceId=None, timezone=0, loginAsEmail = False): """ Validate a user login via username and password. If authentication fails, an ``AccessException`` is raised. @@ -233,37 +181,38 @@ def authenticate( :rtype: dict """ user = None - event = events.trigger( - "model.user.authenticate", {"login": login, "password": password} - ) + event = events.trigger('model.user.authenticate', { + 'login': login, + 'password': password + }) if event.defaultPrevented and len(event.responses): return event.responses[-1] login = login.lower().strip() - loginField = "email" if loginAsEmail else "login" + loginField = 'email' if loginAsEmail else 'login' - user = self.findOne({loginField: self.hash(login), "email_encrypted": True}) + user = self.findOne({loginField: self.hash(login), 'email_encrypted': True}) - if user is None and loginField == "email": - user = self.findOne({loginField: login, "email_encrypted": {"$ne": True}}) + if user is None and loginField == 'email': + user = self.findOne({loginField: login, 'email_encrypted': {'$ne': True}}) if user is None: - raise AccessException("Login failed. User not found.") + raise AccessException('Login failed. User not found.') # Handle users with no password if not self.hasPassword(user): - e = events.trigger( - "no_password_login_attempt", {"user": user, "password": password} - ) + e = events.trigger('no_password_login_attempt', { + 'user': user, + 'password': password + }) if len(e.responses): return e.responses[-1] raise ValidationException( - "This user does not have a password. You must log in with an " - "external service, or reset your password." - ) + 'This user does not have a password. You must log in with an ' + 'external service, or reset your password.') # Handle OTP token concatenation if otpToken is True and self.hasOtpEnabled(user): @@ -279,23 +228,24 @@ def authenticate( if self.hasOtpEnabled(user): if otpToken is None: raise AccessException( - "User authentication must include a one-time password " - '(typically in the "Girder-OTP" header).' - ) + 'User authentication must include a one-time password ' + '(typically in the "Girder-OTP" header).') self.verifyOtp(user, otpToken) elif isinstance(otpToken, six.string_types): - raise AccessException("The user has not enabled one-time passwords.") + raise AccessException( + 'The user has not enabled one-time passwords.' + ) # This has the same behavior as User.canLogin, but returns more # detailed error messages - if user.get("status", "enabled") == "disabled": - return {"exception": "Account is disabled."} + if user.get('status', 'enabled') == 'disabled': + return { 'exception' : 'Account is disabled.' } if self.emailVerificationRequired(user): - return {"exception": "Email verification is required."} + return { 'exception' : 'Email verification is required.' } if self.adminApprovalRequired(user): - return {"exception": "Admin approval required"} + return { 'exception' : 'Admin approval required' } return user @@ -313,23 +263,27 @@ def remove(self, user, progress=None, **kwargs): from girderformindlogger.models.token import Token # Delete all authentication tokens owned by this user - Token().removeWithQuery({"userId": user["_id"]}) + Token().removeWithQuery({'userId': user['_id']}) # Delete all pending group invites for this user - Group().update({"requests": user["_id"]}, {"$pull": {"requests": user["_id"]}}) + Group().update( + {'requests': user['_id']}, + {'$pull': {'requests': user['_id']}} + ) # Delete all of the folders under this user folderModel = Folder() - folders = folderModel.find( - {"parentId": user["_id"], "parentCollection": "user"} - ) + folders = folderModel.find({ + 'parentId': user['_id'], + 'parentCollection': 'user' + }) for folder in folders: folderModel.remove(folder, progress=progress, **kwargs) # Finally, delete the user document itself AccessControlledModel.remove(self, user) if progress: - progress.update(increment=1, message="Deleted user " + user["login"]) + progress.update(increment=1, message='Deleted user ' + user['login']) def getAdmins(self): """ @@ -337,7 +291,7 @@ def getAdmins(self): admins is assumed to be small enough that we will not need to page the results for now. """ - return self.find({"admin": True}) + return self.find({'admin': True}) def search(self, text=None, user=None, limit=0, offset=0, sort=None): """ @@ -360,8 +314,8 @@ def search(self, text=None, user=None, limit=0, offset=0, sort=None): cursor = self.find({}, sort=sort) return self.filterResultsByPermission( - cursor=cursor, user=user, level=AccessType.READ, limit=limit, offset=offset - ) + cursor=cursor, user=user, level=AccessType.READ, limit=limit, + offset=offset) def setUserName(self, user, userName, save=True): """ @@ -371,16 +325,17 @@ def setUserName(self, user, userName, save=True): :param userName: the new userName to be stored """ - oldUserName = user["login"] + oldUserName = user['login'] if len(userName) > 0: - user["login"] = userName + user['login'] = userName else: - raise Exception("username can't be empty") + raise Exception('username can\'t be empty') self.save(user) return oldUserName + def hasPassword(self, user): """ Returns whether or not the given user has a password stored in the @@ -391,7 +346,7 @@ def hasPassword(self, user): :type user: dict :returns: bool """ - return user["salt"] is not None + return user['salt'] is not None def setPassword(self, user, password, save=True): """ @@ -404,56 +359,43 @@ def setPassword(self, user, password, save=True): authenticating the user. """ if password is None: - user["salt"] = None + user['salt'] = None else: cur_config = config.getConfig() # Normally this would go in validate() but password is a special case. - if not re.match(cur_config["users"]["password_regex"], password): - raise ValidationException( - cur_config["users"]["password_description"], "password" - ) + if not re.match(cur_config['users']['password_regex'], password): + raise ValidationException(cur_config['users']['password_description'], 'password') - user["salt"] = self._cryptContext.hash(password) + user['salt'] = self._cryptContext.hash(password) if save: self.save(user) def getEncryptions(self, user, email, password): from girderformindlogger.models.applet import Applet as AppletModel - - accounts = AccountProfile().getAccounts(user["_id"]) + accounts = AccountProfile().getAccounts(user['_id']) applet_ids = [] for account in accounts: - for applet in account.get("applets", {}).get("user", []): + for applet in account.get('applets', {}).get('user', []): applet_ids.append(applet) - applets = [ - AppletModel().load(ObjectId(applet_id), AccessType.READ) - for applet_id in applet_ids - ] + applets = [AppletModel().load(ObjectId(applet_id), AccessType.READ) for applet_id in applet_ids] - privateKey = self.getPrivateKey(user["_id"], email, password) + privateKey = self.getPrivateKey(user['_id'], email, password) keys = {} for applet in applets: - encryption = applet["meta"].get("encryption") + encryption = applet['meta'].get('encryption') if encryption: - publicKey = self.getPublicKey( - privateKey, encryption["appletPrime"], encryption["base"] - ) - aesKey = self.getAESKey( - privateKey, - encryption["appletPublicKey"], - encryption["appletPrime"], - encryption["base"], - ) - - keys[str(applet["_id"])] = { - "userPublicKey": publicKey, - "AESKey": aesKey, + publicKey = self.getPublicKey(privateKey, encryption['appletPrime'], encryption['base']) + aesKey = self.getAESKey(privateKey, encryption['appletPublicKey'], encryption['appletPrime'], encryption['base']) + + keys[str(applet['_id'])] = { + 'userPublicKey': publicKey, + 'AESKey': aesKey } return (privateKey, keys) @@ -470,43 +412,44 @@ def initializeOtp(self, user): """ totp = self._TotpFactory.new() - user["otp"] = {"enabled": False, "totp": totp.to_dict()} + user['otp'] = { + 'enabled': False, + 'totp': totp.to_dict() + } # Use the brand name as the OTP issuer if it's non-default (since that's prettier and more # meaningful for users), but fallback to the site hostname if the brand name isn't set # (to disambiguate otherwise identical "Girder" issuers) # Prevent circular import from girderformindlogger.api.rest import getUrlParts - brandName = Setting().get(SettingKey.BRAND_NAME) defaultBrandName = Setting().getDefault(SettingKey.BRAND_NAME) # OTP URIs ( https://github.com/google/google-authenticator/wiki/Key-Uri-Format ) do not # allow colons, so use only the hostname component - serverHostname = getUrlParts().netloc.partition(":")[0] + serverHostname = getUrlParts().netloc.partition(':')[0] # Normally, the issuer would be set when "self._TotpFactory" is instantiated, but that # happens during model initialization, when there's no current request, so the server # hostname is not known then otpIssuer = brandName if brandName != defaultBrandName else serverHostname - return {"totpUri": totp.to_uri(label=user["login"], issuer=otpIssuer)} + return { + 'totpUri': totp.to_uri(label=user['login'], issuer=otpIssuer) + } def hasOtpEnabled(self, user): - return "otp" in user and user["otp"]["enabled"] + return 'otp' in user and user['otp']['enabled'] def verifyOtp(self, user, otpToken): - lastCounterKey = ( - "girderformindlogger.models.user.%s.otp.totp.counter" % user["_id"] - ) + lastCounterKey = 'girderformindlogger.models.user.%s.otp.totp.counter' % user['_id'] # The last successfully-authenticated key (which is blacklisted from reuse) lastCounter = rateLimitBuffer.get(lastCounterKey) or None try: totpMatch = self._TotpFactory.verify( - otpToken, user["otp"]["totp"], last_counter=lastCounter - ) + otpToken, user['otp']['totp'], last_counter=lastCounter) except TokenError as e: - raise AccessException("One-time password validation failed: %s" % e) + raise AccessException('One-time password validation failed: %s' % e) # The totpMatch.cache_seconds tells us prospectively how long the counter needs to be cached # for, but dogpile.cache expiration times work retrospectively (on "get"), so there's no @@ -514,19 +457,9 @@ def verifyOtp(self, user, otpToken): # "totp.verify" security) rateLimitBuffer.set(lastCounterKey, totpMatch.counter) - def createUser( - self, - login, - password, - displayName="", - email="", - admin=False, - public=False, - currentUser=None, - firstName="", - lastName="", - encryptEmail=False, - ): + def createUser(self, login, password, displayName="", email="", + admin=False, public=False, currentUser=None, + firstName="", lastName="", encryptEmail=False): """ Create a new user with the given information. @@ -539,56 +472,49 @@ def createUser( from girderformindlogger.models.group import Group from girderformindlogger.models.setting import Setting from girderformindlogger.models.account_profile import AccountProfile - - requireApproval = Setting().get(SettingKey.REGISTRATION_POLICY) == "approve" + requireApproval = Setting( + ).get(SettingKey.REGISTRATION_POLICY) == 'approve' email = "" if not email else email login = login.lower().strip() email = email.lower().strip() - if self.findOne( - {"email": email, "email_encrypted": {"$ne": True}} - ) or self.findOne({"email": self.hash(email), "email_encrypted": True}): - raise ValidationException( - "That email is already registered in the system.", - ) + if self.findOne({'email': email, 'email_encrypted': {'$ne': True}}) or self.findOne({'email': self.hash(email), 'email_encrypted': True}): + raise ValidationException('That email is already registered in the system.', ) if admin: requireApproval = False encryptEmail = False user = { - "login": login, - "email": email, - "displayName": displayName - if len(displayName) - else firstName - if firstName is not None - else "", - "firstName": firstName, - "lastName": lastName, - "created": datetime.datetime.utcnow(), - "emailVerified": False, - "status": "pending" if requireApproval else "enabled", - "admin": admin, - "size": 0, - "deviceId": "", - "timezone": 0, - "groups": [], - "groupInvites": [ - {"groupId": gi.get("_id"), "level": 0} - for gi in list(Group().find(query={"queue": email})) - ] - if len(email) - else [], - "email_encrypted": encryptEmail, - "accountName": "", - "displayEmail": email, + 'login': login, + 'email': email, + 'displayName': displayName if len( + displayName + ) else firstName if firstName is not None else "", + 'firstName': firstName, + 'lastName': lastName, + 'created': datetime.datetime.utcnow(), + 'emailVerified': False, + 'status': 'pending' if requireApproval else 'enabled', + 'admin': admin, + 'size': 0, + 'deviceId': '', + 'timezone': 0, + 'groups': [], + 'groupInvites': [ + { + "groupId": gi.get('_id'), + "level": 0 + } for gi in list(Group().find(query={"queue": email})) + ] if len(email) else [], + 'email_encrypted': encryptEmail, + 'accountName': '' } if encryptEmail: if len(email) == 0 or not mail_utils.validateEmailAddress(email): - raise ValidationException("Invalid email address.", "email") + raise ValidationException('Invalid email address.', 'email') - user["email"] = self.hash(user["email"]) + user['email'] = self.hash(user['email']) self.setPassword(user, password, save=False) self.setPublic(user, public, save=False) @@ -597,7 +523,7 @@ def createUser( self.setUserAccess( user, user=currentUser, level=AccessType.WRITE, save=False ) - user["creatorId"] = currentUser["_id"] + user['creatorId'] = currentUser['_id'] user = self.save(user) @@ -606,59 +532,62 @@ def createUser( doc=currentUser, user=user, level=AccessType.READ, save=True ) else: - user["creatorId"] = user["_id"] + user['creatorId'] = user['_id'] user = self.save(user) - verifyEmail = Setting().get(SettingKey.EMAIL_VERIFICATION) != "disabled" + verifyEmail = Setting().get(SettingKey.EMAIL_VERIFICATION) != 'disabled' if verifyEmail: self._sendVerificationEmail(user, email) if requireApproval: self._sendApprovalEmail(user) Group().update( - query={"queue": user["email"]}, - update={"$pull": {"queue": user["email"]}}, - multi=True, + query={"queue": user['email']}, + update={"$pull": {"queue": user['email']}}, + multi=True ) account = AccountProfile().createOwner(user) - user["accountId"] = account["_id"] - self.update({"_id": user["_id"]}, {"$set": {"accountId": user["accountId"]}}) + user['accountId'] = account['_id'] + self.update({'_id': user['_id']}, {'$set': {'accountId': user['accountId']}}) # self.createTemplatesFolder(user) user = self._getGroupInvitesFromProtoUser(user) self._deleteProtoUser(user) - return user + return(user) def createTemplatesFolder(self, user): from girderformindlogger.models.folder import Folder - existing = Folder().findOne( - {"accountId": user["accountId"], "meta.contentType": "templates"} - ) + existing = Folder().findOne({ + 'accountId': user['accountId'], + 'meta.contentType': 'templates' + }) if existing: return existing templatesFolder = Folder().createFolder( parent=user, - parentType="user", - name="templates folder for {} account".format(user["firstName"]), + parentType='user', + name='templates folder for {} account'.format(user['firstName']), creator=user, reuseExisting=True, allowRename=True, public=False, - accountId=user["accountId"], + accountId=user['accountId'] ) - return Folder().setMetadata(templatesFolder, {"contentType": "templates"}) + return Folder().setMetadata(templatesFolder, { + 'contentType': 'templates' + }) def canLogin(self, user): """ Returns True if the user is allowed to login, e.g. email verification is not needed and admin approval is not needed. """ - if user.get("status", "enabled") == "disabled": + if user.get('status', 'enabled') == 'disabled': return False if self.emailVerificationRequired(user): return False @@ -672,11 +601,8 @@ def emailVerificationRequired(self, user): yet verified their email address. """ from girderformindlogger.models.setting import Setting - - return (not user["emailVerified"]) and ( - Setting().get(SettingKey.EMAIL_VERIFICATION) == "required" - or Setting().get(SettingKey.EMAIL_VERIFICATION) == "enabled" - ) + return (not user['emailVerified']) and \ + (Setting().get(SettingKey.EMAIL_VERIFICATION) == 'required' or Setting().get(SettingKey.EMAIL_VERIFICATION) == 'enabled') def adminApprovalRequired(self, user): """ @@ -684,37 +610,44 @@ def adminApprovalRequired(self, user): this user is pending approval. """ from girderformindlogger.models.setting import Setting - - return ( - user.get("status", "enabled") == "pending" - and Setting().get(SettingKey.REGISTRATION_POLICY) == "approve" - ) + return user.get('status', 'enabled') == 'pending' and \ + Setting().get(SettingKey.REGISTRATION_POLICY) == 'approve' def _sendApprovalEmail(self, user): - url = "%s#user/%s" % (mail_utils.getEmailUrlPrefix(), str(user["_id"])) - text = mail_utils.renderTemplate( - "accountApproval.mako", {"user": user, "url": url} - ) - mail_utils.sendMailToAdmins("Girder: Account pending approval", text) + url = '%s#user/%s' % ( + mail_utils.getEmailUrlPrefix(), str(user['_id'])) + text = mail_utils.renderTemplate('accountApproval.mako', { + 'user': user, + 'url': url + }) + mail_utils.sendMailToAdmins( + 'Girder: Account pending approval', + text) def _sendApprovedEmail(self, user, email): - text = mail_utils.renderTemplate( - "accountApproved.mako", - {"user": user, "url": mail_utils.getEmailUrlPrefix()}, - ) - mail_utils.sendMail("Girder: Account approved", text, [email]) + text = mail_utils.renderTemplate('accountApproved.mako', { + 'user': user, + 'url': mail_utils.getEmailUrlPrefix() + }) + mail_utils.sendMail( + 'Girder: Account approved', + text, + [email]) def _sendVerificationEmail(self, user, email): from girderformindlogger.models.token import Token - token = Token().createToken(user, days=1, scope=TokenScope.EMAIL_VERIFICATION) - url = "%s#useraccount/%s/verification/%s" % ( - mail_utils.getEmailUrlPrefix(), - str(user["_id"]), - str(token["_id"]), - ) - text = mail_utils.renderTemplate("emailVerification.mako", {"url": url}) - mail_utils.sendMail("Girder: Email verification", text, [email]) + token = Token().createToken( + user, days=1, scope=TokenScope.EMAIL_VERIFICATION) + url = '%s#useraccount/%s/verification/%s' % ( + mail_utils.getEmailUrlPrefix(), str(user['_id']), str(token['_id'])) + text = mail_utils.renderTemplate('emailVerification.mako', { + 'url': url + }) + mail_utils.sendMail( + 'Girder: Email verification', + text, + [email]) def _grantSelfAccess(self, event): """ @@ -738,22 +671,18 @@ def _addDefaultFolders(self, event): from girderformindlogger.models.folder import Folder from girderformindlogger.models.setting import Setting - if Setting().get(SettingKey.USER_DEFAULT_FOLDERS) == "public_private": + if Setting().get(SettingKey.USER_DEFAULT_FOLDERS) == 'public_private': user = event.info publicFolder = Folder().createFolder( - user, "Public", parentType="user", public=True, creator=user - ) + user, 'Public', parentType='user', public=True, creator=user) privateFolder = Folder().createFolder( - user, "Private", parentType="user", public=False, creator=user - ) + user, 'Private', parentType='user', public=False, creator=user) # Give the user admin access to their own folders Folder().setUserAccess(publicFolder, user, AccessType.ADMIN, save=True) Folder().setUserAccess(privateFolder, user, AccessType.ADMIN, save=True) - def fileList( - self, doc, user=None, path="", includeMetadata=False, subpath=True, data=True - ): + def fileList(self, doc, user=None, path='', includeMetadata=False, subpath=True, data=True): """ This function generates a list of 2-tuples whose first element is the relative path to the file from the user's folders root and whose second @@ -778,21 +707,16 @@ def fileList( from girderformindlogger.models.folder import Folder if subpath: - path = os.path.join(path, doc["login"]) + path = os.path.join(path, doc['login']) folderModel = Folder() # Eagerly evaluate this list, as the MongoDB cursor can time out on long requests - childFolders = list( - folderModel.childFolders( - parentType="user", - parent=doc, - user=user, - fields=["name"] + (["meta"] if includeMetadata else []), - ) - ) + childFolders = list(folderModel.childFolders( + parentType='user', parent=doc, user=user, + fields=['name'] + (['meta'] if includeMetadata else []) + )) for folder in childFolders: for (filepath, file) in folderModel.fileList( - folder, user, path, includeMetadata, subpath=True, data=data - ): + folder, user, path, includeMetadata, subpath=True, data=data): yield (filepath, file) def subtreeCount(self, doc, includeItems=True, user=None, level=None): @@ -811,19 +735,14 @@ def subtreeCount(self, doc, includeItems=True, user=None, level=None): count = 1 folderModel = Folder() - folders = folderModel.findWithPermissions( - {"parentId": doc["_id"], "parentCollection": "user"}, - fields="access", - user=user, - level=level, - ) - - count += sum( - folderModel.subtreeCount( - folder, includeItems=includeItems, user=user, level=level - ) - for folder in folders - ) + folders = folderModel.findWithPermissions({ + 'parentId': doc['_id'], + 'parentCollection': 'user' + }, fields='access', user=user, level=level) + + count += sum(folderModel.subtreeCount( + folder, includeItems=includeItems, user=user, level=level) + for folder in folders) return count def countFolders(self, user, filterUser=None, level=None): @@ -841,15 +760,13 @@ def countFolders(self, user, filterUser=None, level=None): """ from girderformindlogger.models.folder import Folder - fields = () if level is None else ("access", "public") + fields = () if level is None else ('access', 'public') folderModel = Folder() - folders = folderModel.findWithPermissions( - {"parentId": user["_id"], "parentCollection": "user"}, - fields=fields, - user=filterUser, - level=level, - ) + folders = folderModel.findWithPermissions({ + 'parentId': user['_id'], + 'parentCollection': 'user' + }, fields=fields, user=filterUser, level=level) return folders.count() @@ -866,53 +783,67 @@ def updateSize(self, doc): size = 0 fixes = 0 folderModel = Folder() - folders = folderModel.find({"parentId": doc["_id"], "parentCollection": "user"}) + folders = folderModel.find({ + 'parentId': doc['_id'], + 'parentCollection': 'user' + }) for folder in folders: # fix folder size if needed _, f = folderModel.updateSize(folder) fixes += f # get total recursive folder size - folder = folderModel.load(folder["_id"], force=True) + folder = folderModel.load(folder['_id'], force=True) size += folderModel.getSizeRecursive(folder) # fix value if incorrect - if size != doc.get("size"): - self.update({"_id": doc["_id"]}, update={"$set": {"size": size}}) + if size != doc.get('size'): + self.update({'_id': doc['_id']}, update={'$set': {'size': size}}) fixes += 1 return size, fixes def _getGroupInvitesFromProtoUser(self, doc): - """ """ + """ + + """ from girderformindlogger.models.protoUser import ProtoUser # Ensure unique emails - q = {"email": doc["email"]} - if "_id" in doc: - q["_id"] = {"$ne": doc["_id"]} + q = {'email': doc['email']} + if '_id' in doc: + q['_id'] = {'$ne': doc['_id']} existing = ProtoUser().findOne(q) if existing is not None: - doc["groupInvites"] = existing["groupInvites"] - return doc + doc['groupInvites'] = existing['groupInvites'] + return(doc) def _deleteProtoUser(self, doc): - """ """ + """ + + """ from girderformindlogger.models.protoUser import ProtoUser # Ensure unique emails - q = {"email": doc["email"]} - if "_id" in doc: - q["_id"] = {"$ne": doc["_id"]} + q = {'email': doc['email']} + if '_id' in doc: + q['_id'] = {'$ne': doc['_id']} existing = ProtoUser().findOne(q) if existing is not None: ProtoUser().remove(existing) def _verify_password(self, password, user): # Verify password - if not self._cryptContext.verify(password, user["salt"]): - raise AccessException("Login failed.") + if not self._cryptContext.verify(password, user['salt']): + raise AccessException('Login failed.') else: - return True + return(True) def get_users_by_ids(self, user_ids): return self.find( - query={"_id": {"$in": user_ids}}, fields=["timezone", "deviceId"] + query={ + '_id': { + '$in': user_ids + } + }, + fields=[ + 'timezone', 'deviceId' + ] ) From 638a785dd8a1d74c0acb8de266d193af4c5aa045 Mon Sep 17 00:00:00 2001 From: Damirkhon Aloev Date: Tue, 6 Dec 2022 14:01:18 +0500 Subject: [PATCH 3/3] adding displayEmail field with AES_key encryption --- girderformindlogger/models/user.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/girderformindlogger/models/user.py b/girderformindlogger/models/user.py index 0efa11dbd..a64816d73 100644 --- a/girderformindlogger/models/user.py +++ b/girderformindlogger/models/user.py @@ -38,7 +38,7 @@ def initialize(self): }, language='none') self.exposeFields(level=AccessType.READ, fields=( '_id', 'login', 'public', 'displayName', 'firstName', 'lastName', - 'admin', 'email', 'created')) + 'admin', 'email', 'created', 'displayEmail')) self.exposeFields(level=AccessType.ADMIN, fields=( 'size', 'status', 'emailVerified', 'creatorId')) @@ -55,7 +55,8 @@ def initialize(self): self.initAES([ ('firstName', 64), ('lastName', 64), - ('displayName', 64) + ('displayName', 64), + ('displayEmail', 64) ]) events.bind('model.user.save.created', @@ -508,7 +509,9 @@ def createUser(self, login, password, displayName="", email="", } for gi in list(Group().find(query={"queue": email})) ] if len(email) else [], 'email_encrypted': encryptEmail, - 'accountName': '' + 'accountName': '', + 'displayEmail': email, + } if encryptEmail: if len(email) == 0 or not mail_utils.validateEmailAddress(email): @@ -847,3 +850,4 @@ def get_users_by_ids(self, user_ids): 'timezone', 'deviceId' ] ) +