Merge branch 'master' into platform-admin

Conflicts:
	app/main/views/send.py
This commit is contained in:
Rebecca Law
2016-03-21 12:18:44 +00:00
21 changed files with 189 additions and 39 deletions

View File

@@ -168,7 +168,7 @@ def useful_headers_after_request(response):
response.headers.add('X-Content-Type-Options', 'nosniff') response.headers.add('X-Content-Type-Options', 'nosniff')
response.headers.add('X-XSS-Protection', '1; mode=block') response.headers.add('X-XSS-Protection', '1; mode=block')
response.headers.add('Content-Security-Policy', response.headers.add('Content-Security-Policy',
"default-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;") # noqa "default-src 'self' 'unsafe-inline'; script-src 'self' *.google-analytics.com 'unsafe-inline' data:; object-src 'self'; font-src 'self' data:; img-src 'self' *.google-analytics.com data:;") # noqa
if 'Cache-Control' in response.headers: if 'Cache-Control' in response.headers:
del response.headers['Cache-Control'] del response.headers['Cache-Control']
response.headers.add( response.headers.add(

View File

@@ -12,9 +12,9 @@ from wtforms import (
HiddenField HiddenField
) )
from wtforms.fields.html5 import EmailField, TelField from wtforms.fields.html5 import EmailField, TelField
from wtforms.validators import DataRequired, Email, Length, Regexp from wtforms.validators import (DataRequired, Email, Length, Regexp)
from app.main.validators import Blacklist, CsvFileValidator from app.main.validators import (Blacklist, CsvFileValidator, ValidEmailDomainRegex)
from utils.recipients import ( from utils.recipients import (
validate_phone_number, validate_phone_number,
@@ -24,13 +24,11 @@ from utils.recipients import (
def email_address(label='Email address'): def email_address(label='Email address'):
gov_uk_email \
= "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.gov.uk)"
return EmailField(label, validators=[ return EmailField(label, validators=[
Length(min=5, max=255), Length(min=5, max=255),
DataRequired(message='Email cannot be empty'), DataRequired(message='Email cannot be empty'),
Email(message='Enter a valid email address'), Email(message='Enter a valid email address'),
Regexp(regex=gov_uk_email, message='Enter a gov.uk email address')]) ValidEmailDomainRegex()])
class UKMobileNumber(TelField): class UKMobileNumber(TelField):

View File

@@ -1,3 +1,4 @@
import re
from wtforms import ValidationError from wtforms import ValidationError
from datetime import datetime from datetime import datetime
from app.main.encryption import check_hash from app.main.encryption import check_hash
@@ -22,3 +23,18 @@ class CsvFileValidator(object):
def __call__(self, form, field): def __call__(self, form, field):
if not form.file.data.mimetype == 'text/csv': if not form.file.data.mimetype == 'text/csv':
raise ValidationError(self.message) raise ValidationError(self.message)
class ValidEmailDomainRegex(object):
def __call__(self, form, field):
from flask import (current_app, url_for)
message = (
'Enter a central government email address.'
' If you think you should have access'
' <a href="{}">contact us</a>').format(
"https://docs.google.com/forms/d/1AL8U-xJX_HAFEiQiJszGQw0PcEaEUnYATSntEghNDGo/viewform")
valid_domains = current_app.config.get('EMAIL_DOMAIN_REGEXES', [])
email_regex = "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.({}))".format("|".join(valid_domains))
if not re.match(email_regex, field.data):
raise ValidationError(message)

View File

@@ -36,12 +36,7 @@ def accept_invite(token):
session['invited_user'] = invited_user.serialize() session['invited_user'] = invited_user.serialize()
try: existing_user = user_api_client.get_user_by_email_or_none(invited_user.email_address)
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
except HTTPError as ex:
if ex.status_code == 404:
existing_user = False
service_users = user_api_client.get_users_for_service(invited_user.service) service_users = user_api_client.get_users_for_service(invited_user.service)
if existing_user: if existing_user:

View File

@@ -56,9 +56,9 @@ def get_send_button_text(template_type, number_of_messages):
}[template_type].format(number_of_messages) }[template_type].format(number_of_messages)
def get_page_headings(template_type, service_id): def get_page_headings(template_type):
# User has manage_service role # User has manage_service role
if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']): if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']):
return send_messages_page_headings[template_type] return send_messages_page_headings[template_type]
else: else:
return manage_templates_page_headings[template_type] return manage_templates_page_headings[template_type]
@@ -66,7 +66,8 @@ def get_page_headings(template_type, service_id):
@main.route("/services/<service_id>/send/<template_type>", methods=['GET']) @main.route("/services/<service_id>/send/<template_type>", methods=['GET'])
@login_required @login_required
@user_has_permissions('send_texts', 'send_emails', 'send_letters', 'manage_templates', admin_override=True, or_=True) @user_has_permissions('send_texts', 'send_emails', 'send_letters', 'manage_templates', 'manage_api_keys',
admin_override=True, or_=True)
def choose_template(service_id, template_type): def choose_template(service_id, template_type):
service = services_dao.get_service_by_id_or_404(service_id) service = services_dao.get_service_by_id_or_404(service_id)
@@ -85,7 +86,7 @@ def choose_template(service_id, template_type):
if template['template_type'] == template_type if template['template_type'] == template_type
], ],
template_type=template_type, template_type=template_type,
page_heading=get_page_headings(template_type, service_id), page_heading=get_page_headings(template_type),
service=service, service=service,
has_jobs=len(jobs), has_jobs=len(jobs),
service_id=service_id service_id=service_id
@@ -253,7 +254,7 @@ def check_messages(service_id, upload_id):
'views/check.html', 'views/check.html',
recipients=recipients, recipients=recipients,
template=template, template=template,
page_heading=get_page_headings(template.template_type, service_id), page_heading=get_page_headings(template.template_type),
errors=get_errors_for_csv(recipients, template.template_type), errors=get_errors_for_csv(recipients, template.template_type),
rows_have_errors=any(recipients.rows_with_errors), rows_have_errors=any(recipients.rows_with_errors),
count_of_recipients=session['upload_data']['notification_count'], count_of_recipients=session['upload_data']['notification_count'],

View File

@@ -29,7 +29,7 @@ def sign_in():
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
user = user_api_client.get_user_by_email(form.email_address.data) user = user_api_client.get_user_by_email_or_none(form.email_address.data)
user = _get_and_verify_user(user, form.password.data) user = _get_and_verify_user(user, form.password.data)
if user: if user:
# Remember me login # Remember me login

View File

@@ -35,6 +35,13 @@ class UserApiClient(BaseAPIClient):
user_data = self.get('/user/email', params={'email': email_address}) user_data = self.get('/user/email', params={'email': email_address})
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count) return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
def get_user_by_email_or_none(self, email_address):
try:
return self.get_user_by_email(email_address)
except HTTPError as e:
if HTTPError.status_code == 404:
return None
def get_users(self): def get_users(self):
users_data = self.get("/user")['data'] users_data = self.get("/user")['data']
users = [] users = []
@@ -106,15 +113,9 @@ class UserApiClient(BaseAPIClient):
self.post(endpoint, data=data) self.post(endpoint, data=data)
def is_email_unique(self, email_address): def is_email_unique(self, email_address):
try: if self.get_user_by_email_or_none(email_address):
if self.get_user_by_email(email_address): return False
return False return True
return True
except HTTPError as ex:
if ex.status_code == 404:
return True
else:
raise ex
def activate_user(self, user): def activate_user(self, user):
user.state = 'active' user.state = 'active'

View File

@@ -83,4 +83,12 @@
{% block body_end %} {% block body_end %}
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}" /></script> <script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}" /></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-75215134-1', 'auto');
ga('send', 'pageview');
</script>
{% endblock %} {% endblock %}

View File

@@ -7,7 +7,8 @@
help_link_text=None, help_link_text=None,
width='2-3', width='2-3',
suffix=None, suffix=None,
disabled=False disabled=False,
safe_error_message=False
) %} ) %}
<div class="form-group{% if field.errors %} error{% endif %}" {% if autofocus %}data-module="autofocus"{% endif %}> <div class="form-group{% if field.errors %} error{% endif %}" {% if autofocus %}data-module="autofocus"{% endif %}>
<label class="form-label" for="{{ field.name }}"> <label class="form-label" for="{{ field.name }}">
@@ -19,7 +20,7 @@
{% endif %} {% endif %}
{% if field.errors %} {% if field.errors %}
<span class="error-message"> <span class="error-message">
{{ field.errors[0] }} {% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
</span> </span>
{% endif %} {% endif %}
</label> </label>

View File

@@ -7,7 +7,7 @@
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Send text messages</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Send text messages</a></li>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Send emails</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Send emails</a></li>
</ul> </ul>
{% elif current_user.has_permissions(['manage_templates']) %} {% elif current_user.has_permissions(['manage_templates','manage_api_keys'], or_=True) %}
<ul> <ul>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Text message templates</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Text message templates</a></li>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Email templates</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Email templates</a></li>

View File

@@ -15,7 +15,7 @@ Create a new password GOV.UK Notify
<p>If you have forgotten your password, we can send you an email to create a new password.</p> <p>If you have forgotten your password, we can send you an email to create a new password.</p>
<form autocomplete="off" method="post"> <form autocomplete="off" method="post">
{{ textbox(form.email_address) }} {{ textbox(form.email_address, safe_error_message=True) }}
{{ page_footer("Send email") }} {{ page_footer("Send email") }}
</form> </form>

View File

@@ -73,6 +73,50 @@
can check that everything is fully working without accidentally sending can check that everything is fully working without accidentally sending
hundreds of text messages or emails. hundreds of text messages or emails.
</p> </p>
<h2 class="heading-large" id="pricing">Pricing</h2>
<h3 class="heading-medium">Emails</h3>
<p>
Sending email through GOV.UK&nbsp;Notify is completely free.
</p>
<h3 class="heading-medium">Text messages</h3>
<ul class="list list-bullet">
<li>Free allowance: 250,000 SMS per service, per financial year</li>
<li>Standard rate: 1.8 pence per SMS</li>
<li>Fail-over rate: 2.5 pence per SMS</li>
</ul>
<p>
You only pay the fail-over rate if we cant deliver your text message
through our primary SMS provider.
</p>
<p>
We simply charge you the costs we pay to our delivery partners.
We dont mark these costs up in any way.
</p>
<h4 class="heading-small">SMS and text message length</h4>
<ul class="list list-bullet">
<li>Up to 160 characters = 1 SMS</li>
<li>Up to 306 characters = 2 SMS</li>
<li>Up to 459 characters = 3 SMS</li>
</ul>
<h3 class="heading-medium">No monthly charge or setup fee</h3>
<p>
There are no other charges for using Notify. Theres no monthly charge
or setup fee.
</p>
<p>
The Government Digital Service is funding the development and running
of Notify. Were also covering the cost of the free emails and text messages.
</p>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@ Manage users GOV.UK Notify
<div class="grid-row"> <div class="grid-row">
<form method="post" class="column-three-quarters"> <form method="post" class="column-three-quarters">
{{ textbox(form.email_address, hint='Email address must end in .gov.uk', width='1-1') }} {{ textbox(form.email_address, hint='You must use an email address from a central government organisation', width='1-1', safe_error_message=True) }}
<fieldset class='yes-no-wrapper'> <fieldset class='yes-no-wrapper'>
<legend class='heading-small'> <legend class='heading-small'>

View File

@@ -16,7 +16,7 @@ Create an account GOV.UK Notify
<form method="post" autocomplete="nope"> <form method="post" autocomplete="nope">
{{ textbox(form.name, width='3-4') }} {{ textbox(form.name, width='3-4') }}
{{ textbox(form.email_address, hint="Your email address must end in .gov.uk", width='3-4') }} {{ textbox(form.email_address, hint="You must use an email address from a central government organisation", width='3-4', safe_error_message=True) }}
{{ textbox(form.mobile_number, width='3-4') }} {{ textbox(form.mobile_number, width='3-4') }}
{{ textbox(form.password, hint="Your password must have at least 10 characters", width='3-4') }} {{ textbox(form.password, hint="Your password must have at least 10 characters", width='3-4') }}
{{ page_footer("Continue") }} {{ page_footer("Continue") }}

View File

@@ -18,7 +18,7 @@ GOV.UK Notify | Service settings
{% endif %} {% endif %}
<div class="column-three-quarters"> <div class="column-three-quarters">
<form method="post"> <form method="post">
{{ textbox(form_field) }} {{ textbox(form_field, safe_error_message=True) }}
{{ page_footer( {{ page_footer(
'Save', 'Save',
back_link=url_for('.user_profile'), back_link=url_for('.user_profile'),

View File

@@ -50,6 +50,18 @@ class Config(object):
SHOW_STYLEGUIDE = True SHOW_STYLEGUIDE = True
EMAIL_DOMAIN_REGEXES = [
"gov.uk",
"mod.uk",
"mil.uk",
"ddc-mod.org",
"slc.co.uk"
"gov.scot",
"parliament.uk",
"nhs.uk",
"nhs.net",
"police.uk"]
class Development(Config): class Development(Config):
DEBUG = True DEBUG = True

View File

@@ -6,8 +6,43 @@ def test_should_raise_validation_error_for_password(app_, mock_get_user_by_email
form = RegisterUserForm() form = RegisterUserForm()
form.name.data = 'test' form.name.data = 'test'
form.email_address.data = 'teset@example.gov.uk' form.email_address.data = 'teset@example.gov.uk'
form.mobile_number.data = '+441231231231' form.mobile_number.data = '441231231231'
form.password.data = 'password1234' form.password.data = 'password1234'
form.validate() form.validate()
assert 'That password is blacklisted, too common' in form.errors['password'] assert 'That password is blacklisted, too common' in form.errors['password']
def test_valid_email_not_in_valid_domains(app_):
with app_.test_request_context():
form = RegisterUserForm(email_address="test@test.com", mobile_number='441231231231')
assert not form.validate()
assert "Enter a central government email address" in form.errors['email_address'][0]
def test_valid_email_in_valid_domains(app_):
with app_.test_request_context():
form = RegisterUserForm(
name="test",
email_address="test@my.gov.uk",
mobile_number='4407888999111',
password='1234567890')
form.validate()
assert form.errors == {}
def test_invalid_email_address_error_message(app_):
with app_.test_request_context():
form = RegisterUserForm(
name="test",
email_address="test.com",
mobile_number='4407888999111',
password='1234567890')
assert not form.validate()
form = RegisterUserForm(
name="test",
email_address="test.com",
mobile_number='4407888999111',
password='1234567890')
assert not form.validate()

View File

@@ -101,11 +101,11 @@ def test_menu_manage_api_keys(mocker, app_, api_user_active, service_one, mock_g
assert url_for( assert url_for(
'main.choose_template', 'main.choose_template',
service_id=service_one['id'], service_id=service_one['id'],
template_type='email') not in page template_type='email') in page
assert url_for( assert url_for(
'main.choose_template', 'main.choose_template',
service_id=service_one['id'], service_id=service_one['id'],
template_type='sms') not in page template_type='sms') in page
assert url_for('main.manage_users', service_id=service_one['id']) not in page assert url_for('main.manage_users', service_id=service_one['id']) not in page
assert url_for('main.service_settings', service_id=service_one['id']) not in page assert url_for('main.service_settings', service_id=service_one['id']) not in page

View File

@@ -6,4 +6,4 @@ def test_owasp_useful_headers_set(app_):
assert response.headers['X-Frame-Options'] == 'deny' assert response.headers['X-Frame-Options'] == 'deny'
assert response.headers['X-Content-Type-Options'] == 'nosniff' assert response.headers['X-Content-Type-Options'] == 'nosniff'
assert response.headers['X-XSS-Protection'] == '1; mode=block' assert response.headers['X-XSS-Protection'] == '1; mode=block'
assert response.headers['Content-Security-Policy'] == "default-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;" # noqa assert response.headers['Content-Security-Policy'] == "default-src 'self' 'unsafe-inline'; script-src 'self' *.google-analytics.com 'unsafe-inline' data:; object-src 'self'; font-src 'self' data:; img-src 'self' *.google-analytics.com data:;" # noqa

View File

@@ -82,7 +82,8 @@ def test_should_return_200_when_email_is_not_gov_uk(app_,
'password': 'validPassword!'}) 'password': 'validPassword!'})
assert response.status_code == 200 assert response.status_code == 200
assert 'Enter a gov.uk email address' in response.get_data(as_text=True) print(response.get_data(as_text=True))
assert 'Enter a central government email address' in response.get_data(as_text=True)
def test_should_add_user_details_to_session(app_, def test_should_add_user_details_to_session(app_,

View File

@@ -423,3 +423,41 @@ def test_route_choose_template_send_messages_permissions(mocker,
"main.edit_service_template", "main.edit_service_template",
service_id=service_one['id'], service_id=service_one['id'],
template_id=template_id) not in page template_id=template_id) not in page
def test_route_choose_template_manage_api_keys_permissions(mocker,
app_,
api_user_active,
service_one,
mock_get_user,
mock_get_service,
mock_check_verify_code,
mock_get_service_templates,
mock_get_jobs):
with app_.test_request_context():
template_id = mock_get_service_templates(service_one['id'])['data'][0]['id']
resp = validate_route_permission(
mocker,
app_,
"GET",
200,
url_for(
'main.choose_template',
service_id=service_one['id'],
template_type='sms'),
['manage_api_keys', 'access_developer_docs'],
api_user_active,
service_one)
page = resp.get_data(as_text=True)
assert url_for(
"main.send_messages",
service_id=service_one['id'],
template_id=template_id) in page
assert url_for(
"main.send_message_to_self",
service_id=service_one['id'],
template_id=template_id) not in page
assert url_for(
"main.edit_service_template",
service_id=service_one['id'],
template_id=template_id) not in page