diff --git a/README.md b/README.md index 690625b41..2951f77b8 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,24 @@ 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_EXPIRATION_DAYS=2 +export NOTIFY_EMAIL_DOMAIN='dev.notify.com' 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..deb62a6ab 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -156,3 +156,33 @@ 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('You are invited to use GOV.UK Notify by $user_name for service $service_name.' + ' The url to join is $url. This url will expire on $expiry_date') + return t.substitute(user_name=user_name, service_name=service_name, url=url, expiry_date=expiry_date) + + +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: + aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'], + invitation['to'], + 'Invitation to GOV.UK Notify', + 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..44a5c13cc 100644 --- a/app/invite/rest.py +++ b/app/invite/rest.py @@ -1,8 +1,10 @@ +from datetime import timedelta + from flask import ( Blueprint, request, - jsonify -) + jsonify, + current_app) from app.dao.invited_user_dao import ( save_invited_user, @@ -11,6 +13,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 +27,8 @@ 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) + email_invited_user.apply_async(encrypted_invitation=invitation, queue_name='email-invited-user') return jsonify(data=invited_user_schema.dump(invited_user).data), 201 @@ -41,3 +46,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': 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..34f045bcc 100644 --- a/config.py +++ b/config.py @@ -3,12 +3,14 @@ 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']) 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..e3e80423f 100644 --- a/environment_test.sh +++ b/environment_test.sh @@ -1,11 +1,13 @@ #!/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_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/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 0c25826d5..b2dd82300 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,29 @@ 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)) + + aws_ses_client.send_email.assert_called_once_with(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'], + invitation['to'], + 'Invitation to GOV.UK Notify', + expected_content) diff --git a/tests/app/invite/test_invite_rest.py b/tests/app/invite/test_invite_rest.py index e0214fcb1..fcdd13c5f 100644 --- a/tests/app/invite/test_invite_rest.py +++ b/tests/app/invite/test_invite_rest.py @@ -1,13 +1,17 @@ import json import uuid +from datetime import datetime, timedelta + 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] @@ -37,12 +41,26 @@ def test_create_invited_user(notify_api, sample_service): assert json_resp['data']['email_address'] == email_address assert json_resp['data']['from_user'] == invite_from.id 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': expiry_date + } + app.celery.tasks.email_invited_user.apply_async.assert_called_once_with( + encrypted_invitation=encrypted_invitation, + queue_name='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] @@ -69,6 +87,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):