Merge pull request #110 from alphagov/invitation

Email invitation to user
This commit is contained in:
Adam Shimali
2016-02-29 16:25:04 +00:00
10 changed files with 139 additions and 14 deletions

View File

@@ -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
```

View File

@@ -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)

View File

@@ -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/<service_id>/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

View File

@@ -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

View File

@@ -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']

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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):