def _validate_api_input(json_req, key, expected_type): """ Validate the API input to ensure it is not empty and the correct type. :param dict json_req: the JSON of the Flask request :param str key: the key of the input to validate :param type expected_type: the type the input should be :raises ValidationError: if the input is empty or the wrong type """ value = json_req.get(key) if value is not False and value != 0 and not value: raise ValidationError( 'The parameter "{0}" must not be empty'.format(key)) elif not isinstance(value, expected_type): if expected_type == str or expected_type == string_types: type_name = 'string' elif expected_type == dict: type_name = 'object' elif expected_type == list: type_name = 'array' elif expected_type == int: type_name = 'integer' elif expected_type == bool: type_name = 'boolean' else: type_name = expected_type.__name__ raise ValidationError('The parameter "{0}" must be a {1}'.format( key, type_name))
def reset_password(self, sam_account_name, new_password): """ Reset and unlock a user's password. :param str sam_account_name: the user's sAMAccountName to reset :param str new_password: the user's new password :raises ValidationError: if the new password doesn't meet the domain standards """ if not self.match_pwd_complexity(new_password): raise ValidationError( 'The password did not match the complexity requirements. Please ensure your ' 'password contains at least three of the four requirements: lowercase letters, ' 'uppercase letters, numbers, and special charcters.') elif not self.match_min_pwd_length(new_password): raise ValidationError( 'The password must be at least {0} characters long'.format( self.min_pwd_length)) dn = self.get_dn(sam_account_name) self.connection.extend.microsoft.modify_password(dn, new_password, old_password=None) self.connection.extend.microsoft.unlock_account(dn) self.log( 'info', 'The password for "{0}" was reset'.format(self.connection.user))
def add_jwt_claims(identity): """ Verify the user is authorized and add the role (admin or user) to the JWT. :param dict identity: a dictionary with user's GUID stored """ ad = adreset.ad.AD() ad.service_account_login() claims = {} if ad.check_admin_group_membership(identity['guid']): claims['roles'] = ['admin'] elif ad.check_user_group_membership(identity['guid']): # Make sure there are enough questions configured for the application to be usable total_questions = db.session.query(func.count( Question.question)).scalar() if total_questions < current_app.config['REQUIRED_ANSWERS']: log.error( 'There are {0} questions configured. There must be at least {1}.' .format(total_questions, current_app.config['REQUIRED_ANSWERS'])) raise ValidationError( 'The administrator has not finished configuring the application' ) else: claims['roles'] = ['user'] else: raise Unauthorized('You don\'t have access to use this application') username = ad.get_sam_account_name(identity['guid']) claims['username'] = username return claims
def patch_question(question_id): """ Patch a question that users can use for their secret answers. :rtype: flask.Response """ req_json = request.get_json(force=True) valid_keys = set(['question', 'enabled']) if not valid_keys.issuperset(set(req_json.keys())): raise ValidationError( 'Invalid keys were supplied. Please use the following keys: {0}'. format(', '.join(sorted(valid_keys)))) question = Question.query.get(question_id) if not question: raise NotFound('The question was not found') if 'question' in req_json: _validate_api_input(req_json, 'question', string_types) question.question = req_json['question'] if 'enabled' in req_json: _validate_api_input(req_json, 'enabled', bool) question.enabled = req_json['enabled'] db.session.commit() return jsonify(question.to_json()), 200
def login(self, username, password): """ Login to Active Directory. :param str username: the Active Directory username :param str password: the Active Directory password """ if self.connection.bound: msg = 'The login method was called but the connection is already bound. Will reconnect.' self.log('debug', msg) self.connection.unbind() self.connection.open() domain = self._get_config('AD_DOMAIN') if '@' in username or '\\' in username or 'CN=' in username: self.connection.user = username else: if self.connection.authentication == ldap3.NTLM: self.connection.user = '******'.format(domain, username) else: self.connection.user = '******'.format(username, domain) self.connection.password = password if not self.connection.bind(): self.log( 'info', 'The user "{0}" failed to login'.format(self.connection.user)) raise ValidationError( 'The username or password is incorrect. Please try again.') else: self.log( 'info', 'The user "{0}" logged in successfully'.format( self.connection.user))
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 add_question(): """ Add a question that users can use for their secret answers. :rtype: flask.Response """ req_json = request.get_json(force=True) _validate_api_input(req_json, 'question', string_types) if 'enabled' in req_json: _validate_api_input(req_json, 'enabled', bool) exists = bool((db.session.query(func.count( Question.question))).filter_by(question=req_json['question']).scalar()) if exists: raise ValidationError('The supplied question already exists') question = Question(question=req_json['question']) if 'enabled' in req_json: question.enabled = req_json['enabled'] db.session.add(question) db.session.commit() return jsonify(question.to_json()), 201
def reset_password(): """ Reset a user's password using their secret answers. :rtype: flask.Response """ req_json = request.get_json(force=True) _validate_api_input(req_json, 'answers', list) _validate_api_input(req_json, 'new_password', string_types) _validate_api_input(req_json, 'username', string_types) answers = req_json['answers'] new_password = req_json['new_password'] username = req_json['username'] not_setup_msg = ( 'You must have configured at least {0} secret answers before resetting your ' 'password').format(current_app.config['REQUIRED_ANSWERS']) # Verify the user exists in the database ad = adreset.ad.AD() ad.service_account_login() user_id = User.get_id_from_ad_username(username, ad) if not user_id: msg = 'The user attempted a password reset but does not exist in the database' log.debug({'message': msg, 'user': username}) raise ValidationError(not_setup_msg) # Make sure the user isn't locked out if User.is_user_locked_out(user_id): msg = 'The user attempted a password reset but their account is locked in ADReset' log.info({'message': msg, 'user': username}) raise Unauthorized('Your account is locked. Please try again later.') db_answers = Answer.query.filter_by(user_id=user_id).all() # Create a dictionary of question_id to answer from entries in the database. This will avoid # the need to continuously loop through these answers looking for specific answers later on. q_id_to_answer_db = {} for answer in db_answers: q_id_to_answer_db[answer.question_id] = answer.answer # Make sure the user has all their answers configured if len(q_id_to_answer_db.keys()) != current_app.config['REQUIRED_ANSWERS']: msg = ( 'The user did not have their secret answers configured and attempted to reset their ' 'password') log.debug({'message': msg, 'user': username}) raise ValidationError(not_setup_msg) seen_question_ids = set() for answer in answers: if not isinstance( answer, dict) or 'question_id' not in answer or 'answer' not in answer: raise ValidationError( 'The answers must be an object with the keys "question_id" and "answer"' ) _validate_api_input(answer, 'question_id', int) _validate_api_input(answer, 'answer', string_types) if answer['question_id'] not in q_id_to_answer_db: msg = ( 'The user answered a question they did not previously configure while ' 'attempting to reset their password') log.info({'message': msg, 'user': username}) raise ValidationError( 'One of the answers was to a question that wasn\'t previously configured' ) # Don't allow an attacker to enter in the same question and answer combination more than # once if answer['question_id'] in seen_question_ids: msg = ( 'The user answered the same question multiple times while attempting to reset ' 'their password') log.info({'message': msg, 'user': username}) raise ValidationError( 'You must answer {0} different questions'.format( current_app.config['REQUIRED_ANSWERS'])) seen_question_ids.add(answer['question_id']) # Only check if the answers are correct after knowing the input is valid as to not give away # any hints as to which answer is incorrect for an attacker for answer in answers: if current_app.config['CASE_SENSITIVE_ANSWERS'] is True: input_answer = answer['answer'] else: input_answer = answer['answer'].lower() is_correct_answer = Answer.verify_answer( input_answer, q_id_to_answer_db[answer['question_id']]) if is_correct_answer is not True: log.info({ 'message': 'The user entered an incorrect answer', 'user': username }) failed_attempt = FailedAttempt(user_id=user_id, time=datetime.utcnow()) db.session.add(failed_attempt) db.session.commit() if User.is_user_locked_out(user_id): msg = 'The user failed too many password reset attempts. They are now locked out.' log.info({'message': msg, 'user': username}) raise Unauthorized( 'You have answered incorrectly too many times. Your account is ' 'now locked. Please try again later.') raise Unauthorized( 'One or more answers were incorrect. Please try again.') log.debug({ 'message': 'The user successfully answered their questions', 'user': username }) ad.reset_password(username, new_password) log.info({ 'message': 'The user successfully reset their password', 'user': username }) return jsonify({}), 204
def add_answers(): """ Add a user's secret answers tied to administrator approved questions. :rtype: flask.Response """ user_ad_guid = get_jwt_identity()['guid'] user_id = db.session.query( User.id).filter_by(ad_guid=user_ad_guid).scalar() username = get_jwt_identity()['username'] # Make sure the user hasn't already set the required amount of secret answers num_answers_in_db = \ (db.session.query(func.count(Answer.answer))).filter_by(user_id=user_id).scalar() if num_answers_in_db != 0: log.debug({ 'message': 'The user attempted to set their secret answers but had them already set', 'user': username, }) raise ValidationError( 'You\'ve previously set your secret answers. Please reset them to set them again.' ) req_json = copy.deepcopy(request.get_json(force=True)) if not isinstance(req_json, list): log.debug({ 'message': 'The user did not supply an array', 'user': username }) raise ValidationError('The input must be an array') num_answers = len(req_json) # Verify that the user supplied the required amount of answers if num_answers != current_app.config['REQUIRED_ANSWERS']: log.info({ 'message': 'The user supplied an invalid amount of answers', 'user': username }) if num_answers == 1: error_prefix = '1 answer was' else: error_prefix = '{0} answers were'.format(num_answers) raise ValidationError('{0} supplied but {1} are required'.format( error_prefix, current_app.config['REQUIRED_ANSWERS'])) question_ids = set() answer_strings = set() for answer in req_json: _validate_api_input(answer, 'answer', string_types) _validate_api_input(answer, 'question_id', int) # Verify the answers meet the length requirements if len(answer['answer'] ) < current_app.config['ANSWERS_MINIMUM_LENGTH']: log.info({ 'message': 'The user supplied an answer of length {0}, but {1} is required' .format(len(answer['answer']), current_app.config['ANSWERS_MINIMUM_LENGTH']), 'user': username, }) raise ValidationError( 'The answer must be at least {0} characters long'.format( current_app.config['ANSWERS_MINIMUM_LENGTH'])) # If answers aren't stored as case-sensitive, then convert it to lowercase if current_app.config['CASE_SENSITIVE_ANSWERS'] is False: log.debug({ 'message': 'Setting the answer to lowercase', 'user': username }) answer['answer'] = answer['answer'].lower() # Make sure the supplied question_id maps to a real and enabled question in the database question = Question.query.get(answer['question_id']) if not question: log.info({ 'message': 'The user supplied an invalid question', 'user': username }) raise ValidationError('The "question_id" is invalid') elif question.enabled is False: log.info({ 'message': 'The user tried to use a disabled question', 'user': username }) raise ValidationError( 'The "question_id" of {0} is to a disabled question'.format( question.id)) # Store these in sets to check duplicates question_ids.add(answer['question_id']) answer_strings.add(answer['answer']) # Make sure the user doesn't try to reuse the same question if len(question_ids) != num_answers: log.info({ 'message': 'The user supplied duplicate questions', 'user': username }) raise ValidationError( 'One or more questions were the same. Please provide unique questions.' ) # If duplicate answers aren't allowed, then verify the answers are unique allow_dup_answers = current_app.config['ALLOW_DUPLICATE_ANSWERS'] if allow_dup_answers is False and num_answers != len(answer_strings): log.info({ 'message': 'The user supplied duplicate answers', 'user': username }) raise ValidationError( 'One or more answers were the same. Please provide unique answers.' ) # Now that the input is validated, add the entries to the database answer_objects = [] for answer in req_json: hashed_answer = Answer.hash_answer(answer['answer']) answer_obj = Answer(answer=hashed_answer, question_id=answer['question_id'], user_id=user_id) db.session.add(answer_obj) answer_objects.append(answer_obj) db.session.commit() # This must be run after the session is committed because the ID needs to be set answers_json = [answer.to_json() for answer in answer_objects] log.info({ 'message': 'The user successfully set their secret answers', 'user': username }) return jsonify(answers_json), 201