Пример #1
0
class FailedAttempt(db.Model):
    """Represent a failed password reset attempt."""

    id = db.Column(db.Integer(), primary_key=True)
    user_id = db.Column(db.Integer(),
                        db.ForeignKey('user.id', ondelete='CASCADE'),
                        nullable=False)
    time = db.Column(db.DateTime, nullable=False)
Пример #2
0
class User(db.Model):
    """Represent the Active Directory user."""

    id = db.Column(db.Integer(), primary_key=True)
    # We store the GUID as a string for easier auditing in Active Directory. This must be 36
    # characters because the GUID as a string is 32 characters + 4 hyphens.
    ad_guid = db.Column(db.String(36), nullable=False, unique=True, index=True)
    answers = db.relationship('adreset.models.questions.Answer',
                              backref='user')
    blacklisted_tokens = db.relationship(
        'adreset.models.tokens.BlacklistedToken', backref='user')
Пример #3
0
class Answer(db.Model):
    """Contain the user's answers to the secret questions they've chosen."""

    id = db.Column(db.Integer(), primary_key=True)
    answer = db.Column(db.String(256), nullable=False)
    user_id = db.Column(db.Integer(),
                        db.ForeignKey('user.id', ondelete='CASCADE'),
                        nullable=False)
    question_id = db.Column(db.Integer(),
                            db.ForeignKey('question.id', ondelete='CASCADE'),
                            nullable=False)
Пример #4
0
class BlacklistedToken(db.Model):
    """Contain issued JSON web tokens."""

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(
        db.Integer(), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    jti = db.Column(db.String(36), nullable=False)
    expires = db.Column(db.DateTime, nullable=False)

    @staticmethod
    def add_token(token):
        """
        Add a new token to the database in the unrevoked state.

        :param dict token: the decoded token to blacklist
        """
        user = User.query.filter_by(ad_guid=token['sub']).one()
        db_token = BlacklistedToken(
            jti=token['jti'],
            user_id=user.id,
            expires=datetime.fromtimestamp(token['exp'])
        )
        db.session.add(db_token)
        db.session.commit()

    @staticmethod
    def is_token_revoked(token):
        """
        Check if the token is revoked and default to True if the token is not present.

        :param dict token: the decoded JSON web token to check
        :rtype: bool
        :return: a boolean representing if the token is revoked
        """
        jti = token['jti']
        return bool(BlacklistedToken.query.filter_by(jti=jti).first())

    @staticmethod
    def revoke_token(jti):
        """
        Revoke a token for the given user.

        :param str jti: the GUID that identifies the token
        :raises werkzeug.exceptions.NotFound: if the specified token does not exist in the database
        """
        return bool(BlacklistedToken.query.filter_by(jti=jti).first())
Пример #5
0
class Question(db.Model):
    """Contain the secret questions the administrator decides to configure ADReset with."""

    id = db.Column(db.Integer(), primary_key=True)
    question = db.Column(db.String(256), nullable=False, unique=True)
    answers = db.relationship('Answer', backref='question')
    enabled = db.Column(db.Boolean, default=True, nullable=False)

    @validates('question')
    def validate_question(self, key, question):
        """
        Ensure the question is a string of 256 characters or less.

        :param str key: the key/column being validated
        :param str question: the question being validated
        :return: the question being validated
        :rtype: str
        :raises ValidationError: if the string is more than 256 characters
        :raises RuntimeError: if the question is an invalid type
        """
        if not isinstance(question, string_types):
            raise RuntimeError(_must_be_str.format(key))
        elif len(question) > 256:
            raise ValidationError(
                'The question must be less than 256 characters')
        return question

    def to_json(self, include_url=True):
        """Represent the row as a dictionary for JSON output."""
        rv = {
            'id': self.id,
            'question': self.question,
            'enabled': self.enabled,
        }
        if include_url:
            rv['url'] = url_for('api_v1.get_question',
                                question_id=self.id,
                                _external=True)
        return rv
Пример #6
0
class User(db.Model):
    """Represent the Active Directory user."""

    id = db.Column(db.Integer(), primary_key=True)
    # We store the GUID as a string for easier auditing in Active Directory. This must be 36
    # characters because the GUID as a string is 32 characters + 4 hyphens.
    ad_guid = db.Column(db.String(36), nullable=False, unique=True, index=True)
    answers = db.relationship('adreset.models.questions.Answer',
                              backref='user')
    blacklisted_tokens = db.relationship(
        'adreset.models.tokens.BlacklistedToken', backref='user')
    failed_reset__attempts = db.relationship('FailedAttempt', backref='user')

    @staticmethod
    def get_id_from_ad_username(username, ad=None):
        """
        Query Active Directory to find the user's ID in the database.

        :param str username: the user's sAMAccountName
        :kwarg adreset.ad.AD ad: an optional Active Directory session that is logged in with the
            service account
        :return: the user's ID in the database
        :rtype: int or None
        """
        if not ad:
            ad = adreset.ad.AD()
            ad.service_account_login()
        try:
            user_guid = ad.get_guid(username)
        except adreset.error.ADError:
            return None

        return db.session.query(User.id).filter_by(ad_guid=user_guid).scalar()

    @staticmethod
    def get_ad_username_from_id(user_id, ad=None):
        """
        Query Active Directory to find the user's sAMAccountName from the ID in the database.

        :param int user_id: the user's ID in the database
        :kwarg adreset.ad.AD ad: an optional Active Directory session that is logged in with the
            service account
        :return: the user's sAMAccountName
        :rtype: str or None
        """
        if not ad:
            ad = adreset.ad.AD()
            ad.service_account_login()

        user_guid = db.session.query(
            User.ad_guid).filter_by(id=user_id).scalar()
        try:
            username = ad.get_sam_account_name(user_guid)
        except adreset.error.ADError:
            return None

        return username

    @staticmethod
    def is_user_locked_out(user_id):
        """
        Check if the passed-in user is locked out.

        :param int user_id: the user ID to check
        :return: a boolean determining if the user is locked out
        :rtype: bool
        """
        lockout_mins = current_app.config['LOCKOUT_MINUTES']
        lockout_datetime = datetime.utcnow() - timedelta(minutes=lockout_mins)
        failed_attempts = (db.session.query(func.count(
            FailedAttempt.id)).filter(
                FailedAttempt.time >= lockout_datetime).scalar())
        return failed_attempts >= current_app.config['ATTEMPTS_BEFORE_LOCKOUT']

    def is_locked_out(self):
        """
        Check if the current user is locked out.

        :return: a boolean determining if the user is locked out
        :rtype: bool
        """
        return self.is_user_locked_out(self.id)
Пример #7
0
class Answer(db.Model):
    """Contain the user's answers to the secret questions they've chosen."""

    id = db.Column(db.Integer(), primary_key=True)
    # The hashed answer should be around 120 characters, but give it plenty of room to expand in
    # the event the hashing algorithm is updated
    answer = db.Column(db.String(256), nullable=False)
    user_id = db.Column(db.Integer(),
                        db.ForeignKey('user.id', ondelete='CASCADE'),
                        nullable=False)
    question_id = db.Column(db.Integer(),
                            db.ForeignKey('question.id', ondelete='CASCADE'),
                            nullable=False)

    @validates('answer')
    def validate_answer(self, key, answer):
        """
        Ensure the answer is hashed.

        :param str key: the key/column being validated
        :param str answer: the answer being validated
        :return: the answer being validated
        :rtype: str
        :raises RuntimeError: if the answer is not hashed or isn't a string
        """
        if not isinstance(answer, string_types):
            raise RuntimeError(_must_be_str.format(key))
        elif not passlib.hash.sha512_crypt.identify(answer):
            raise RuntimeError('The answer must be stored as a SHA512 hash')
        return answer

    @staticmethod
    def hash_answer(answer):
        """
        Hash the answer using the SHA512 algorithm.

        :param str answer: the answer to hash
        :return: a SHA512 hash of the string
        :rtype: str
        """
        return passlib.hash.sha512_crypt.hash(answer)

    @staticmethod
    def verify_answer(input_answer, hashed_answer):
        """
        Verify the input answer and the hashed answer stored in the database are the same.

        :param str input_answer: the answer to verify
        :param str hashed_answer: the hashed answer to verify against
        :return: a boolean determining if the answers match
        :rtype: bool
        """
        return passlib.hash.sha512_crypt.verify(input_answer, hashed_answer)

    def to_json(self, include_url=True):
        """Represent the row as a dictionary for JSON output."""
        rv = {
            'id': self.id,
            'user_id': self.user_id,
            'question': self.question.to_json(),
        }
        if include_url:
            rv['url'] = url_for('api_v1.get_answer',
                                answer_id=self.id,
                                _external=True)
        return rv
Пример #8
0
class Question(db.Model):
    """Contain the secret questions the administrator decides to configure ADReset with."""

    id = db.Column(db.Integer(), primary_key=True)
    question = db.Column(db.String(256), nullable=False, unique=True)
    answers = db.relationship('Answer', backref='question')