mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 02:42:26 -05:00
Merge branch 'master' into platform-admin
Conflicts: app/main/views/send.py
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 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 can’t deliver your text message
|
||||
through our primary SMS provider.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We simply charge you the costs we pay to our delivery partners.
|
||||
We don’t 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. There’s no monthly charge
|
||||
or setup fee.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The Government Digital Service is funding the development and running
|
||||
of Notify. We’re also covering the cost of the free emails and text messages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
12
config.py
12
config.py
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user