Example #1
0
def test_get_answer(client, logged_in_headers, admin_logged_in_headers):
    """Test the answers/<id> route."""
    answer = Answer(answer=Answer.hash_answer('strawberry'),
                    user_id=1,
                    question_id=1)
    db.session.add(answer)
    db.session.commit()
    rv = client.get('/api/v1/answers/1', headers=logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'id': 1,
        'question': {
            'enabled': True,
            'id': 1,
            'question': 'What is your favorite flavor of ice cream?',
            'url': 'http://localhost/api/v1/questions/1'
        },
        'user_id': 1
    }

    rv = client.get('/api/v1/answers/1', headers=admin_logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message':
        'Administrators are not authorized to proceed with this action',
        'status': 403
    }
Example #2
0
def test_get_answers(client, logged_in_headers, admin_logged_in_headers):
    """Test the answers route."""
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'), user_id=1, question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('Buzz Lightyear'), user_id=1, question_id=3)
    answer4 = Answer(answer=Answer.hash_answer('Hamm'), user_id=2, question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.add(answer4)
    db.session.commit()
    rv = client.get('/api/v1/answers', headers=logged_in_headers)
    items = [
        {
            'id': 1,
            'question': {
                'enabled': True,
                'id': 1,
                'question': 'What is your favorite flavor of ice cream?',
                'url': 'http://localhost/api/v1/questions/1',
            },
            'url': 'http://localhost/api/v1/answers/1',
            'user_id': 1
        },
        {
            'id': 2,
            'question': {
                'enabled': True,
                'id': 2,
                'question': 'What is your favorite color?',
                'url': 'http://localhost/api/v1/questions/2',
            },
            'url': 'http://localhost/api/v1/answers/2',
            'user_id': 1
        },
        {
            'id': 3,
            'question': {
                'enabled': True,
                'id': 3,
                'question': 'What is your favorite toy?',
                'url': 'http://localhost/api/v1/questions/3',
            },
            'url': 'http://localhost/api/v1/answers/3',
            'user_id': 1
        }
    ]
    assert json.loads(rv.data.decode('utf-8'))['items'] == items

    rv = client.get('/api/v1/answers', headers=admin_logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message': 'Administrators are not authorized to proceed with this action',
        'status': 403
    }
Example #3
0
def _configure_user():
    """Configure testuser2 in the database."""
    user = User(ad_guid='10385a23-6def-4990-84a8-32444e36e496')
    db.session.add(user)
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'), user_id=1, question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('buzz lightyear'), user_id=1, question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.commit()
Example #4
0
def test_get_answer_different_user(client, logged_in_headers):
    """Test accessing the answer of a different user in the answers/<id> route."""
    user = User(ad_guid='5609c5ec-c0df-4480-a94b-b6eb0fc4c066')
    db.session.add(user)
    db.session.commit()
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=user.id, question_id=1)
    db.session.add(answer)
    db.session.commit()
    rv = client.get('/api/v1/answers/1', headers=logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message': 'This answer is not associated with your account',
        'status': 401,
    }
Example #5
0
def test_add_answers_case_sensitive(app, client, logged_in_headers):
    """Test the answers POST route when case sensitive answers are enabled."""
    data = json.dumps(
        [
            {'question_id': 2, 'answer': 'Bright Green'},
            {'question_id': 3, 'answer': 'Buzz Lightyear'},
            {'question_id': 1, 'answer': 'strawberry'},
        ]
    )
    with mock.patch.dict(app.config, {'CASE_SENSITIVE_ANSWERS': True}):
        client.post('/api/v1/answers', headers=logged_in_headers, data=data)
    assert Answer.verify_answer('bright green', Answer.query.get(1).answer) is False
    assert Answer.verify_answer('Bright Green', Answer.query.get(1).answer) is True
Example #6
0
def test_get_answers(client, logged_in_headers, admin_logged_in_headers):
    """Test the answers route."""
    answer = Answer(answer=Answer.hash_answer('strawberry'),
                    user_id=1,
                    question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'),
                     user_id=1,
                     question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('Buzz Lightyear'),
                     user_id=1,
                     question_id=3)
    answer4 = Answer(answer=Answer.hash_answer('Hamm'),
                     user_id=2,
                     question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.add(answer4)
    db.session.commit()
    rv = client.get('/api/v1/answers', headers=logged_in_headers)
    items = [{
        'id': 1,
        'question': {
            'enabled': True,
            'id': 1,
            'question': 'What is your favorite flavor of ice cream?',
            'url': 'http://localhost/api/v1/questions/1',
        },
        'url': 'http://localhost/api/v1/answers/1',
        'user_id': 1
    }, {
        'id': 2,
        'question': {
            'enabled': True,
            'id': 2,
            'question': 'What is your favorite color?',
            'url': 'http://localhost/api/v1/questions/2',
        },
        'url': 'http://localhost/api/v1/answers/2',
        'user_id': 1
    }, {
        'id': 3,
        'question': {
            'enabled': True,
            'id': 3,
            'question': 'What is your favorite toy?',
            'url': 'http://localhost/api/v1/questions/3',
        },
        'url': 'http://localhost/api/v1/answers/3',
        'user_id': 1
    }]
    assert json.loads(rv.data.decode('utf-8'))['items'] == items

    rv = client.get('/api/v1/answers', headers=admin_logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message':
        'Administrators are not authorized to proceed with this action',
        'status': 403
    }
Example #7
0
def test_get_answers_unauthenticated(client, logged_in_headers, mock_ad):
    """Test the unauthenticated answers route."""
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'), user_id=1, question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('Buzz Lightyear'), user_id=1, question_id=3)
    answer4 = Answer(answer=Answer.hash_answer('Hamm'), user_id=2, question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.add(answer4)
    db.session.commit()
    rv = client.get('/api/v1/answers/testuser2', headers={'Content-Type': 'application/json'})
    items = [
        {
            'id': 1,
            'question': {
                'enabled': True,
                'id': 1,
                'question': 'What is your favorite flavor of ice cream?',
                'url': 'http://localhost/api/v1/questions/1',
            },
            'url': 'http://localhost/api/v1/answers/1',
            'user_id': 1
        },
        {
            'id': 2,
            'question': {
                'enabled': True,
                'id': 2,
                'question': 'What is your favorite color?',
                'url': 'http://localhost/api/v1/questions/2',
            },
            'url': 'http://localhost/api/v1/answers/2',
            'user_id': 1
        },
        {
            'id': 3,
            'question': {
                'enabled': True,
                'id': 3,
                'question': 'What is your favorite toy?',
                'url': 'http://localhost/api/v1/questions/3',
            },
            'url': 'http://localhost/api/v1/answers/3',
            'user_id': 1
        }
    ]
    assert json.loads(rv.data.decode('utf-8'))['items'] == items
Example #8
0
def test_get_answers_unauthenticated(client, logged_in_headers, mock_ad):
    """Test the unauthenticated answers route."""
    answer = Answer(answer=Answer.hash_answer('strawberry'),
                    user_id=1,
                    question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'),
                     user_id=1,
                     question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('Buzz Lightyear'),
                     user_id=1,
                     question_id=3)
    answer4 = Answer(answer=Answer.hash_answer('Hamm'),
                     user_id=2,
                     question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.add(answer4)
    db.session.commit()
    rv = client.get('/api/v1/answers/testuser2',
                    headers={'Content-Type': 'application/json'})
    items = [{
        'id': 1,
        'question': {
            'enabled': True,
            'id': 1,
            'question': 'What is your favorite flavor of ice cream?',
            'url': 'http://localhost/api/v1/questions/1',
        },
        'url': 'http://localhost/api/v1/answers/1',
        'user_id': 1
    }, {
        'id': 2,
        'question': {
            'enabled': True,
            'id': 2,
            'question': 'What is your favorite color?',
            'url': 'http://localhost/api/v1/questions/2',
        },
        'url': 'http://localhost/api/v1/answers/2',
        'user_id': 1
    }, {
        'id': 3,
        'question': {
            'enabled': True,
            'id': 3,
            'question': 'What is your favorite toy?',
            'url': 'http://localhost/api/v1/questions/3',
        },
        'url': 'http://localhost/api/v1/answers/3',
        'user_id': 1
    }]
    assert json.loads(rv.data.decode('utf-8'))['items'] == items
Example #9
0
def test_delete_answers(client, logged_in_headers, admin_logged_in_headers):
    """Test the answers route using the DELETE method to reset the user's configured answers."""
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'), user_id=1, question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('Buzz Lightyear'), user_id=1, question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.commit()
    rv = client.delete('/api/v1/answers', headers=logged_in_headers)
    assert rv.status_code == 204
    assert rv.data.decode('utf-8') == ''
    assert len(Answer.query.filter_by(user_id=1).all()) == 0

    rv = client.delete('/api/v1/answers', headers=admin_logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message': 'Administrators are not authorized to proceed with this action',
        'status': 403
    }
Example #10
0
def test_add_answers_case_sensitive(app, client, logged_in_headers):
    """Test the answers POST route when case sensitive answers are enabled."""
    data = json.dumps([
        {
            'question_id': 2,
            'answer': 'Bright Green'
        },
        {
            'question_id': 3,
            'answer': 'Buzz Lightyear'
        },
        {
            'question_id': 1,
            'answer': 'strawberry'
        },
    ])
    with mock.patch.dict(app.config, {'CASE_SENSITIVE_ANSWERS': True}):
        client.post('/api/v1/answers', headers=logged_in_headers, data=data)
    assert Answer.verify_answer('bright green', Answer.query.get(1).answer) is False
    assert Answer.verify_answer('Bright Green', Answer.query.get(1).answer) is True
Example #11
0
def test_delete_answers(client, logged_in_headers, admin_logged_in_headers):
    """Test the answers route using the DELETE method to reset the user's configured answers."""
    answer = Answer(answer=Answer.hash_answer('strawberry'),
                    user_id=1,
                    question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'),
                     user_id=1,
                     question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('Buzz Lightyear'),
                     user_id=1,
                     question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.commit()
    rv = client.delete('/api/v1/answers', headers=logged_in_headers)
    assert rv.status_code == 204
    assert rv.data.decode('utf-8') == ''
    assert len(Answer.query.filter_by(user_id=1).all()) == 0

    rv = client.delete('/api/v1/answers', headers=admin_logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message':
        'Administrators are not authorized to proceed with this action',
        'status': 403
    }
Example #12
0
def test_get_answer_different_user(client, logged_in_headers):
    """Test accessing the answer of a different user in the answers/<id> route."""
    user = User(ad_guid='5609c5ec-c0df-4480-a94b-b6eb0fc4c066')
    db.session.add(user)
    db.session.commit()
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=user.id, question_id=1)
    db.session.add(answer)
    db.session.commit()
    rv = client.get('/api/v1/answers/1', headers=logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message': 'This answer is not associated with your account',
        'status': 401
    }
Example #13
0
def _configure_user():
    """Configure testuser2 in the database."""
    user = User(ad_guid='10385a23-6def-4990-84a8-32444e36e496')
    db.session.add(user)
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1)
    answer2 = Answer(answer=Answer.hash_answer('green'), user_id=1, question_id=2)
    answer3 = Answer(answer=Answer.hash_answer('buzz lightyear'), user_id=1, question_id=3)
    db.session.add(answer)
    db.session.add(answer2)
    db.session.add(answer3)
    db.session.commit()
Example #14
0
def test_get_answer(client, logged_in_headers, admin_logged_in_headers):
    """Test the answers/<id> route."""
    answer = Answer(answer=Answer.hash_answer('strawberry'), user_id=1, question_id=1)
    db.session.add(answer)
    db.session.commit()
    rv = client.get('/api/v1/answers/1', headers=logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'id': 1,
        'question': {
            'enabled': True,
            'id': 1,
            'question': 'What is your favorite flavor of ice cream?',
            'url': 'http://localhost/api/v1/questions/1'
        },
        'user_id': 1
    }

    rv = client.get('/api/v1/answers/1', headers=admin_logged_in_headers)
    assert json.loads(rv.data.decode('utf-8')) == {
        'message': 'Administrators are not authorized to proceed with this action',
        'status': 403
    }
Example #15
0
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
Example #16
0
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
Example #17
0
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
Example #18
0
def reset_password(username):
    """
    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)
    answers = req_json['answers']
    new_password = req_json['new_password']

    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