Merge branch 'master' into capture-delivery-outcomes

This commit is contained in:
Martyn Inglis
2016-03-18 15:35:51 +00:00
27 changed files with 550 additions and 92 deletions

View File

@@ -27,11 +27,14 @@ encryption = Encryption()
api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user)
def create_app():
def create_app(app_name=None):
application = Flask(__name__)
application.config.from_object(os.environ['NOTIFY_API_ENVIRONMENT'])
if app_name:
application.config['NOTIFY_APP_NAME'] = app_name
init_app(application)
db.init_app(application)
ma.init_app(application)
@@ -92,9 +95,7 @@ def init_app(app):
def email_safe(string):
return "".join([
character.lower()
if character.isalnum() or character == "."
else "" for character in re.sub("\s+", ".", string.strip())
character.lower() if character.isalnum() or character == "." else "" for character in re.sub("\s+", ".", string.strip()) # noqa
])

View File

@@ -25,7 +25,8 @@ from sqlalchemy.exc import SQLAlchemyError
from app.aws import s3
from datetime import datetime
from utils.template import Template
from utils.recipients import RecipientCSV, validate_phone_number, format_phone_number
from utils.recipients import RecipientCSV, format_phone_number, validate_phone_number
from app.validation import (allowed_send_to_email, allowed_send_to_number)
@notify_celery.task(name="delete-verify-codes")
@@ -204,7 +205,7 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
)
client.send_sms(
to=notification['to'],
to=format_phone_number(validate_phone_number(notification['to'])),
content=template.replaced,
reference=str(notification_id)
)
@@ -223,20 +224,6 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
current_app.logger.debug(e)
def allowed_send_to_number(service, to):
if service.restricted and format_phone_number(validate_phone_number(to)) not in [
format_phone_number(validate_phone_number(user.mobile_number)) for user in service.users
]:
return False
return True
def allowed_send_to_email(service, to):
if service.restricted and to not in [user.email_address for user in service.users]:
return False
return True
@notify_celery.task(name="send-email")
def send_email(service_id, notification_id, subject, from_address, encrypted_notification, created_at):
notification = encryption.decrypt(encrypted_notification)
@@ -300,7 +287,9 @@ def send_sms_code(encrypted_verification):
verification_message = encryption.decrypt(encrypted_verification)
try:
firetext_client.send_sms(
verification_message['to'], verification_message['secret_code'], 'send-sms-code'
format_phone_number(validate_phone_number(verification_message['to'])),
verification_message['secret_code'],
'send-sms-code'
)
except FiretextClientException as e:
current_app.logger.exception(e)
@@ -381,3 +370,23 @@ def email_reset_password(encrypted_reset_password_message):
url=reset_password_message['reset_password_url']))
except AwsSesClientException as e:
current_app.logger.exception(e)
def registration_verification_template(name, url):
from string import Template
t = Template("Hi $name,\n\n"
"To complete your registration for GOV.UK Notify please click the link below\n\n $url")
return t.substitute(name=name, url=url)
@notify_celery.task(name='email-registration-verification')
def email_registration_verification(encrypted_verification_message):
verification_message = encryption.decrypt(encrypted_verification_message)
try:
aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'],
verification_message['to'],
"Confirm GOV.UK Notify registration",
registration_verification_template(name=verification_message['name'],
url=verification_message['url']))
except AwsSesClientException as e:
current_app.logger.exception(e)

View File

@@ -40,6 +40,7 @@ class User(db.Model):
logged_in_at = db.Column(db.DateTime, nullable=True)
failed_login_count = db.Column(db.Integer, nullable=False, default=0)
state = db.Column(db.String, nullable=False, default='pending')
platform_admin = db.Column(db.Boolean, nullable=False, default=False)
@property
def password(self):
@@ -114,12 +115,12 @@ class NotificationStatistics(db.Model):
day = db.Column(db.String(255), nullable=False)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False)
service = db.relationship('Service', backref=db.backref('service_notification_stats', lazy='dynamic'))
emails_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
emails_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
emails_error = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
sms_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
sms_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
sms_error = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
emails_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
emails_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
emails_error = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
sms_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
sms_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
sms_error = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
__table_args__ = (
UniqueConstraint('service_id', 'day', name='uix_service_to_day'),
@@ -144,13 +145,13 @@ class Template(db.Model):
index=False,
unique=False,
nullable=False,
default=datetime.datetime.now)
default=datetime.datetime.utcnow)
updated_at = db.Column(
db.DateTime,
index=False,
unique=False,
nullable=True,
onupdate=datetime.datetime.utcnow())
onupdate=datetime.datetime.utcnow)
content = db.Column(db.Text, index=False, unique=False, nullable=False)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False, nullable=False)
service = db.relationship('Service', backref=db.backref('templates', lazy='dynamic'))
@@ -176,13 +177,13 @@ class Job(db.Model):
index=False,
unique=False,
nullable=False,
default=datetime.datetime.utcnow())
default=datetime.datetime.utcnow)
updated_at = db.Column(
db.DateTime,
index=False,
unique=False,
nullable=True,
onupdate=datetime.datetime.utcnow())
onupdate=datetime.datetime.utcnow)
status = db.Column(db.Enum(*JOB_STATUS_TYPES, name='job_status_types'), nullable=False, default='pending')
notification_count = db.Column(db.Integer, nullable=False)
notifications_sent = db.Column(db.Integer, nullable=False, default=0)
@@ -217,7 +218,7 @@ class VerifyCode(db.Model):
index=False,
unique=False,
nullable=False,
default=datetime.datetime.utcnow())
default=datetime.datetime.utcnow)
@property
def code(self):
@@ -262,7 +263,7 @@ class Notification(db.Model):
index=False,
unique=False,
nullable=True,
onupdate=datetime.datetime.utcnow())
onupdate=datetime.datetime.utcnow)
status = db.Column(
db.Enum(*NOTIFICATION_STATUS_TYPES, name='notification_status_types'), nullable=False, default='sent')
reference = db.Column(db.String, nullable=True, index=True)
@@ -286,7 +287,7 @@ class InvitedUser(db.Model):
index=False,
unique=False,
nullable=False,
default=datetime.datetime.utcnow())
default=datetime.datetime.utcnow)
status = db.Column(
db.Enum(*INVITED_USER_STATUS_TYPES, name='invited_users_status_types'), nullable=False, default='pending')
permissions = db.Column(db.String, nullable=False)
@@ -306,6 +307,7 @@ SEND_EMAILS = 'send_emails'
SEND_LETTERS = 'send_letters'
MANAGE_API_KEYS = 'manage_api_keys'
ACCESS_DEVELOPER_DOCS = 'access_developer_docs'
PLATFORM_ADMIN = 'platform_admin'
# List of permissions
PERMISSION_LIST = [
@@ -316,7 +318,8 @@ PERMISSION_LIST = [
SEND_EMAILS,
SEND_LETTERS,
MANAGE_API_KEYS,
ACCESS_DEVELOPER_DOCS]
ACCESS_DEVELOPER_DOCS,
PLATFORM_ADMIN]
class Permission(db.Model):
@@ -338,7 +341,7 @@ class Permission(db.Model):
index=False,
unique=False,
nullable=False,
default=datetime.datetime.utcnow())
default=datetime.datetime.utcnow)
__table_args__ = (
UniqueConstraint('service_id', 'user_id', 'permission', name='uix_service_user_permission'),

View File

@@ -26,6 +26,7 @@ from app.schemas import (
notification_status_schema
)
from app.celery.tasks import send_sms, send_email
from app.validation import allowed_send_to_number, allowed_send_to_email
notifications = Blueprint('notifications', __name__)
@@ -193,13 +194,12 @@ def get_all_notifications():
return jsonify(result="error", message="Invalid page"), 400
all_notifications = notifications_dao.get_notifications_for_service(api_user['client'], page)
return jsonify(
notifications=notification_status_schema.dump(all_notifications.items, many=True).data,
links=pagination_links(
all_notifications,
'.get_all_notifications',
request.args
**request.args.to_dict()
)
), 200
@@ -213,13 +213,14 @@ def get_all_notifications_for_service(service_id):
return jsonify(result="error", message="Invalid page"), 400
all_notifications = notifications_dao.get_notifications_for_service(service_id, page)
kwargs = request.args.to_dict()
kwargs['service_id'] = service_id
return jsonify(
notifications=notification_status_schema.dump(all_notifications.items, many=True).data,
links=pagination_links(
all_notifications,
'.get_all_notifications_for_service',
request.args
**kwargs
)
), 200
@@ -233,13 +234,15 @@ def get_all_notifications_for_service_job(service_id, job_id):
return jsonify(result="error", message="Invalid page"), 400
all_notifications = notifications_dao.get_notifications_for_job(service_id, job_id, page)
kwargs = request.args.to_dict()
kwargs['service_id'] = service_id
kwargs['job_id'] = job_id
return jsonify(
notifications=notification_status_schema.dump(all_notifications.items, many=True).data,
links=pagination_links(
all_notifications,
'.get_all_notifications_for_service_job',
request.args
**kwargs
)
), 200
@@ -255,13 +258,15 @@ def get_page_from_request():
return 1
def pagination_links(pagination, endpoint, args):
def pagination_links(pagination, endpoint, **kwargs):
if 'page' in kwargs:
kwargs.pop('page', None)
links = dict()
if pagination.has_prev:
links['prev'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.prev_num)]))
links['prev'] = url_for(endpoint, page=pagination.prev_num, **kwargs)
if pagination.has_next:
links['next'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.next_num)]))
links['last'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.pages)]))
links['next'] = url_for(endpoint, page=pagination.next_num, **kwargs)
links['last'] = url_for(endpoint, page=pagination.pages, **kwargs)
return links
@@ -320,7 +325,7 @@ def send_notification(notification_type):
notification_id = create_uuid()
if notification_type == 'sms':
if service.restricted and notification['to'] not in [user.mobile_number for user in service.users]:
if not allowed_send_to_number(service, notification['to']):
return jsonify(
result="error", message={'to': ['Invalid phone number for restricted service']}), 400
send_sms.apply_async((
@@ -330,7 +335,7 @@ def send_notification(notification_type):
datetime.utcnow().strftime(DATETIME_FORMAT)
), queue='sms')
else:
if service.restricted and notification['to'] not in [user.email_address for user in service.users]:
if not allowed_send_to_email(service, notification['to']):
return jsonify(
result="error", message={'to': ['Email address not permitted for restricted service']}), 400
send_email.apply_async((

View File

@@ -171,6 +171,9 @@ class SmsAdminNotificationSchema(SmsNotificationSchema):
class NotificationStatusSchema(BaseSchema):
template = fields.Nested(TemplateSchema, only=["id", "name", "template_type"], dump_only=True)
job = fields.Nested(JobSchema, only=["id", "original_file_name"], dump_only=True)
class Meta:
model = models.Notification

View File

@@ -24,7 +24,13 @@ from app.schemas import (
permission_schema
)
from app.celery.tasks import (send_sms_code, send_email_code, email_reset_password)
from app.celery.tasks import (
send_sms_code,
send_email_code,
email_reset_password,
email_registration_verification
)
from app.errors import register_errors
user = Blueprint('user', __name__)
@@ -148,6 +154,28 @@ def send_user_email_code(user_id):
return jsonify({}), 204
@user.route('/<int:user_id>/email-verification', methods=['POST'])
def send_user_email_verification(user_id):
user_to_send_to = get_model_users(user_id=user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
from app.dao.users_dao import create_secret_code
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, 'email')
email = user_to_send_to.email_address
verification_message = {'to': email,
'name': user_to_send_to.name,
'url': _create_verification_url(user_to_send_to, secret_code)}
email_registration_verification.apply_async([encryption.encrypt(verification_message)],
queue='email-registration-verification')
return jsonify({}), 204
@user.route('/<int:user_id>', methods=['GET'])
@user.route('', methods=['GET'])
def get_user(user_id=None):
@@ -207,3 +235,12 @@ def _create_reset_password_url(email):
token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'])
return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token
def _create_verification_url(user, secret_code):
from utils.url_safe_token import generate_token
import json
data = json.dumps({'user_id': user.id, 'email': user.email_address, 'secret_code': secret_code})
token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'])
return current_app.config['ADMIN_BASE_URL'] + '/verify-email/' + token

15
app/validation.py Normal file
View File

@@ -0,0 +1,15 @@
from utils.recipients import format_phone_number, validate_phone_number
def allowed_send_to_number(service, to):
if service.restricted and format_phone_number(validate_phone_number(to)) not in [
format_phone_number(validate_phone_number(user.mobile_number)) for user in service.users
]:
return False
return True
def allowed_send_to_email(service, to):
if service.restricted and to not in [user.email_address for user in service.users]:
return False
return True