示例#1
0
 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 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 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
示例#14
0
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
示例#15
0
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")