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-XSS-Protection', '1; mode=block')
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:
del response.headers['Cache-Control']
response.headers.add(

View File

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

View File

@@ -1,3 +1,4 @@
import re
from wtforms import ValidationError
from datetime import datetime
from app.main.encryption import check_hash
@@ -22,3 +23,18 @@ class CsvFileValidator(object):
def __call__(self, form, field):
if not form.file.data.mimetype == 'text/csv':
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()
try:
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
existing_user = user_api_client.get_user_by_email_or_none(invited_user.email_address)
service_users = user_api_client.get_users_for_service(invited_user.service)
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)
def get_page_headings(template_type, service_id):
def get_page_headings(template_type):
# 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]
else:
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'])
@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):
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
],
template_type=template_type,
page_heading=get_page_headings(template_type, service_id),
page_heading=get_page_headings(template_type),
service=service,
has_jobs=len(jobs),
service_id=service_id
@@ -253,7 +254,7 @@ def check_messages(service_id, upload_id):
'views/check.html',
recipients=recipients,
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),
rows_have_errors=any(recipients.rows_with_errors),
count_of_recipients=session['upload_data']['notification_count'],

View File

@@ -29,7 +29,7 @@ def sign_in():
form = LoginForm()
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)
if user:
# Remember me login

View File

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

View File

@@ -83,4 +83,12 @@
{% block body_end %}
<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 %}

View File

@@ -7,7 +7,8 @@
help_link_text=None,
width='2-3',
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 %}>
<label class="form-label" for="{{ field.name }}">
@@ -19,7 +20,7 @@
{% endif %}
{% if field.errors %}
<span class="error-message">
{{ field.errors[0] }}
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
</span>
{% endif %}
</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='email') }}">Send emails</a></li>
</ul>
{% elif current_user.has_permissions(['manage_templates']) %}
{% elif current_user.has_permissions(['manage_templates','manage_api_keys'], or_=True) %}
<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='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>
<form autocomplete="off" method="post">
{{ textbox(form.email_address) }}
{{ textbox(form.email_address, safe_error_message=True) }}
{{ page_footer("Send email") }}
</form>

View File

@@ -73,6 +73,50 @@
can check that everything is fully working without accidentally sending
hundreds of text messages or emails.
</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>

View File

@@ -16,7 +16,7 @@ Manage users GOV.UK Notify
<div class="grid-row">
<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'>
<legend class='heading-small'>

View File

@@ -16,7 +16,7 @@ Create an account GOV.UK Notify
<form method="post" autocomplete="nope">
{{ 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.password, hint="Your password must have at least 10 characters", width='3-4') }}
{{ page_footer("Continue") }}

View File

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

View File

@@ -50,6 +50,18 @@ class Config(object):
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):
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.name.data = 'test'
form.email_address.data = 'teset@example.gov.uk'
form.mobile_number.data = '+441231231231'
form.mobile_number.data = '441231231231'
form.password.data = 'password1234'
form.validate()
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(
'main.choose_template',
service_id=service_one['id'],
template_type='email') not in page
template_type='email') in page
assert url_for(
'main.choose_template',
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.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-Content-Type-Options'] == 'nosniff'
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!'})
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_,

View File

@@ -423,3 +423,41 @@ def test_route_choose_template_send_messages_permissions(mocker,
"main.edit_service_template",
service_id=service_one['id'],
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