diff --git a/app/celery/tasks.py b/app/celery/tasks.py index d4388a08c..c1306193a 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -251,3 +251,15 @@ def email_invited_user(encrypted_invitation): invitation_content) except AwsSesClientException as e: current_app.logger.error(e) + + +@notify_celery.task(name='send-reset-password') +def email_reset_password(encrypted_reset_password_message): + reset_password_message = encryption.decrypt(encryption) + try: + aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'], + reset_password_message['to'], + "Reset password for GOV.UK Notify", + reset_password_message['reset_password_url']) + except AwsSesClientException as e: + current_app.logger.error(e) diff --git a/app/user/rest.py b/app/user/rest.py index 9977410db..716eea2be 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -24,7 +24,7 @@ from app.schemas import ( permission_schema ) -from app.celery.tasks import (send_sms_code, send_email_code) +from app.celery.tasks import (send_sms_code, send_email_code, email_reset_password) from app.errors import register_errors user = Blueprint('user', __name__) @@ -49,7 +49,7 @@ def create_user(): def update_user(user_id): user_to_update = get_model_users(user_id=user_id) if not user_to_update: - return jsonify(result="error", message="User not found"), 404 + return _user_not_found(user_id) req_json = request.get_json() update_dct, errors = user_schema_load_json.load(req_json) @@ -116,7 +116,7 @@ def send_user_sms_code(user_id): user_to_send_to = get_model_users(user_id=user_id) if not user_to_send_to: - return jsonify(result="error", message="No user found"), 404 + return _user_not_found(user_id) verify_code, errors = request_verify_code_schema.load(request.get_json()) if errors: @@ -138,7 +138,7 @@ def send_user_sms_code(user_id): def send_user_email_code(user_id): user_to_send_to = get_model_users(user_id=user_id) if not user_to_send_to: - return jsonify(result="error", message="No user found"), 404 + return _user_not_found(user_id) verify_code, errors = request_verify_code_schema.load(request.get_json()) if errors: @@ -172,7 +172,7 @@ def set_permissions(user_id, service_id): # who is making this request has permission to make the request. user = get_model_users(user_id=user_id) if not user: - abort(404, 'User not found for id: {}'.format(user_id)) + _user_not_found(user_id) service = dao_fetch_service_by_id(service_id=service_id) if not service: abort(404, 'Service not found for id: {}'.format(service_id)) @@ -197,3 +197,27 @@ def get_by_email(): result = user_schema.dump(user) return jsonify(data=result.data) + + +@user.route('//reset-password', methods=['POST']) +def send_reset_password(user_id): + user_to_send_to = get_model_users(user_id=user_id) + if not user_to_send_to: + return _user_not_found(user_id) + + reset_password_message = {'to': user_to_send_to.email_address, + 'reset_password_url': _create_reset_password_url(user_to_send_to.email_address)} + + email_reset_password.apply_async([encryption.encrypt(reset_password_message)], queue='send-reset-password') + return jsonify({}), 204 + + +def _user_not_found(user_id): + return abort(404, 'User not found for id: {}'.format(user_id)) + + +def _create_reset_password_url(email): + from utils.url_safe_token import generate_token + token = generate_token(email, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT']) + + return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token diff --git a/config.py b/config.py index 7237a2c07..dd6fa5652 100644 --- a/config.py +++ b/config.py @@ -48,6 +48,7 @@ class Config(object): Queue('email', Exchange('default'), routing_key='email'), Queue('sms-code', Exchange('default'), routing_key='sms-code'), Queue('email-code', Exchange('default'), routing_key='email-code'), + Queue('email-forgot-password', Exchange('default'), routing_key='email-forgot-password'), Queue('process-job', Exchange('default'), routing_key='process-job'), Queue('bulk-sms', Exchange('default'), routing_key='bulk-sms'), Queue('bulk-email', Exchange('default'), routing_key='bulk-email'), diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 6ee7c5391..0e2acef20 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -1,7 +1,13 @@ import uuid import pytest from flask import current_app -from app.celery.tasks import (send_sms, send_sms_code, send_email_code, send_email, process_job, email_invited_user) +from app.celery.tasks import (send_sms, + send_sms_code, + send_email_code, + send_email, + process_job, + email_invited_user, + email_reset_password) from app import (firetext_client, aws_ses_client, encryption) from app.clients.email.aws_ses import AwsSesClientException from app.clients.sms.firetext import FiretextClientException @@ -503,3 +509,20 @@ def test_email_invited_user_should_send_email(notify_api, mocker): invitation['to'], expected_subject, expected_content) + + +def test_email_reset_password_should_send_email(notify_api, mocker): + with notify_api.test_request_context(): + reset_password_message = {'to': 'someone@it.gov.uk', + 'reset_password_url': 'bah'} + + mocker.patch('app.aws_ses_client.send_email') + mocker.patch('app.encryption.decrypt', return_value=reset_password_message) + + encrypted_message = encryption.encrypt(reset_password_message) + email_reset_password(encrypted_message) + + aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'], + reset_password_message['to'], + "Reset password for GOV.UK Notify", + reset_password_message['reset_password_url']) diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index c014272a1..f1aa634a7 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -1,10 +1,12 @@ import json +import uuid from flask import url_for +import app from app.models import (User, Permission, MANAGE_SETTINGS, MANAGE_TEMPLATES) from app.dao.permissions_dao import default_service_permissions -from app import db +from app import db, encryption from tests import create_authorization_header @@ -256,7 +258,7 @@ def test_put_user_not_exists(notify_api, notify_db, notify_db_session, sample_us user = User.query.filter_by(id=sample_user.id).first() json_resp = json.loads(resp.get_data(as_text=True)) assert json_resp['result'] == "error" - assert json_resp['message'] == "User not found" + assert json_resp['message'] == "User not found for id: {}".format("9999") assert user == sample_user assert user.email_address != new_email @@ -426,3 +428,46 @@ def test_set_user_permissions_remove_old(notify_api, query = Permission.query.filter_by(user=sample_user) assert query.count() == 1 assert query.first().permission == MANAGE_SETTINGS + + +def test_send_reset_password_should_send_reset_password_link(notify_api, + sample_user, + mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch('app.celery.tasks.email_reset_password.apply_async') + auth_header = create_authorization_header( + path=url_for('user.send_reset_password', user_id=sample_user.id), + method='POST', + request_body={}) + resp = client.post( + url_for('user.send_reset_password', user_id=sample_user.id), + data={}, + headers=[('Content-Type', 'application/json'), auth_header]) + + assert resp.status_code == 204 + from app.user.rest import _create_reset_password_url + url = _create_reset_password_url(sample_user.email_address) + encrypted = encryption.encrypt({'to': sample_user.email_address, 'reset_password_url': url}) + app.celery.tasks.email_reset_password.apply_async.assert_called_once_with([encrypted], + queue='send-reset-password') + + +def test_send_reset_password_should_return_404_when_user_doesnot_exist(notify_api, + sample_user, + mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + user_id = 99999 + auth_header = create_authorization_header( + path=url_for('user.send_reset_password', user_id=user_id), + method='POST', + request_body={}) + + resp = client.post( + url_for('user.send_reset_password', user_id=user_id), + data={}, + headers=[('Content-Type', 'application/json'), auth_header]) + + assert resp.status_code == 404 + assert json.loads(resp.get_data(as_text=True))['message'] == 'User not found for id: {}'.format(user_id) diff --git a/tests/app/user/test_rest_verify.py b/tests/app/user/test_rest_verify.py index 10310627c..a337ffa5e 100644 --- a/tests/app/user/test_rest_verify.py +++ b/tests/app/user/test_rest_verify.py @@ -9,8 +9,6 @@ from tests import create_authorization_header def test_user_verify_code_sms(notify_api, - notify_db, - notify_db_session, sample_sms_code): """ Tests POST endpoint '//verify/code' @@ -34,8 +32,6 @@ def test_user_verify_code_sms(notify_api, def test_user_verify_code_sms_missing_code(notify_api, - notify_db, - notify_db_session, sample_sms_code): """ Tests POST endpoint '//verify/code' @@ -58,8 +54,6 @@ def test_user_verify_code_sms_missing_code(notify_api, @moto.mock_sqs def test_user_verify_code_email(notify_api, - notify_db, - notify_db_session, sqs_client_conn, sample_email_code): """ @@ -84,8 +78,6 @@ def test_user_verify_code_email(notify_api, def test_user_verify_code_email_bad_code(notify_api, - notify_db, - notify_db_session, sample_email_code): """ Tests POST endpoint '//verify/code' @@ -109,8 +101,6 @@ def test_user_verify_code_email_bad_code(notify_api, def test_user_verify_code_email_expired_code(notify_api, - notify_db, - notify_db_session, sample_email_code): """ Tests POST endpoint '//verify/code' @@ -159,8 +149,6 @@ def test_user_verify_password(notify_api, def test_user_verify_password_invalid_password(notify_api, - notify_db, - notify_db_session, sample_user): """ Tests POST endpoint '//verify/password' invalid endpoint. @@ -186,8 +174,6 @@ def test_user_verify_password_invalid_password(notify_api, def test_user_verify_password_valid_password_resets_failed_logins(notify_api, - notify_db, - notify_db_session, sample_user): with notify_api.test_request_context(): with notify_api.test_client() as client: @@ -224,8 +210,6 @@ def test_user_verify_password_valid_password_resets_failed_logins(notify_api, def test_user_verify_password_missing_password(notify_api, - notify_db, - notify_db_session, sample_user): """ Tests POST endpoint '//verify/password' missing password. @@ -311,7 +295,7 @@ def test_send_sms_code_returns_404_for_bad_input_data(notify_api, notify_db, not data=data, headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 404 - assert json.loads(resp.get_data(as_text=True))['message'] == 'No user found' + assert json.loads(resp.get_data(as_text=True))['message'] == 'User not found for id: {}'.format(int(uuid_)) def test_send_user_email_code(notify_api, @@ -353,4 +337,4 @@ def test_send_user_email_code_returns_404_for_when_user_does_not_exist(notify_ap data=data, headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 404 - assert json.loads(resp.get_data(as_text=True))['message'] == 'No user found' + assert json.loads(resp.get_data(as_text=True))['message'] == 'User not found for id: {}'.format(1)