def __init__(self, options=None): if options is None: options = {} params = dict(self.defaults, **options) self.options = Bunch() self.options.merge(params) self.configure()
def __init__(self, options=None): if options is None: options = {} params = dict(self.defaults, **options) self.validate_store_option(params) self.options = Bunch() self.options.merge(params)
def get_account(self, userkey): """ Gets the account with the given key. :param userkey: key of the user (e.g. email or username) """ data = self.options.store.get_account(userkey) if data is None: return None result = Bunch() result.merge(data) return result
def __init__(self): store = self.get_membership_store() options = self.get_options() require_params(store=store) if options is None: options = {} params = dict(self.defaults, **options) self.validate_store(store) self.store = store self.options = Bunch() self.options.merge(params) self.principal_type = self.get_principal_type()
def get_account_by_id(self, account_id): """ Gets the account details by id :param id: account id :return: account """ data = self.options.store.get_account_by_id(account_id) if data is None: return None del data["salt"] del data["hash"] result = Bunch() result.merge(data) return result
def __init__(self, options = None): if options is None: options = {} params = dict(self.defaults, **options) self.options = Bunch() self.options.merge(params) self.configure()
def __init__(self, options = None): if options is None: options = {} params = dict(self.defaults, **options) self.validate_store_option(params) self.options = Bunch() self.options.merge(params)
class MembershipStore(MongoStore): """ Represents a Sessions and Accounts Storage manager for MongoDB utilized to manage Membership for an application. It can be used to handle global authentication; or per-area authentication. Contains data access logic for accounts and sessions. """ defaults = { "accounts_collection": "accounts", "sessions_collection": "sessions", "login_attempts_collection": "login_attempts", "user_key_field": "email" } def __init__(self, options = None): if options is None: options = {} params = dict(self.defaults, **options) self.options = Bunch() self.options.merge(params) self.configure() def configure(self): """ Configure the db, creating the needed collections and theirs indexes. :return: self """ opt = self.options db_collections = db.collection_names(include_system_collections=False) accounts_collection = opt.accounts_collection sessions_collection = opt.sessions_collection login_attempts_collection = opt.login_attempts_collection user_key_field = opt.user_key_field if accounts_collection not in db_collections: # create the account collection accounts = db.create_collection(accounts_collection) accounts.create_index([(user_key_field, ASCENDING)]) if sessions_collection not in db_collections: # create the sessions collection sessions = db.create_collection(sessions_collection) sessions.create_index([(user_key_field, ASCENDING)]) sessions.create_index([("guid", ASCENDING)]) if login_attempts_collection and login_attempts_collection not in db_collections: # create the login attempts collection login_attempts = db.create_collection(login_attempts_collection) login_attempts.create_index([(user_key_field, ASCENDING)]) return self def get_account_condition(self, userkey): """ Returns account search condition. :param userkey: user key :return: condition for a mongodb search """ a = { self.options.user_key_field: userkey } return a def get_account(self, userkey): """ Gets the account data associated with the user with the given key. :param userkey: user key :return: """ collection = db[self.options.accounts_collection] condition = self.get_account_condition(userkey) data = collection.find_one(condition) return self.normalize_id(data) def get_account_by_id(self, account_id): """ Gets the account data associated with the user with the given id. :param id: user id :return: """ collection = db[self.options.accounts_collection] condition = { "_id": ObjectId(account_id) } data = collection.find_one(condition) return self.normalize_id(data) def get_accounts(self, options): """ Gets a list of all application accounts. """ collection = db[self.options.accounts_collection] return self.get_catalog_page(collection, options) def get_sessions(self, options): """ Gets a list of current sessions. """ collection = db[self.options.sessions_collection] return self.get_catalog_page(collection, options) def get_session(self, sessionkey): """ Gets the session with the given key. :param sessionkey: session guid :return: """ collection = db[self.options.sessions_collection] data = collection.find_one({ "guid": sessionkey }) if not data: return None #set user key data["userkey"] = data[self.options.user_key_field] return self.normalize_id(data) def create_session(self, userkey, expiration, client_ip, client_data): """ Creates a session in the database. :param userkey: key of the user for whom we are initializing the session :param expiration: expiration datetime of the session :param client_ip: ip of the client for whom this function is invoked :param client_data: client speficif information (e.g. browser navigator) :return: """ collection = db[self.options.sessions_collection] data = { "guid": str(uuid.uuid1()), self.options.user_key_field: userkey, "anonymous": userkey is None or userkey == False, "expiration": expiration, "client_ip": client_ip, "client_data": client_data, "timestamp": datetime.now() } result = collection.insert_one(data) session_id = result.inserted_id data = collection.find_one({ "_id": session_id }) return self.normalize_id(data) def create_account(self, userkey, hashedpassword, salt, data, roles = None): """ Creates a new account :param userkey: user key (e.g. email or username) :param hashedpassword: hashed password :param salt: salt used to hash the account password :param data: extra account data """ collection = db[self.options.accounts_collection] account_data = { self.options.user_key_field: userkey, "hash": hashedpassword, "salt": salt, "data": data, "roles": roles, "timestamp": datetime.now() } result = collection.insert_one(account_data) return { "id": str(result.inserted_id) } def save_session_data(self, sessionkey, data): """ Stores data for the session with the given key. :param sessionkey: session guid. :param data: session data """ collection = db[self.options.sessions_collection] condition = { "guid": sessionkey } update = { "data": data } collection.update_one(condition, update) def get_session_data(self, sessionkey): """ Gets the data associated with the session with the given key. :param sessionkey: session guid. """ collection = db[self.options.sessions_collection] condition = { "guid": sessionkey } session = collection.find_one(condition) return session["data"] def destroy_session(self, sessionkey): collection = db[self.options.sessions_collection] condition = { "guid": sessionkey } collection.delete_one(condition) def get_failed_login_attempts(self, userkey, start, end): """ Gets the number of failed login attempts for a user with a given key, in the last minutes. :param userkey: key of the user for whom we are initializing the session :param start: start datetime to check for login attempts :param end: end datetime to check for login attempts :return: """ condition = { self.options.user_key_field: userkey, "timestamp": { "$gte": start, "$lt": end } } collection = db[self.options.login_attempts_collection] attempts = collection.find(condition) return attempts.count() def save_login_attempt(self, userkey, client_ip, time): """ Stores a failed login attempt in database :param userkey: key of the user for whom the login attempt must be stored :param client_ip: ip of the client for which the method is invoked :param time: timestamp of the login attempt :return: """ data = { self.options.user_key_field: userkey, "client_ip": client_ip, "timestamp": time } collection = db[self.options.login_attempts_collection] collection.insert_one(data) def update_account(self, userkey, data): """ Updates the account associated to the user with the given key. :param userkey: key of the user whose account must be deleted :param data: new account data """ collection = db[self.options.accounts_collection] condition = self.get_account_condition(userkey) collection.update_one(condition, { "$set": data }) def delete_account(self, userkey): """ Deletes the account associated to the user with the given key. :param userkey: key of the user whose account must be deleted """ collection = db[self.options.accounts_collection] condition = self.get_account_condition(userkey) collection.delete_one(condition)
class ReportsStore(MongoStore): """ Represents a storage manager for MongoDB based application reports. """ defaults = { "messages_collection": "app_messages", "exceptions_collection": "app_exceptions" } def __init__(self, options = None): if options is None: options = {} params = dict(self.defaults, **options) self.options = Bunch() self.options.merge(params) self.configure() def configure(self): """ Configure the db, creating the needed collections and theirs indexes. :return: self """ opt = self.options db_collections = db.collection_names(include_system_collections=False) messages_collection = opt.messages_collection exceptions_collection = opt.exceptions_collection if messages_collection not in db_collections: # create the messages collection messages = db.create_collection(messages_collection) if exceptions_collection not in db_collections: # create the sessions collection exceptions = db.create_collection(exceptions_collection) return self def store_message(self, message, time, kind = "Normal"): """ Stores an application message in database. :param message: message to store :param time: timestamp :param kind: the kind of message (e.g. Normal, Warning, etc.) :return: message data """ collection = db[self.options.messages_collection] data = { "message": message, "timestamp": time, "kind": kind } result = collection.insert_one(data) message_id = result.inserted_id data = collection.find_one({ "_id": message_id }) return self.normalize_id(data) def store_exception(self, message, time, typename, callstack): """ Stores an application exception in database. :param ex: exception to store :param time: timestamp :param callstack: exception callstack :return: data stored in db """ collection = db[self.options.exceptions_collection] data = { "message": message, "timestamp": time, "type": typename, "callstack": callstack } result = collection.insert_one(data) ex_id = result.inserted_id data = collection.find_one({ "_id": ex_id }) return self.normalize_id(data) def get_messages(self, options): """ Gets a paginated subset of application messages. """ collection = db[self.options.messages_collection] data = self.get_catalog_page(collection, options) return data def get_exceptions(self, options): """ Gets a paginated subset of application exceptions. """ collection = db[self.options.exceptions_collection] data = self.get_catalog_page(collection, options) return data
class MembershipProvider: """ Provides business logic to provide user authentication. Generic MembershipProvider utilized to manage Membership for an application. It can be used to handle global authentication; or per-area authentication. Contains business logic for Login, Logout, ChangePassword. """ defaults = { "store": None, "short_time_expiration": 1e3 * 60 * 20, "long_time_expiration": 1e3 * 60 * 60 * 24 * 365, "failed_login_attempts_limit": 4, "minutes_limit": 15, "requires_account_confirmation": False } def __init__(self, options = None): if options is None: options = {} params = dict(self.defaults, **options) self.validate_store_option(params) self.options = Bunch() self.options.merge(params) @staticmethod def get_hash(password, salt): """ Returns an hashed version of password, created using the given salt :param salt: salt to use to hash a password :return: hashed version of password """ key = (password + salt).encode("utf-8") return hashlib.sha224(key).hexdigest() @staticmethod def get_new_salt(): """ Returns a new salt to be used to hash password :return: {String} new salt to be used to hash passwords """ ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" return ''.join(random.choice(ALPHABET) for i in range(16)) @staticmethod def validate_store_option(params): """ Validates the store option passed when instantiating a MembershipProvider. :param params: constructor options """ if params["store"] is None: raise Exception("Missing `store` option") req = ["get_account", "get_accounts", "get_session", "create_account", "update_account", "create_session", "destroy_session", "save_session_data", "get_session_data", "get_failed_login_attempts", "save_login_attempt"] for name in req: if not hasattr(params["store"], name): raise Exception("The given store does not implement `" + name + "` member") def get_account(self, userkey): """ Gets the account with the given key. :param userkey: key of the user (e.g. email or username) """ data = self.options.store.get_account(userkey) if data is None: return None result = Bunch() result.merge(data) return result def get_account_by_id(self, account_id): """ Gets the account details by id :param id: account id :return: account """ data = self.options.store.get_account_by_id(account_id) if data is None: return None del data["salt"] del data["hash"] result = Bunch() result.merge(data) return result def get_accounts(self, options): """ Gets the list of all application accounts. """ # define searchable properties options["search_properties"] = ["email", "roles"] data = self.options.store.get_accounts(options) # NB !!! # Salt and hashed password must be kept private in this case. for item in data.subset: self.prepare_account_data(item) return data def get_sessions(self, options): """ Gets the list of current user sessions. """ # define searchable properties options["search_properties"] = ["email", "client_data.user_agent", "client_ip"] data = self.options.store.get_sessions(options) for o in data.subset: if o["client_data"]: o["user_agent"] = o["client_data"]["user_agent"] else: o["user_agent"] = "" del o["client_data"] return data def prepare_account_data(self, account): """ Prepares account data, to share it outside of bll. Salt and hashed password must be never get out of bll. """ del account["salt"] del account["hash"] if "roles" not in account: account["roles"] = [] return account def create_account(self, userkey, password, data = None, roles = None): """ Creates a new user account in db :param userkey: key of the user (e.g. email or username) :param password: account clear password (e.g. user defined password) :param data: dict, optional account data :return: """ # verify that an account with the same key doesn't exist already account_data = self.options.store.get_account(userkey) if account_data is not None: return False, "AccountAlreadyExisting" if data is None: data = {} if roles is None: roles = [] salt = self.get_new_salt() hashedpassword = self.get_hash(password, salt) now = datetime.datetime.now() if self.options.requires_account_confirmation: # set a confirmation token inside the account data data["confirmation_token"] = uuid.uuid1() return True, self.options.store.create_account(userkey, hashedpassword, salt, data, roles) def update_password(self, userkey, password): """ Updates the password for the account with the given key. :param userkey: key of the user (e.g. email or username) :param password: account clear password (e.g. user defined password) :return: """ account_data = self.options.store.get_account(userkey) if account_data is None: return False, "Account not found" salt = account_data["salt"] hashedpassword = self.get_hash(password, salt) now = datetime.datetime.now() self.options.store.update_account(userkey, { "hash": hashedpassword }) return True, "" def delete_account(self, userkey): """ Deletes the account with the given userkey :param userkey: the user key (email address or username) :return: success, error """ account = self.options.store.get_account(userkey) if account is None: return False, "AccountNotFound" self.options.store.delete_account(userkey) return True def confirm_account(self, userkey): """ Confirms the account with the given userkey. :param userkey: the user key (email address or username) :return: success, error """ return self.update_account(userkey, { "confirmed": True }) def ban_account(self, userkey): """ Bans the account with the given userkey. :param userkey: the user key (email address or username) :return: success, error """ return self.update_account(userkey, { "banned": True }) def update_account(self, userkey, data): """ Updates the account with the given key; setting the data. :param userkey: the user key (email address or username) :param data: account data to update :return: self """ account = self.options.store.get_account(userkey) if account is None: return False, "AccountNotFound" self.options.store.update_account(userkey, data) return True, None def try_login(self, userkey, password, remember, client_ip, client_data = None, options = None): """ Tries to perform a login for the user with the given key (e.g. email or username); password; :param userkey: the user key (email address or username) :param password: :param remember: whether to have a longer expiration time or not :param client_ip: ip of the client for which the function has been called :param client_data: optional client data :param options: extra options :return: """ # get account data account_data = self.get_account(userkey) if account_data is None: return False, "AccountNotFound" login_attempts = self.get_failed_login_attempts(userkey) tooManyAttempts = self.options.failed_login_attempts_limit <= login_attempts if tooManyAttempts: return False, "TooManyAttempts" # error: too many attempts in the last minutes, for this user check_password = options is None or options["automatic_no_password"] is None if check_password: # generate hash of given password, appending salt hsh = self.get_hash(password, account_data.salt) if account_data.hash != hsh: # the key exists, but the password is wrong self.report_login_attempt(userkey, client_ip) # exit return False, "WrongPassword" # check if the account is confirmed if self.options.requires_account_confirmation and not account_data.confirmed: return False, "RequireConfirmation" # check if the account was banned if hasattr(account_data, "banned") and account_data.banned == True: return False, "BannedAccount" # get session expiration expiration = self.get_new_expiration(remember) # save session session = self.options.store.create_session(userkey, expiration, client_ip, client_data) del account_data.salt del account_data.hash return True, { "principal": Principal(account_data.id, account_data, session, True), "session": Session.from_dict(session) } def report_login_attempt(self, userkey, client_ip): """ Reports a login attempt. :param userkey: the user key (email address or username) :param client_ip: ip of the client """ now = datetime.datetime.now() self.options.store.save_login_attempt(userkey, client_ip, now) return self def change_password(self, userkey, password_reset_key, new_password): pass def get_failed_login_attempts(self, userkey): """ Gets the number of failed login attempts for a userkey in the amount of minutes defined by MinutesLimit option. :param userkey: the user key (email address or username) """ now = datetime.datetime.now() ms = self.options.minutes_limit * 60 * 1e3 start = now - datetime.timedelta(milliseconds=ms) count = self.options.store.get_failed_login_attempts(userkey, start, now) return count def try_login_by_session_key(self, sessionkey): """ Tries to perform login by user session key. :param sessionkey: :return: boolean, session, account """ # returns bool, principal if sessionkey is None: return False, None session = self.options.store.get_session(sessionkey) if session is None: return False, None # convert into a class session = Session.from_dict(session) now = datetime.datetime.now() if session.expiration < now: return False, None if session.anonymous: return True, { "principal": Principal(None, None, session, False), "session": session } # get account data account = self.options.store.get_account(session.userkey) if account is None: return False, None # return session and account data return True, { "principal": Principal(account["id"], account, session, True), "session": session } def initialize_anonymous_session(self, client_ip, client_data): """ Initializes a session for an anonymous user. :param client_ip: :param navigator: :return: """ expiration = self.get_new_expiration(True) userkey = None # save session session = self.options.store.create_session(userkey, expiration, client_ip, client_data) return { "principal": Principal(None, None, session, False), "session": Session.from_dict(session) } def get_new_expiration(self, remember = None): """ Returns the expiration for a new session, based on the provider settings and if the user wants to be remembered. for longer or not. :param remember: boolean :return: datetime """ if remember is None: remember = False now = datetime.datetime.now() ms = self.options.long_time_expiration if remember else self.options.short_time_expiration expiration = now + datetime.timedelta(milliseconds=ms) return expiration def save_session_data(self, sessionkey, data): """ Stores session data in the database. :param sessionkey: the key of the session :param data: dictionary of data to store in database :return: """ self.options.store.save_session_data(sessionkey, data) return self def get_session_data(self, sessionkey): """ Gets the data associated with the given session, in database. :param sessionkey: the key of the session :return: dict """ return self.options.get_session_data(sessionkey) def destroy_session(self, sessionkey): """ Destroys the session with the given key. :param sessionkey: key of the session to destroy. :return: self """ self.options.store.destroy_session(sessionkey) return self @staticmethod def validate_password(password_one, password_two): """ Validates the two given passwords. :param password_one: first password written by user. :param password_two: password confirmation. :return: success, error """ if not password_one or not password_two: return False, "missing password" if password_one != password_two: return False, "password mismatch" v = password_two rx = { "enforce": "^(?=.*\d)(?=.*[a-zA-Z\.\,\;\@\$\!\?\#\*\-\_\%\&]).{9,}$" } if not re.match(rx["enforce"], v): return False, "password too weak" return True, None
class MembershipProvider: """ Provides business logic to provide user authentication. Generic MembershipProvider utilized to manage Membership for an application. It can be used to handle global authentication; or per-area authentication. Contains business logic for Login, Logout, ChangePassword. """ defaults = { "store": None, "short_time_expiration": 1e3 * 60 * 20, "long_time_expiration": 1e3 * 60 * 60 * 24 * 365, "failed_login_attempts_limit": 4, "minutes_limit": 15, "requires_account_confirmation": False } def __init__(self, options=None): if options is None: options = {} params = dict(self.defaults, **options) self.validate_store_option(params) self.options = Bunch() self.options.merge(params) @staticmethod def get_hash(password, salt): """ Returns an hashed version of password, created using the given salt :param salt: salt to use to hash a password :return: hashed version of password """ key = (password + salt).encode("utf-8") return hashlib.sha224(key).hexdigest() @staticmethod def get_new_salt(): """ Returns a new salt to be used to hash password :return: {String} new salt to be used to hash passwords """ ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" return ''.join(random.choice(ALPHABET) for i in range(16)) @staticmethod def validate_store_option(params): """ Validates the store option passed when instantiating a MembershipProvider. :param params: constructor options """ if params["store"] is None: raise Exception("Missing `store` option") req = [ "get_account", "get_accounts", "get_session", "create_account", "update_account", "create_session", "destroy_session", "save_session_data", "get_session_data", "get_failed_login_attempts", "save_login_attempt" ] for name in req: if not hasattr(params["store"], name): raise Exception("The given store does not implement `" + name + "` member") def get_account(self, userkey): """ Gets the account with the given key. :param userkey: key of the user (e.g. email or username) """ data = self.options.store.get_account(userkey) if data is None: return None result = Bunch() result.merge(data) return result def get_account_by_id(self, account_id): """ Gets the account details by id :param id: account id :return: account """ data = self.options.store.get_account_by_id(account_id) if data is None: return None del data["salt"] del data["hash"] result = Bunch() result.merge(data) return result def get_accounts(self, options): """ Gets the list of all application accounts. """ # define searchable properties options["search_properties"] = ["email", "roles"] data = self.options.store.get_accounts(options) # NB !!! # Salt and hashed password must be kept private in this case. for item in data.subset: self.prepare_account_data(item) return data def get_sessions(self, options): """ Gets the list of current user sessions. """ # define searchable properties options["search_properties"] = [ "email", "client_data.user_agent", "client_ip" ] data = self.options.store.get_sessions(options) for o in data.subset: if o["client_data"]: o["user_agent"] = o["client_data"]["user_agent"] else: o["user_agent"] = "" del o["client_data"] return data def prepare_account_data(self, account): """ Prepares account data, to share it outside of bll. Salt and hashed password must be never get out of bll. """ del account["salt"] del account["hash"] if "roles" not in account: account["roles"] = [] return account def create_account(self, userkey, password, data=None, roles=None): """ Creates a new user account in db :param userkey: key of the user (e.g. email or username) :param password: account clear password (e.g. user defined password) :param data: dict, optional account data :return: """ # verify that an account with the same key doesn't exist already account_data = self.options.store.get_account(userkey) if account_data is not None: return False, "AccountAlreadyExisting" if data is None: data = {} if roles is None: roles = [] salt = self.get_new_salt() hashedpassword = self.get_hash(password, salt) now = datetime.datetime.now() if self.options.requires_account_confirmation: # set a confirmation token inside the account data data["confirmation_token"] = uuid.uuid1() return True, self.options.store.create_account(userkey, hashedpassword, salt, data, roles) def update_password(self, userkey, password): """ Updates the password for the account with the given key. :param userkey: key of the user (e.g. email or username) :param password: account clear password (e.g. user defined password) :return: """ account_data = self.options.store.get_account(userkey) if account_data is None: return False, "Account not found" salt = account_data["salt"] hashedpassword = self.get_hash(password, salt) now = datetime.datetime.now() self.options.store.update_account(userkey, {"hash": hashedpassword}) return True, "" def delete_account(self, userkey): """ Deletes the account with the given userkey :param userkey: the user key (email address or username) :return: success, error """ account = self.options.store.get_account(userkey) if account is None: return False, "AccountNotFound" self.options.store.delete_account(userkey) return True def confirm_account(self, userkey): """ Confirms the account with the given userkey. :param userkey: the user key (email address or username) :return: success, error """ return self.update_account(userkey, {"confirmed": True}) def ban_account(self, userkey): """ Bans the account with the given userkey. :param userkey: the user key (email address or username) :return: success, error """ return self.update_account(userkey, {"banned": True}) def update_account(self, userkey, data): """ Updates the account with the given key; setting the data. :param userkey: the user key (email address or username) :param data: account data to update :return: self """ account = self.options.store.get_account(userkey) if account is None: return False, "AccountNotFound" self.options.store.update_account(userkey, data) return True, None def try_login(self, userkey, password, remember, client_ip, client_data=None, options=None): """ Tries to perform a login for the user with the given key (e.g. email or username); password; :param userkey: the user key (email address or username) :param password: :param remember: whether to have a longer expiration time or not :param client_ip: ip of the client for which the function has been called :param client_data: optional client data :param options: extra options :return: """ # get account data account_data = self.get_account(userkey) if account_data is None: return False, "AccountNotFound" login_attempts = self.get_failed_login_attempts(userkey) tooManyAttempts = self.options.failed_login_attempts_limit <= login_attempts if tooManyAttempts: return False, "TooManyAttempts" # error: too many attempts in the last minutes, for this user check_password = options is None or options[ "automatic_no_password"] is None if check_password: # generate hash of given password, appending salt hsh = self.get_hash(password, account_data.salt) if account_data.hash != hsh: # the key exists, but the password is wrong self.report_login_attempt(userkey, client_ip) # exit return False, "WrongPassword" # check if the account is confirmed if self.options.requires_account_confirmation and not account_data.confirmed: return False, "RequireConfirmation" # check if the account was banned if hasattr(account_data, "banned") and account_data.banned == True: return False, "BannedAccount" # get session expiration expiration = self.get_new_expiration(remember) # save session session = self.options.store.create_session(userkey, expiration, client_ip, client_data) del account_data.salt del account_data.hash return True, { "principal": Principal(account_data.id, account_data, session, True), "session": Session.from_dict(session) } def report_login_attempt(self, userkey, client_ip): """ Reports a login attempt. :param userkey: the user key (email address or username) :param client_ip: ip of the client """ now = datetime.datetime.now() self.options.store.save_login_attempt(userkey, client_ip, now) return self def change_password(self, userkey, password_reset_key, new_password): pass def get_failed_login_attempts(self, userkey): """ Gets the number of failed login attempts for a userkey in the amount of minutes defined by MinutesLimit option. :param userkey: the user key (email address or username) """ now = datetime.datetime.now() ms = self.options.minutes_limit * 60 * 1e3 start = now - datetime.timedelta(milliseconds=ms) count = self.options.store.get_failed_login_attempts( userkey, start, now) return count def try_login_by_session_key(self, sessionkey): """ Tries to perform login by user session key. :param sessionkey: :return: boolean, session, account """ # returns bool, principal if sessionkey is None: return False, None session = self.options.store.get_session(sessionkey) if session is None: return False, None # convert into a class session = Session.from_dict(session) now = datetime.datetime.now() if session.expiration < now: return False, None if session.anonymous: return True, { "principal": Principal(None, None, session, False), "session": session } # get account data account = self.options.store.get_account(session.userkey) if account is None: return False, None # return session and account data return True, { "principal": Principal(account["id"], account, session, True), "session": session } def initialize_anonymous_session(self, client_ip, client_data): """ Initializes a session for an anonymous user. :param client_ip: :param navigator: :return: """ expiration = self.get_new_expiration(True) userkey = None # save session session = self.options.store.create_session(userkey, expiration, client_ip, client_data) return { "principal": Principal(None, None, session, False), "session": Session.from_dict(session) } def get_new_expiration(self, remember=None): """ Returns the expiration for a new session, based on the provider settings and if the user wants to be remembered. for longer or not. :param remember: boolean :return: datetime """ if remember is None: remember = False now = datetime.datetime.now() ms = self.options.long_time_expiration if remember else self.options.short_time_expiration expiration = now + datetime.timedelta(milliseconds=ms) return expiration def save_session_data(self, sessionkey, data): """ Stores session data in the database. :param sessionkey: the key of the session :param data: dictionary of data to store in database :return: """ self.options.store.save_session_data(sessionkey, data) return self def get_session_data(self, sessionkey): """ Gets the data associated with the given session, in database. :param sessionkey: the key of the session :return: dict """ return self.options.get_session_data(sessionkey) def destroy_session(self, sessionkey): """ Destroys the session with the given key. :param sessionkey: key of the session to destroy. :return: self """ self.options.store.destroy_session(sessionkey) return self @staticmethod def validate_password(password_one, password_two): """ Validates the two given passwords. :param password_one: first password written by user. :param password_two: password confirmation. :return: success, error """ if not password_one or not password_two: return False, "missing password" if password_one != password_two: return False, "password mismatch" v = password_two rx = { "enforce": "^(?=.*\d)(?=.*[a-zA-Z\.\,\;\@\$\!\?\#\*\-\_\%\&]).{9,}$" } if not re.match(rx["enforce"], v): return False, "password too weak" return True, None
class MembershipProvider: """ Provides business logic to provide user authentication. It can be used to handle global authentication; or per-area authentication. Contains business logic for Login, Logout, ChangePassword. """ defaults = { "host": None, # the host used when generating emails "short_time_expiration": 1e3 * 60 * 20, "long_time_expiration": 1e3 * 60 * 60 * 24 * 365, "failed_login_attempts_limit": 4, "minutes_limit": 15, "requires_account_confirmation": False } def __init__(self): store = self.get_membership_store() options = self.get_options() require_params(store=store) if options is None: options = {} params = dict(self.defaults, **options) self.validate_store(store) self.store = store self.options = Bunch() self.options.merge(params) self.principal_type = self.get_principal_type() def get_membership_store(self): """ Returns the membership store used by this membership provider. This method must be implemented in subclasses. """ name = type(self).__name__ raise NotImplementedError( "{} does not implement 'get_membership_store'.".format(name)) def get_options(self): """ Returns options to override the default parameters. """ return {} def get_principal_type(self): """ Returns the membership store used by this membership provider. This method can be implemented to set specific Principal types by implementation. """ return Principal @staticmethod def get_hash(password, salt): """ Returns an hashed version of password, created using the given salt :param password: original password, to obfuscate with the given salt :param salt: salt to use to hash a password :return: hashed version of password """ key = (password + salt).encode("utf-8") return hashlib.sha224(key).hexdigest() @staticmethod def get_new_salt(): """ Returns a new salt to be used to hash password :return: {String} new salt to be used to hash passwords """ alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" return ''.join(random.choice(alphabet) for _ in range(16)) @staticmethod def validate_store(store): """ Validates the store option passed when instantiating a MembershipProvider. :param store: store """ # TODO: use abstract class!? req = [ "get_account", "get_accounts", "get_session", "get_session_by_guid", "create_account", "update_account", "create_session", "destroy_session", "save_session_data", "get_session_data", "get_failed_login_attempts", "save_login_attempt" ] for name in req: if not hasattr(store, name): raise Exception("The given store does not implement `" + name + "` member") def get_account(self, userkey): """ Gets the account with the given key. :param userkey: key of the user (e.g. email or username) """ data = self.store.get_account(userkey) if data is None: return None result = Bunch() result.merge(data) return result def get_account_by_id(self, account_id): """ Gets the account details by id :param account_id: account id :return: account """ data = self.store.get_account_by_id(account_id) if data is None: return None del data["salt"] del data["hash"] result = Bunch() result.merge(data) return result def get_accounts(self, options): """ Gets the list of all application accounts. """ # define searchable properties options["search_properties"] = ["email", "roles"] data = self.store.get_accounts(options) # NB !!! # Salt and hashed password must be kept private in this case. for item in data.subset: self.prepare_account_data(item) return data def get_sessions(self, options): """ Gets the list of current user sessions. """ # define searchable properties options["search_properties"] = [ "email", "client_data.user_agent", "client_ip" ] data = self.store.get_sessions(options) for o in data.subset: if o["client_data"]: o["user_agent"] = o["client_data"]["user_agent"] else: o["user_agent"] = "" del o["client_data"] return data def prepare_account_data(self, account): """ Prepares account data, to share it outside of bll. Salt and hashed password must be never get out of bll. """ del account["salt"] del account["hash"] if "roles" not in account: account["roles"] = [] return account def validate_userkey(self, userkey): if userkey is None or re.match("^\s*$", userkey): return False return True def get_account_defaults(self): return {} def create_account(self, userkey, password, data=None, roles=None, lang="en"): """ Creates a new user account. :param userkey: key of the user (e.g. email or username) :param password: account clear password (e.g. user defined password) :param data: dict, optional account data :return: """ if not self.validate_userkey(userkey) or not self.validate_password( password): return False, "InvalidParameter" # verify that an account with the same key doesn't exist already account_data = self.store.get_account(userkey) if account_data is not None: return False, "AccountAlreadyExisting" if data is None: data = {} if roles is None: roles = [] salt = self.get_new_salt() hashedpassword = self.get_hash(password, salt) data = dict(self.get_account_defaults(), **data) if self.options.requires_account_confirmation: # set a confirmation token inside the account data confirmation_token = str(uuid.uuid1()) data["confirmation_token"] = confirmation_token account_data = self.store.create_account(userkey, hashedpassword, salt, data, roles) # TODO: if desired, implement send email logic return True, account_data def is_password_correct(self, account_id, password): require_params(account_id=account_id, password=password) if not self.validate_password(password): return False account_data = self.store.get_account_by_id(account_id) if account_data is None: raise ValueError("AccountNotFound") hsh = self.get_hash(password, account_data["salt"]) if account_data["hash"] != hsh: return False return True def update_password(self, userkey, password): """ Updates the password for the account with the given key. :param userkey: key of the user (e.g. email or username) :param password: account clear password (e.g. user defined password) :return: """ if not self.validate_userkey(userkey) or not self.validate_password( password): return False, "InvalidParameter" account_data = self.store.get_account(userkey) if account_data is None: return False, "Account not found" salt = account_data["salt"] hashedpassword = self.get_hash(password, salt) self.store.update_account(userkey, {"hash": hashedpassword}) return True, "" def delete_account(self, userkey): """ Deletes the account with the given userkey :param userkey: the user key (email address or username) :return: success, error """ account = self.store.get_account(userkey) if account is None: return False, "AccountNotFound" self.store.delete_account(userkey) return True, None def delete_account_with_validation(self, account_id, current_password, lang="en"): """ Deletes the account with the given id, validating its password. :param account_id: :param current_password: :return: """ account = self.store.get_account_by_id(account_id) if account is None: return False, "AccountNotFound" hsh = self.get_hash(current_password, account["salt"]) if account.get("hash") != hsh: return False, "InvalidPassword" # delete the account deleted = self.store.delete_account_by_id(account_id) if not deleted: return False, "NoDocumentDeleted" # TODO: if desired, implement farewell email here return True, None def confirm_account(self, account_id, confirmation_token): """ Confirms the account with the given id and using the given confirmation token. """ if not account_id or account_id.isspace(): raise ArgumentNullException("account_id") if not confirmation_token or confirmation_token.isspace(): raise ArgumentNullException("confirmation_token") account_data = self.store.get_account_by_id(account_id) if not account_data: raise ValueError("Account not found") if account_data.get("confirmed") is True: return True if account_data.get("confirmation_token") != confirmation_token: raise ValueError("Invalid confirmation token for account `%s`" % account_id) self.store.update_account_by_id(account_id, {"confirmed": True}, unset_data={"confirmation_token": ""}) return True def ban_account(self, userkey): """ Bans the account with the given userkey. :param userkey: the user key (email address or username) :return: success, error """ return self.update_account(userkey, {"banned": True}) def update_account(self, userkey, data): """ Updates the account with the given key; setting the data. :param userkey: the user key (email address or username) :param data: account data to update :return: self """ account = self.store.get_account(userkey) if account is None: return False, "AccountNotFound" self.store.update_account(userkey, data) return True, None def try_login(self, userkey, password, remember, client_ip, client_data=None, check_password=True): """ Tries to perform a login for the user with the given key (e.g. email or username); password; :param userkey: the user key (email address or username) :param password: :param remember: whether to have a longer expiration time or not :param client_ip: ip of the client for which the function has been called :param client_data: optional client data :param options: extra options :return: """ # get account data account_data = self.get_account(userkey) if account_data is None: return False, "WrongCombo" login_attempts = self.get_failed_login_attempts(userkey) too_many_attempts = self.options.failed_login_attempts_limit <= login_attempts if too_many_attempts: # log information return False, "TooManyAttempts" # error: too many attempts in the last minutes, for this user if check_password: # generate hash of given password, appending salt hsh = self.get_hash(password, account_data.salt) if account_data.hash != hsh: # the key exists, but the password is wrong self.report_login_attempt(userkey, client_ip) # exit return False, "WrongCombo" # check if the account is confirmed if self.options.requires_account_confirmation and not account_data.confirmed: return False, "RequireConfirmation" # check if the account was banned if hasattr(account_data, "banned") and account_data.banned is True: return False, "BannedAccount" # get session expiration expiration = self.get_new_expiration(remember) # save session session = self.store.create_session(userkey, expiration, client_ip, client_data) session = Session.from_dict(session) del account_data.salt del account_data.hash principal = self.principal_type # TODO: if desired, implement sending of email here "new login from..." return True, AuthenticationResult( principal(account_data.id, account_data, session, True), session) def report_login_attempt(self, userkey, client_ip): """ Reports a login attempt, storing it in the persistence layer. :param userkey: the user key (email address or username) :param client_ip: ip of the client """ now = datetime.datetime.utcnow() self.store.save_login_attempt(userkey, client_ip, now) return self def validate_password_reset_token(self, account_id, token): account_data = self.store.get_account_by_id(account_id) if account_data is None: return False, "AccountNotFound" password_reset_token = account_data.get("passwordResetToken") if not password_reset_token or password_reset_token.isspace(): return False, "MissingPasswordResetToken" if password_reset_token != token: return False, "InvalidToken" return True, None def change_password(self, account_id, current_password, password_one, password_two): """ This function is used when a user wants to change a password, using the current password """ require_params(account_id=account_id, current_password=current_password, password_one=password_one, password_two=password_two) account_data = self.store.get_account_by_id(account_id) if account_data is None: raise InvalidOperation("Account not found") # validate current password if not self.is_password_correct(account_id, current_password): raise ValueError("WrongPassword") # validate passwords valid_passwords, error = self.validate_passwords( password_one, password_two) if not valid_passwords: raise ValueError("Passwords are not valid: " + error) # update account password salt = self.get_new_salt() hashedpassword = self.get_hash(password_one, salt) # commit the change: self.store.change_password(account_id, hashedpassword, salt) return True, None def commit_password_reset(self, account_id, token, password_one, password_two): """ This function is used when a user requested a password change, using a token that was sent by email. It validates a token that was set previously. :param account_id: id of the account of which the password should be changed :param token: password reset token :param password_one: first password :param password_two: password repetition :return: """ require_params(account_id=account_id, token=token, password_one=password_one, password_two=password_two) account_data = self.store.get_account_by_id(account_id) if account_data is None: raise InvalidOperation("Account not found") password_reset_token = account_data.get("passwordResetToken") if not password_reset_token or password_reset_token.isspace(): raise InvalidOperation("Password reset token not initialized") if password_reset_token != token: raise ValueError("Invalid password reset token") # validate passwords valid_passwords, error = self.validate_passwords( password_one, password_two) if not valid_passwords: raise ValueError("Passwords are not valid: " + error) salt = self.get_new_salt() hashedpassword = self.get_hash(password_one, salt) # commit the change: self.store.change_password(account_id, hashedpassword, salt) return True def get_failed_login_attempts(self, userkey): """ Gets the number of failed login attempts for a userkey in the amount of minutes defined by MinutesLimit option. :param userkey: the user key (email address or username) """ now = datetime.datetime.utcnow() ms = self.options.minutes_limit * 60 * 1e3 start = now - datetime.timedelta(milliseconds=ms) count = self.store.get_failed_login_attempts(userkey, start, now) return count async def try_login_by_session_key(self, sessionkey): """ Tries to perform login by user session key. :param sessionkey: :return: boolean, session, account """ # returns bool, principal if sessionkey is None: return False, None session = await self.store.get_session_by_guid(sessionkey) if session is None: return False, None # convert into a class session = Session.from_dict(session) now = datetime.datetime.utcnow() if session.expiration < now: return False, None principal_type = self.principal_type if session.anonymous: return True, AuthenticationResult( principal_type(None, None, session, False), session) # get account data account = await self.store.get_account(session.userkey) if account is None: return False, None # return session and account data principal_type = self.principal_type return True, AuthenticationResult( principal_type(account["id"], account, session, True), session) async def initialize_anonymous_session(self, client_ip, client_data): """ Initializes a session for an anonymous user. :param client_ip: ip of the client :param client_data: information about the client software :return: """ expiration = self.get_new_expiration(True) user_id = None session_data = await self.store.create_session(user_id, expiration, client_ip, client_data) session = Session.from_dict(session_data) principal_type = self.principal_type return AuthenticationResult(principal_type(None, None, session, False), session) def get_new_expiration(self, remember=None): """ Returns the expiration for a new session, based on the provider settings and if the user wants to be remembered. for longer or not. :param remember: boolean :return: datetime """ if remember is None: remember = False now = datetime.datetime.utcnow() ms = self.options.long_time_expiration if remember else self.options.short_time_expiration expiration = now + datetime.timedelta(milliseconds=ms) return expiration def save_session_data(self, sessionkey, data): """ Stores session data in the database. :param sessionkey: the key of the session :param data: dictionary of data to store in database :return: """ self.store.save_session_data(sessionkey, data) return self def get_session_data(self, sessionkey): """ Gets the data associated with the given session, in database. :param sessionkey: the key of the session :return: dict """ return self.options.get_session_data(sessionkey) def destroy_session(self, sessionkey): """ Destroys the session with the given key. :param sessionkey: key of the session to destroy. :return: self """ self.store.destroy_session(sessionkey) return self def validate_new_passwords(self, pass_one, pass_two): if pass_one != pass_two: return False if not self.validate_password(pass_one): return False return True def validate_passwords(self, password_one, password_two): """ Validates the two given passwords. :param password_one: first password written by user. :param password_two: password confirmation. :return: success, error """ if not password_one or not password_two: return False, "missing password" if password_one != password_two: return False, "password mismatch" v = password_two valid = self.validate_password(v) if not valid: return False, "password too weak" return True, None def validate_password(self, password): """ Validates a single value to see if it's suitable for a password. :param password: value to validate. """ if not password or password.isspace(): return False l = len(password) if l < 6: # too simple, either way return False if l > 50: # too long: the interface allows maximum 50 chars return False keys = OrderedDict.fromkeys(password).keys() keys_length = len(keys) if keys_length < 3: # the password is too weak, it contains less than 3 different types of character return False forbidden = {"password", "qwerty", "123456", "1234567"} if password.lower() in forbidden: return False return True
class MembershipStore(MongoStore): """ Represents a Sessions and Accounts Storage manager for MongoDB utilized to manage Membership for an application. It can be used to handle global authentication; or per-area authentication. Contains data access logic for accounts and sessions. """ defaults = { "accounts_collection": "accounts", "sessions_collection": "sessions", "login_attempts_collection": "login_attempts", "user_key_field": "email" } def __init__(self, options=None): if options is None: options = {} params = dict(self.defaults, **options) self.options = Bunch() self.options.merge(params) self.configure() def configure(self): """ Configure the db, creating the needed collections and theirs indexes. :return: self """ opt = self.options db_collections = db.collection_names(include_system_collections=False) accounts_collection = opt.accounts_collection sessions_collection = opt.sessions_collection login_attempts_collection = opt.login_attempts_collection user_key_field = opt.user_key_field if accounts_collection not in db_collections: # create the account collection accounts = db.create_collection(accounts_collection) accounts.create_index([(user_key_field, ASCENDING)]) if sessions_collection not in db_collections: # create the sessions collection sessions = db.create_collection(sessions_collection) sessions.create_index([(user_key_field, ASCENDING)]) sessions.create_index([("guid", ASCENDING)]) if login_attempts_collection and login_attempts_collection not in db_collections: # create the login attempts collection login_attempts = db.create_collection(login_attempts_collection) login_attempts.create_index([(user_key_field, ASCENDING)]) return self def get_account_condition(self, userkey): """ Returns account search condition. :param userkey: user key :return: condition for a mongodb search """ a = {self.options.user_key_field: userkey} return a def get_account(self, userkey): """ Gets the account data associated with the user with the given key. :param userkey: user key :return: """ collection = db[self.options.accounts_collection] condition = self.get_account_condition(userkey) data = collection.find_one(condition) return self.normalize_id(data) def get_account_by_id(self, account_id): """ Gets the account data associated with the user with the given id. :param id: user id :return: """ collection = db[self.options.accounts_collection] condition = {"_id": ObjectId(account_id)} data = collection.find_one(condition) return self.normalize_id(data) def get_accounts(self, options): """ Gets a list of all application accounts. """ collection = db[self.options.accounts_collection] return self.get_catalog_page(collection, options) def get_sessions(self, options): """ Gets a list of current sessions. """ collection = db[self.options.sessions_collection] return self.get_catalog_page(collection, options) def get_session(self, sessionkey): """ Gets the session with the given key. :param sessionkey: session guid :return: """ collection = db[self.options.sessions_collection] data = collection.find_one({"guid": sessionkey}) if not data: return None #set user key data["userkey"] = data[self.options.user_key_field] return self.normalize_id(data) def create_session(self, userkey, expiration, client_ip, client_data): """ Creates a session in the database. :param userkey: key of the user for whom we are initializing the session :param expiration: expiration datetime of the session :param client_ip: ip of the client for whom this function is invoked :param client_data: client speficif information (e.g. browser navigator) :return: """ collection = db[self.options.sessions_collection] data = { "guid": str(uuid.uuid1()), self.options.user_key_field: userkey, "anonymous": userkey is None or userkey == False, "expiration": expiration, "client_ip": client_ip, "client_data": client_data, "timestamp": datetime.now() } result = collection.insert_one(data) session_id = result.inserted_id data = collection.find_one({"_id": session_id}) return self.normalize_id(data) def create_account(self, userkey, hashedpassword, salt, data, roles=None): """ Creates a new account :param userkey: user key (e.g. email or username) :param hashedpassword: hashed password :param salt: salt used to hash the account password :param data: extra account data """ collection = db[self.options.accounts_collection] account_data = { self.options.user_key_field: userkey, "hash": hashedpassword, "salt": salt, "data": data, "roles": roles, "timestamp": datetime.now() } result = collection.insert_one(account_data) return {"id": str(result.inserted_id)} def save_session_data(self, sessionkey, data): """ Stores data for the session with the given key. :param sessionkey: session guid. :param data: session data """ collection = db[self.options.sessions_collection] condition = {"guid": sessionkey} update = {"data": data} collection.update_one(condition, update) def get_session_data(self, sessionkey): """ Gets the data associated with the session with the given key. :param sessionkey: session guid. """ collection = db[self.options.sessions_collection] condition = {"guid": sessionkey} session = collection.find_one(condition) return session["data"] def destroy_session(self, sessionkey): collection = db[self.options.sessions_collection] condition = {"guid": sessionkey} collection.delete_one(condition) def get_failed_login_attempts(self, userkey, start, end): """ Gets the number of failed login attempts for a user with a given key, in the last minutes. :param userkey: key of the user for whom we are initializing the session :param start: start datetime to check for login attempts :param end: end datetime to check for login attempts :return: """ condition = { self.options.user_key_field: userkey, "timestamp": { "$gte": start, "$lt": end } } collection = db[self.options.login_attempts_collection] attempts = collection.find(condition) return attempts.count() def save_login_attempt(self, userkey, client_ip, time): """ Stores a failed login attempt in database :param userkey: key of the user for whom the login attempt must be stored :param client_ip: ip of the client for which the method is invoked :param time: timestamp of the login attempt :return: """ data = { self.options.user_key_field: userkey, "client_ip": client_ip, "timestamp": time } collection = db[self.options.login_attempts_collection] collection.insert_one(data) def update_account(self, userkey, data): """ Updates the account associated to the user with the given key. :param userkey: key of the user whose account must be deleted :param data: new account data """ collection = db[self.options.accounts_collection] condition = self.get_account_condition(userkey) collection.update_one(condition, {"$set": data}) def delete_account(self, userkey): """ Deletes the account associated to the user with the given key. :param userkey: key of the user whose account must be deleted """ collection = db[self.options.accounts_collection] condition = self.get_account_condition(userkey) collection.delete_one(condition)
class ReportsManager(): """ Provides business logic to log application specific messages in a database. """ defaults = {"store": None} def __init__(self, options=None): if options is None: options = {} params = dict(self.defaults, **options) self.validate_store_option(params) self.options = Bunch() self.options.merge(params) def get_messages(self, options): """ Gets a paginated result of messages. :param options: pagination options :return: catalog page """ if options is None: raise Exception("Missing pagination options.") options["search_properties"] = ["message"] return self.options.store.get_messages(options) def get_exceptions(self, options): """ Gets a paginated result of exceptions. :param options: pagination options :return: catalog page """ if options is None: raise Exception("Missing pagination options.") options["search_properties"] = ["type", "message", "callstack"] return self.options.store.get_exceptions(options) def log_message(self, message, kind="Normal"): """ Logs a message :param message: application message to store :param kind: kind of message """ now = datetime.now() return self.options.store.store_message(message, now, kind) def log_exception(self, ex=None): """ Logs an exception in database. :param ex: exception object """ now = datetime.now() exc_type, exc_value, exc_traceback = sys.exc_info() if ex is None: ex = exc_value call_stack = "\n".join(traceback.format_tb(exc_traceback)) message = ex.message if hasattr(ex, "message") \ else exc_value.args[0] if len(exc_value.args) > 0 else str(ex) return self.options.store.store_exception(message, now, str(exc_type), call_stack) def try_log_exception(self, ex=None): """ Tries to log an exception in database; does nothing if the log itself causes exception. :param ex: exception object """ try: self.log_exception(ex) except Exception: pass def log_warning(self, message): """ Logs a warning message :param message: application message to store """ now = datetime.now() return self.options.store.store_message(message, now, "Warning") @staticmethod def validate_store_option(params): """ Validates the store option passed when instantiating a ReportsManager. :param params: constructor options """ if params["store"] is None: raise Exception("Missing `store` option") req = [ "store_message", "store_exception", "get_messages", "get_exceptions" ] for name in req: if not hasattr(params["store"], name): raise Exception("The given store does not implement `" + name + "` member")