diff --git a/README.md b/README.md index 690625b41..171d96da9 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,25 @@ Create a local environment.sh file containing the following: ``` echo " export NOTIFY_API_ENVIRONMENT='config.Development' +export ADMIN_BASE_URL='http://localhost:6012' export ADMIN_CLIENT_SECRET='dev-notify-secret-key' export ADMIN_CLIENT_USER_NAME='dev-notify-admin' export AWS_REGION='eu-west-1' export DANGEROUS_SALT='dev-notify-salt' export DELIVERY_CLIENT_USER_NAME='dev-notify-delivery' export DELIVERY_CLIENT_SECRET='dev-notify-secret-key' -export FIRETEXT_API_KEY="secret-fire-text" # This has to be real so speak to a grownup +export FIRETEXT_API_KEY=[contact team member for api key] export FIRETEXT_NUMBER="Firetext" +export INVITATION_EMAIL_FROM='invites@notifications.service.gov.uk' +export INVITATION_EXPIRATION_DAYS=2 +export NOTIFY_EMAIL_DOMAIN='dev.notify.works' export NOTIFY_JOB_QUEUE='[unique-to-environment]-notify-jobs-queue' # NOTE unique prefix export NOTIFICATION_QUEUE_PREFIX='[unique-to-environment]-notification_development' # NOTE unique prefix export SECRET_KEY='dev-notify-secret-key' export SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notification_api' +export TWILIO_ACCOUNT_SID=[contact team member for account sid] +export TWILIO_AUTH_TOKEN=[contact team member for auth token] export VERIFY_CODE_FROM_EMAIL_ADDRESS='no-reply@notify.works' -export NOTIFY_EMAIL_DOMAIN='dev.notify.com' "> environment.sh ``` diff --git a/app/celery/tasks.py b/app/celery/tasks.py index d4913e24d..c9f55126b 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -156,3 +156,45 @@ def send_email_code(encrypted_verification_message): verification_message['secret_code']) except AwsSesClientException as e: current_app.logger.error(e) + + +# TODO: when placeholders in templates work, this will be a real template +def invitation_template(user_name, service_name, url, expiry_date): + from string import Template + t = Template( + '$user_name has invited you to collaborate on $service_name on GOV.UK Notify.\n\n' + 'GOV.UK Notify makes it easy to keep people updated by helping you send text messages, emails and letters.\n\n' + 'Click this link to create an account on GOV.UK Notify:\n$url\n\n' + 'This invitation will stop working at midnight tomorrow. This is to keep $service_name secure.') + return t.substitute(user_name=user_name, service_name=service_name, url=url, expiry_date=expiry_date) + + +def invitation_subject_line(user_name, service_name): + from string import Template + t = Template('$user_name has invited you to collaborate on $service_name on GOV.UK Notify') + return t.substitute(user_name=user_name, service_name=service_name) + + +def invited_user_url(base_url, token): + return '{0}/invitation/{1}'.format(base_url, token) + + +@notify_celery.task(name='email-invited-user') +def email_invited_user(encrypted_invitation): + invitation = encryption.decrypt(encrypted_invitation) + url = invited_user_url(current_app.config['ADMIN_BASE_URL'], + invitation['token']) + invitation_content = invitation_template(invitation['user_name'], + invitation['service_name'], + url, + invitation['expiry_date']) + try: + email_from = "{}@{}".format(current_app.config['INVITATION_EMAIL_FROM'], + current_app.config['NOTIFY_EMAIL_DOMAIN']) + subject_line = invitation_subject_line(invitation['user_name'], invitation['service_name']) + aws_ses_client.send_email(email_from, + invitation['to'], + subject_line, + invitation_content) + except AwsSesClientException as e: + current_app.logger.error(e) diff --git a/app/invite/rest.py b/app/invite/rest.py index ec295da46..80c39f177 100644 --- a/app/invite/rest.py +++ b/app/invite/rest.py @@ -1,9 +1,12 @@ +from datetime import timedelta + from flask import ( Blueprint, request, - jsonify -) + jsonify, + current_app) +from app import encryption from app.dao.invited_user_dao import ( save_invited_user, get_invited_user, @@ -11,6 +14,7 @@ from app.dao.invited_user_dao import ( ) from app.schemas import invited_user_schema +from app.celery.tasks import (email_invited_user) invite = Blueprint('invite', __name__, url_prefix='/service//invite') @@ -24,6 +28,9 @@ def create_invited_user(service_id): if errors: return jsonify(result="error", message=errors), 400 save_invited_user(invited_user) + invitation = _create_invitation(invited_user) + encrypted_invitation = encryption.encrypt(invitation) + email_invited_user.apply_async([encrypted_invitation], queue='email-invited-user') return jsonify(data=invited_user_schema.dump(invited_user).data), 201 @@ -41,3 +48,21 @@ def get_invited_user_by_service_and_id(service_id, invited_user_id): invited_user_id) return jsonify(result='error', message=message), 404 return jsonify(data=invited_user_schema.dump(invited_user).data), 200 + + +def _create_invitation(invited_user): + from utils.url_safe_token import generate_token + token = generate_token(str(invited_user.id), current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT']) + # TODO: confirm what we want to do for this - the idea is that we say expires tomorrow at midnight + # and give 48 hours as the max_age + expiration_date = (invited_user.created_at + timedelta(days=current_app.config['INVITATION_EXPIRATION_DAYS'])) \ + .replace(hour=0, minute=0, second=0, microsecond=0) + + invitation = {'to': invited_user.email_address, + 'user_name': invited_user.from_user.name, + 'service_id': str(invited_user.service_id), + 'service_name': invited_user.service.name, + 'token': token, + 'expiry_date': str(expiration_date) + } + return invitation diff --git a/app/service/rest.py b/app/service/rest.py index 7c52acbe3..827a1e18e 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -93,7 +93,7 @@ def update_service(service_id): current_data = dict(service_schema.dump(fetched_service).data.items()) current_data.update(request.get_json()) - + print(current_data) update_dict, errors = service_schema.load(current_data) if errors: return jsonify(result="error", message=errors), 400 diff --git a/config.py b/config.py index 000112235..06495ecbb 100644 --- a/config.py +++ b/config.py @@ -3,12 +3,15 @@ import os class Config(object): DEBUG = False + ADMIN_BASE_URL = os.environ['ADMIN_BASE_URL'] ADMIN_CLIENT_USER_NAME = os.environ['ADMIN_CLIENT_USER_NAME'] ADMIN_CLIENT_SECRET = os.environ['ADMIN_CLIENT_SECRET'] AWS_REGION = os.environ['AWS_REGION'] DANGEROUS_SALT = os.environ['DANGEROUS_SALT'] DELIVERY_CLIENT_USER_NAME = os.environ['DELIVERY_CLIENT_USER_NAME'] DELIVERY_CLIENT_SECRET = os.environ['DELIVERY_CLIENT_SECRET'] + INVITATION_EXPIRATION_DAYS = int(os.environ['INVITATION_EXPIRATION_DAYS']) + INVITATION_EMAIL_FROM = os.environ['INVITATION_EMAIL_FROM'] NOTIFY_APP_NAME = 'api' NOTIFY_LOG_PATH = '/var/log/notify/application.log' NOTIFY_JOB_QUEUE = os.environ['NOTIFY_JOB_QUEUE'] diff --git a/environment_test.sh b/environment_test.sh index 815a6281b..73d03a94a 100644 --- a/environment_test.sh +++ b/environment_test.sh @@ -1,11 +1,14 @@ #!/bin/bash export NOTIFY_API_ENVIRONMENT='config.Test' +export ADMIN_BASE_URL='http://localhost:6012' export ADMIN_CLIENT_USER_NAME='dev-notify-admin' export ADMIN_CLIENT_SECRET='dev-notify-secret-key' export AWS_REGION='eu-west-1' export DANGEROUS_SALT='dangerous-salt' export DELIVERY_CLIENT_USER_NAME='dev-notify-delivery' export DELIVERY_CLIENT_SECRET='dev-notify-secret-key' +export INVITATION_EMAIL_FROM='invites' +export INVITATION_EXPIRATION_DAYS=2 export NOTIFY_JOB_QUEUE='notify-jobs-queue-test' export NOTIFICATION_QUEUE_PREFIX='notification_development-test' export SECRET_KEY='secret-key' diff --git a/requirements.txt b/requirements.txt index cca101658..1d835f1df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ PyJWT==1.4.0 marshmallow==2.4.2 marshmallow-sqlalchemy==0.8.0 flask-marshmallow==0.6.2 -itsdangerous==0.24 Flask-Bcrypt==0.6.2 credstash==1.8.0 boto3==1.2.3 @@ -20,4 +19,4 @@ twilio==4.6.0 git+https://github.com/alphagov/notifications-python-client.git@0.2.6#egg=notifications-python-client==0.2.6 -git+https://github.com/alphagov/notifications-utils.git@0.0.3#egg=notifications-utils==0.0.3 +git+https://github.com/alphagov/notifications-utils.git@0.2.1#egg=notifications-utils==0.2.1 diff --git a/scripts/run_celery.sh b/scripts/run_celery.sh index f8c7e674b..1d2abbc8b 100755 --- a/scripts/run_celery.sh +++ b/scripts/run_celery.sh @@ -3,4 +3,4 @@ set -e source environment.sh -celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=4 -Q sms,sms-code,email-code,email,process-job,bulk-sms,bulk-email +celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=4 -Q sms,sms-code,email-code,email,process-job,bulk-sms,bulk-email,email-invited-user diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 0c25826d5..4b094344f 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -1,7 +1,7 @@ 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) +from app.celery.tasks import (send_sms, send_sms_code, send_email_code, send_email, process_job, email_invited_user) from app import (firetext_client, aws_ses_client, encryption) from app.clients.email.aws_ses import AwsSesClientException from app.clients.sms.firetext import FiretextClientException @@ -11,7 +11,7 @@ from sqlalchemy.orm.exc import NoResultFound from app.celery.tasks import s3 from app.celery import tasks from tests.app import load_example_csv -from datetime import datetime +from datetime import datetime, timedelta from freezegun import freeze_time @@ -332,3 +332,31 @@ def test_should_send_email_code(mocker): "Verification code", verification['secret_code'] ) + + +def test_email_invited_user_should_send_email(notify_api, mocker): + with notify_api.test_request_context(): + invitation = {'to': 'new_person@it.gov.uk', + 'user_name': 'John Smith', + 'service_id': '123123', + 'service_name': 'Blacksmith Service', + 'token': 'the-token', + 'expiry_date': str(datetime.now() + timedelta(days=1)) + } + + mocker.patch('app.aws_ses_client.send_email') + mocker.patch('app.encryption.decrypt', return_value=invitation) + url = tasks.invited_user_url(current_app.config['ADMIN_BASE_URL'], invitation['token']) + expected_content = tasks.invitation_template(invitation['user_name'], + invitation['service_name'], + url, + invitation['expiry_date']) + + email_invited_user(encryption.encrypt(invitation)) + email_from = "{}@{}".format(current_app.config['INVITATION_EMAIL_FROM'], + current_app.config['NOTIFY_EMAIL_DOMAIN']) + expected_subject = tasks.invitation_subject_line(invitation['user_name'], invitation['service_name']) + aws_ses_client.send_email.assert_called_once_with(email_from, + invitation['to'], + expected_subject, + expected_content) diff --git a/tests/app/invite/test_invite_rest.py b/tests/app/invite/test_invite_rest.py index 5e1c6f1e8..8d8783899 100644 --- a/tests/app/invite/test_invite_rest.py +++ b/tests/app/invite/test_invite_rest.py @@ -1,13 +1,18 @@ import json import uuid +from datetime import datetime, timedelta + +from app import encryption from tests import create_authorization_header +import app.celery.tasks -def test_create_invited_user(notify_api, sample_service): +def test_create_invited_user(notify_api, sample_service, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - + mocker.patch('app.celery.tasks.email_invited_user.apply_async') + mocker.patch('utils.url_safe_token.generate_token', return_value='the-token') email_address = 'invited_user@service.gov.uk' invite_from = sample_service.users[0] @@ -39,12 +44,26 @@ def test_create_invited_user(notify_api, sample_service): assert json_resp['data']['from_user'] == invite_from.id assert json_resp['data']['permissions'] == 'send_messages,manage_service,manage_api_keys' assert json_resp['data']['id'] + invitation_expiration_days = notify_api.config['INVITATION_EXPIRATION_DAYS'] + expiry_date = (datetime.now() + timedelta(days=invitation_expiration_days)).replace(hour=0, minute=0, + second=0, + microsecond=0) + encrypted_invitation = {'to': email_address, + 'user_name': invite_from.name, + 'service_id': str(sample_service.id), + 'service_name': sample_service.name, + 'token': 'the-token', + 'expiry_date': str(expiry_date) + } + app.celery.tasks.email_invited_user.apply_async.assert_called_once_with( + [encryption.encrypt(encrypted_invitation)], + queue='email-invited-user') -def test_create_invited_user_invalid_email(notify_api, sample_service): +def test_create_invited_user_invalid_email(notify_api, sample_service, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - + mocker.patch('app.celery.tasks.email_invited_user.apply_async') email_address = 'notanemail' invite_from = sample_service.users[0] @@ -72,6 +91,7 @@ def test_create_invited_user_invalid_email(notify_api, sample_service): json_resp = json.loads(response.get_data(as_text=True)) assert json_resp['result'] == 'error' assert json_resp['message'] == {'email_address': ['Invalid email']} + app.celery.tasks.email_invited_user.apply_async.assert_not_called() def test_get_all_invited_users_by_service(notify_api, notify_db, notify_db_session, sample_service):