Merge branch 'master' into rate-limit

Conflicts:
	app/celery/tasks.py
	tests/app/celery/test_tasks.py
This commit is contained in:
Martyn Inglis
2016-03-09 14:16:59 +00:00
15 changed files with 247 additions and 90 deletions

View File

@@ -16,7 +16,7 @@ from sqlalchemy.exc import SQLAlchemyError
from app.aws import s3
from datetime import datetime
from utils.template import Template
from utils.recipients import RecipientCSV, first_column_heading
from utils.recipients import RecipientCSV
@notify_celery.task(name="process-job")
@@ -47,19 +47,24 @@ def process_job(job_id):
job.status = 'in progress'
dao_update_job(job)
template = Template(
dao_get_template_by_id(job.template_id).__dict__
)
for recipient, personalisation in RecipientCSV(
s3.get_job_from_s3(job.bucket_name, job_id),
template_type=job.template.template_type
s3.get_job_from_s3(job.bucket_name, job_id),
template_type=template.template_type,
placeholders=template.placeholders
).recipients_and_personalisation:
encrypted = encryption.encrypt({
'template': job.template_id,
'template': template.id,
'job': str(job.id),
'to': recipient,
'personalisation': personalisation
})
if job.template.template_type == 'sms':
if template.template_type == 'sms':
send_sms.apply_async((
str(job.service_id),
str(create_uuid()),
@@ -68,11 +73,11 @@ def process_job(job_id):
queue='bulk-sms'
)
if job.template.template_type == 'email':
if template.template_type == 'email':
send_email.apply_async((
str(job.service_id),
str(create_uuid()),
job.template.subject,
template.subject,
"{}@{}".format(job.service.email_from, current_app.config['NOTIFY_EMAIL_DOMAIN']),
encrypted,
datetime.utcnow().strftime(DATETIME_FORMAT)),
@@ -126,8 +131,7 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
template = Template(
dao_get_template_by_id(notification['template']).__dict__,
values=notification.get('personalisation', {}),
prefix=service.name,
drop_values={first_column_heading['sms']}
prefix=service.name
)
client.send_sms(
@@ -194,8 +198,7 @@ def send_email(service_id, notification_id, subject, from_address, encrypted_not
try:
template = Template(
dao_get_template_by_id(notification['template']).__dict__,
values=notification.get('personalisation', {}),
drop_values={first_column_heading['email']}
values=notification.get('personalisation', {})
)
client.send_email(
@@ -222,7 +225,7 @@ def send_sms_code(encrypted_verification):
try:
firetext_client.send_sms(verification_message['to'], verification_message['secret_code'])
except FiretextClientException as e:
current_app.logger.error(e)
current_app.logger.exception(e)
@notify_celery.task(name='send-email-code')
@@ -234,7 +237,7 @@ def send_email_code(encrypted_verification_message):
"Verification code",
verification_message['secret_code'])
except AwsSesClientException as e:
current_app.logger.error(e)
current_app.logger.exception(e)
# TODO: when placeholders in templates work, this will be a real template
@@ -276,4 +279,27 @@ def email_invited_user(encrypted_invitation):
subject_line,
invitation_content)
except AwsSesClientException as e:
current_app.logger.error(e)
current_app.logger.exception(e)
def password_reset_message(name, url):
from string import Template
t = Template("Hi $user_name,\n\n"
"We received a request to reset your password on GOV.UK Notify.\n\n"
"If you didn't request this email, you can ignore it your password has not been changed.\n\n"
"To reset your password, click this link:\n\n"
"$url")
return t.substitute(user_name=name, url=url)
@notify_celery.task(name='email-reset-password')
def email_reset_password(encrypted_reset_password_message):
reset_password_message = encryption.decrypt(encrypted_reset_password_message)
try:
aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'],
reset_password_message['to'],
"Reset your GOV.UK Notify password",
password_reset_message(name=reset_password_message['name'],
url=reset_password_message['reset_password_url']))
except AwsSesClientException as e:
current_app.logger.exception(e)

View File

@@ -16,6 +16,7 @@ def save_model_user(usr, update_dict={}, pwd=None):
if update_dict:
if update_dict.get('id'):
del update_dict['id']
update_dict.pop('password_changed_at')
db.session.query(User).filter_by(id=usr.id).update(update_dict)
else:
db.session.add(usr)

View File

@@ -41,7 +41,7 @@ def register_errors(blueprint):
@blueprint.app_errorhandler(500)
def internal_server_error(e):
if isinstance(e, str):
current_app.logger.error(e)
current_app.logger.exception(e)
elif isinstance(e, Exception):
current_app.logger.exception(e)
return jsonify(result='error', message="Internal server error"), 500

View File

@@ -1,10 +1,8 @@
import re
from flask import current_app
from flask_marshmallow.fields import fields
from . import ma
from . import models
from app.dao.permissions_dao import permission_dao
from marshmallow import (post_load, ValidationError, validates, validates_schema)
from marshmallow import (post_load, ValidationError, validates)
from marshmallow_sqlalchemy import field_for
from utils.recipients import (
validate_email_address, InvalidEmailError,
@@ -50,6 +48,8 @@ class BaseSchema(ma.ModelSchema):
class UserSchema(BaseSchema):
permissions = fields.Method("user_permissions", dump_only=True)
password_changed_at = field_for(models.User, 'password_changed_at', format='%Y-%m-%d %H:%M:%S.%f')
created_at = field_for(models.User, 'created_at', format='%Y-%m-%d %H:%M:%S.%f')
def user_permissions(self, usr):
retval = {}
@@ -95,18 +95,6 @@ class JobSchema(BaseSchema):
model = models.Job
# TODO: Remove this schema once the admin app has stopped using the /user/<user_id>code endpoint
class OldRequestVerifyCodeSchema(ma.Schema):
code_type = fields.Str(required=True)
to = fields.Str(required=False)
@validates('code_type')
def validate_code_type(self, code):
if code not in models.VERIFY_CODE_TYPES:
raise ValidationError('Invalid code type')
class RequestVerifyCodeSchema(ma.Schema):
to = fields.Str(required=False)
@@ -142,8 +130,8 @@ class EmailNotificationSchema(NotificationSchema):
def validate_to(self, value):
try:
validate_email_address(value)
except InvalidEmailError:
raise ValidationError('Invalid email')
except InvalidEmailError as e:
raise ValidationError(e.message)
class SmsTemplateNotificationSchema(SmsNotificationSchema):
@@ -180,8 +168,8 @@ class InvitedUserSchema(BaseSchema):
def validate_to(self, value):
try:
validate_email_address(value)
except InvalidEmailError:
raise ValidationError('Invalid email')
except InvalidEmailError as e:
raise ValidationError(e.message)
class PermissionSchema(BaseSchema):
@@ -201,6 +189,16 @@ class PermissionSchema(BaseSchema):
exclude = ("created_at",)
class EmailDataSchema(ma.Schema):
email = fields.Str(required=False)
@validates('email')
def validate_email(self, value):
try:
validate_email_address(value)
except InvalidEmailError as e:
raise ValidationError(e.message)
user_schema = UserSchema()
user_schema_load_json = UserSchema(load_json=True)
service_schema = ServiceSchema()
@@ -211,7 +209,6 @@ api_key_schema = ApiKeySchema()
api_key_schema_load_json = ApiKeySchema(load_json=True)
job_schema = JobSchema()
job_schema_load_json = JobSchema(load_json=True)
old_request_verify_code_schema = OldRequestVerifyCodeSchema()
request_verify_code_schema = RequestVerifyCodeSchema()
sms_admin_notification_schema = SmsAdminNotificationSchema()
sms_template_notification_schema = SmsTemplateNotificationSchema()
@@ -222,4 +219,5 @@ notification_status_schema = NotificationStatusSchema()
notification_status_schema_load_json = NotificationStatusSchema(load_json=True)
invited_user_schema = InvitedUserSchema()
permission_schema = PermissionSchema()
email_data_request_schema = EmailDataSchema()
notifications_statistics_schema = NotificationsStatisticsSchema()

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from flask import (jsonify, request, abort, Blueprint)
from flask import (jsonify, request, abort, Blueprint, current_app)
from app import encryption
from app.dao.users_dao import (
@@ -17,14 +17,14 @@ from app.dao.permissions_dao import permission_dao
from app.dao.services_dao import dao_fetch_service_by_id
from app.schemas import (
old_request_verify_code_schema,
email_data_request_schema,
user_schema,
request_verify_code_schema,
user_schema_load_json,
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)
@@ -118,7 +118,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:
@@ -140,7 +140,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:
@@ -174,7 +174,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))
@@ -193,9 +193,45 @@ def get_by_email():
email = request.args.get('email')
if not email:
return jsonify(result="error", message="invalid request"), 400
user = get_user_by_email(email)
if not user:
return jsonify(result="error", message="not found"), 404
result = user_schema.dump(user)
fetched_user = get_user_by_email(email)
if not fetched_user:
return _user_not_found_for_email()
result = user_schema.dump(fetched_user)
return jsonify(data=result.data)
@user.route('/reset-password', methods=['POST'])
def send_user_reset_password():
email, errors = email_data_request_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
user_to_send_to = get_user_by_email(email['email'])
if not user_to_send_to:
return _user_not_found_for_email()
reset_password_message = {'to': user_to_send_to.email_address,
'name': user_to_send_to.name,
'reset_password_url': _create_reset_password_url(user_to_send_to.email_address)}
email_reset_password.apply_async([encryption.encrypt(reset_password_message)], queue='email-reset-password')
return jsonify({}), 204
def _user_not_found(user_id):
return abort(404, 'User not found for id: {}'.format(user_id))
def _user_not_found_for_email():
return abort(404, 'User not found for email address')
def _create_reset_password_url(email):
from utils.url_safe_token import generate_token
import json
data = json.dumps({'email': email, 'created_at': str(datetime.now())})
token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'])
return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token