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')
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)
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())
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
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)
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
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')