diff --git a/app/__init__.py b/app/__init__.py index 926fd5f13..6f4c79ff0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,6 +9,7 @@ from flask_wtf import CsrfProtect from webassets.filter import get_filter from werkzeug.exceptions import abort +from app.notify_client.api_client import AdminAPIClient from app.its_dangerous_session import ItsdangerousSessionInterface import app.proxy_fix from config import configs @@ -18,6 +19,8 @@ db = SQLAlchemy() login_manager = LoginManager() csrf = CsrfProtect() +admin_api_client = AdminAPIClient() + def create_app(config_name): application = Flask(__name__) @@ -39,6 +42,7 @@ def create_app(config_name): proxy_fix.init_app(application) application.session_interface = ItsdangerousSessionInterface() + admin_api_client.init_app(application) return application @@ -98,7 +102,8 @@ def init_asset_environment(app): assets.Bundle( 'govuk_template/govuk-template.scss', filters='scss', - output='stylesheets/govuk-template.css' + output='stylesheets/govuk-template.css', + depends='*.scss' ) ) diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index 9ceb5468d..50c9ec702 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -19,4 +19,29 @@ font-size: 19px; line-height: 1.31579; } +} + +.phase-tag { + @include phase-tag(beta); +} + +@media (min-width: 641px) { + .phase-tag { + font-size: 16px; + line-height: 1.25; + margin-top: 7px; + + } +} + +@media (max-width: 641px) { + .phase-tag { + margin-top: 11px; + } +} + + + +#global-header #logo { + white-space: nowrap; } \ No newline at end of file diff --git a/app/main/exceptions.py b/app/main/exceptions.py new file mode 100644 index 000000000..45f0515c0 --- /dev/null +++ b/app/main/exceptions.py @@ -0,0 +1,5 @@ + + +class AdminApiClientException(Exception): + def __init__(self, message): + self.value = message diff --git a/app/main/forms.py b/app/main/forms.py index bc013fccc..ea5978fa3 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -2,6 +2,8 @@ from flask_wtf import Form from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, Email, Length, Regexp +from app.main.validators import Blacklist + class LoginForm(Form): email_address = StringField('Email address', validators=[ @@ -19,7 +21,7 @@ mobile_number = "^\\+44[\\d]{10}$" class RegisterUserForm(Form): - name = StringField('Name', + name = StringField('Full name', validators=[DataRequired(message='Name can not be empty')]) email_address = StringField('Email address', validators=[ Length(min=5, max=255), @@ -30,6 +32,7 @@ class RegisterUserForm(Form): mobile_number = StringField('Mobile phone number', validators=[DataRequired(message='Please enter your mobile number'), Regexp(regex=mobile_number, message='Please enter a +44 mobile number')]) - password = PasswordField('Password', + password = PasswordField('Create a password', validators=[DataRequired(message='Please enter your password'), - Length(10, 255, message='Password must be at least 10 characters')]) + Length(10, 255, message='Password must be at least 10 characters'), + Blacklist(message='That password is blacklisted, too common')]) diff --git a/app/main/validators.py b/app/main/validators.py new file mode 100644 index 000000000..2638bd5e2 --- /dev/null +++ b/app/main/validators.py @@ -0,0 +1,12 @@ +from wtforms import ValidationError + + +class Blacklist(object): + def __init__(self, message=None): + if not message: + message = 'Password is blacklisted.' + self.message = message + + def __call__(self, form, field): + if field.data in ['password1234', 'passw0rd1234']: + raise ValidationError(self.message) diff --git a/app/main/views/index.py b/app/main/views/index.py index 2ffe73da4..f664ee835 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -15,19 +15,16 @@ def govuk(): @main.route("/register-from-invite") -@login_required def registerfrominvite(): return render_template('register-from-invite.html') @main.route("/verify") -@login_required def verify(): return render_template('verify.html') @main.route("/verify-mobile") -@login_required def verifymobile(): return render_template('verify-mobile.html') @@ -50,7 +47,6 @@ def addservice(): @main.route("/two-factor") -@login_required def twofactor(): return render_template('two-factor.html') diff --git a/app/main/views/register.py b/app/main/views/register.py index 4892832ea..48c9d68fa 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -1,9 +1,14 @@ -from datetime import datetime +from datetime import datetime, timedelta +from random import randint -from flask import render_template, redirect, jsonify +from flask import render_template, redirect, jsonify, session +from sqlalchemy.exc import SQLAlchemyError +from app import admin_api_client from app.main import main from app.main.dao import users_dao +from app.main.encryption import hashpw +from app.main.exceptions import AdminApiClientException from app.main.forms import RegisterUserForm from app.models import User @@ -25,9 +30,43 @@ def process_register(): created_at=datetime.now(), role_id=1) try: + sms_code = send_sms_code(form.mobile_number.data) + email_code = send_email_code(form.email_address.data) + session['sms_code'] = hashpw(sms_code) + session['email_code'] = hashpw(email_code) + session['expiry_date'] = str(datetime.now() + timedelta(hours=1)) users_dao.insert_user(user) - return redirect('/two-factor') - except Exception as e: + except AdminApiClientException as e: + return jsonify(admin_api_client_error=e.value) + except SQLAlchemyError: return jsonify(database_error='encountered database error'), 400 else: return jsonify(form.errors), 400 + return redirect('/verify') + + +def send_sms_code(mobile_number): + sms_code = _create_code() + try: + admin_api_client.send_sms(mobile_number, message=sms_code, token=admin_api_client.auth_token) + except: + raise AdminApiClientException('Exception when sending sms.') + return sms_code + + +def send_email_code(email): + email_code = _create_code() + try: + admin_api_client.send_email(email_address=email, + from_str='notify@digital.cabinet-office.gov.uk', + message=email_code, + subject='Verification code', + token=admin_api_client.auth_token) + except: + raise AdminApiClientException('Exception when sending email.') + + return email_code + + +def _create_code(): + return ''.join(["%s" % randint(0, 9) for _ in range(0, 5)]) diff --git a/app/notify_client/__init__.py b/app/notify_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/notify_client/api_client.py b/app/notify_client/api_client.py new file mode 100644 index 000000000..e32e8e37e --- /dev/null +++ b/app/notify_client/api_client.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +from notify_client import NotifyAPIClient + + +class AdminAPIClient(NotifyAPIClient): + def init_app(self, app): + self.base_url = app.config['NOTIFY_DATA_API_URL'] + self.auth_token = app.config['NOTIFY_DATA_API_AUTH_TOKEN'] diff --git a/app/templates/add-service.html b/app/templates/add-service.html index dd2f3b1a0..0924f35aa 100644 --- a/app/templates/add-service.html +++ b/app/templates/add-service.html @@ -9,6 +9,12 @@ GOV.UK Notify | Set up service

Set up notifications for your service

+ +

Users will see your service name:

+

@@ -17,11 +23,6 @@ GOV.UK Notify | Set up service For example, 'Vehicle tax' or 'Carer's allowance'

-

We'll create your service in test mode

-

In test mode you can only send notifications to people in your team.

-

When you're ready to go live we'll remove this restriction.

- -

Continue

diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 5e24c7294..e31094bed 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -7,5 +7,13 @@ GOV.UK notifications admin {% block cookie_message %} {% endblock %} + {% block inside_header %} +
+ + BETA + +
+ {% endblock %} + {% set global_header_text = "GOV.UK Notify" %} diff --git a/app/templates/email-not-received.html b/app/templates/email-not-received.html index 462fbb961..758589199 100644 --- a/app/templates/email-not-received.html +++ b/app/templates/email-not-received.html @@ -10,7 +10,7 @@ GOV.UK Notify

Check your email address

-

Check your email address is correct and resend a confirmation code.

+

Check your email address is correct and then resend the confirmation code.

@@ -25,4 +25,4 @@ GOV.UK Notify

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/new-password.html b/app/templates/new-password.html index 0d2c811f7..f64c68eeb 100644 --- a/app/templates/new-password.html +++ b/app/templates/new-password.html @@ -9,10 +9,13 @@ GOV.UK Notify

Create a new password

+ +

You can now create a new password for your account.

- + + Your password must have at least 10 characters

@@ -21,4 +24,4 @@ GOV.UK Notify

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/register-from-invite.html b/app/templates/register-from-invite.html index 73169cc06..c1565d7f9 100644 --- a/app/templates/register-from-invite.html +++ b/app/templates/register-from-invite.html @@ -13,7 +13,7 @@ GOV.UK Notify | Create a user account

If you've used GOV.UK Notify before, sign in to your account.

- +

@@ -21,8 +21,9 @@ GOV.UK Notify | Create a user account

- + + Your password must have at least 10 characters

diff --git a/app/templates/register.html b/app/templates/register.html index 7a8f41f8b..99c585714 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -12,9 +12,7 @@ GOV.UK Notify | Create an account

If you've used GOV.UK Notify before, sign in to your account.

-

You need to have access to your email account and a mobile phone to register.

- -
+ {{ form.hidden_tag() }}

@@ -32,6 +30,7 @@ GOV.UK Notify | Create an account

{{ form.password(class="form-control-1-4", autocomplete="off") }}
+ Your password must have at least 10 characters

diff --git a/app/templates/signedout.html b/app/templates/signedout.html index d2beafabe..49d5d6be8 100644 --- a/app/templates/signedout.html +++ b/app/templates/signedout.html @@ -12,13 +12,13 @@ GOV.UK Notify | Get started

Use GOV.UK Notify to send notifications by text message, email and letter.

We're making it easy to keep your users informed.

-

If you work for a central UK Government department or agency you can set up a test account now.

+

If you work for a UK government department or agency you can set up a test account now.

Set up an account -

Sign in

+

If you've used GOV.UK Notify before, sign in to your account.

diff --git a/app/templates/signin.html b/app/templates/signin.html index b47f53475..0c3d34531 100644 --- a/app/templates/signin.html +++ b/app/templates/signin.html @@ -10,7 +10,7 @@ Sign in

Sign in

-

If you do not have an account, you can register.

+

If you do not have an account, you can register for one now.

{{ form.hidden_tag() }} @@ -33,4 +33,4 @@ Sign in
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/text-not-received-2.html b/app/templates/text-not-received-2.html index c5ffb8e8e..ea6e87eca 100644 --- a/app/templates/text-not-received-2.html +++ b/app/templates/text-not-received-2.html @@ -12,7 +12,7 @@ GOV.UK Notify

Check your mobile number

-

Check your mobile phone number is correct and resend a confirmation code.

+

Check your mobile phone number is correct and then resend the confirmation code.

@@ -26,4 +26,4 @@ GOV.UK Notify

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/text-not-received.html b/app/templates/text-not-received.html index ce8fff18c..4b332c894 100644 --- a/app/templates/text-not-received.html +++ b/app/templates/text-not-received.html @@ -10,7 +10,7 @@ GOV.UK Notify

Check your mobile number

-

Check your mobile phone number is correct and resend a confirmation code.

+

Check your mobile phone number is correct and then resend the confirmation code.

@@ -24,4 +24,4 @@ GOV.UK Notify

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/two-factor.html b/app/templates/two-factor.html index 293679b0c..9f23831af 100644 --- a/app/templates/two-factor.html +++ b/app/templates/two-factor.html @@ -10,11 +10,10 @@ GOV.UK Notify | Text verification

Text verification

-

We've sent you a text message containing a verification code.

+

We've sent you a text message with a verification code.

- +

diff --git a/app/templates/verification-not-received.html b/app/templates/verification-not-received.html index b21b16850..de1d9204f 100644 --- a/app/templates/verification-not-received.html +++ b/app/templates/verification-not-received.html @@ -10,15 +10,15 @@ GOV.UK Notify

Resend verification code

-

Text messages sometimes take a few minutes to be received.
If you have not received a text message, you can resend.

+

Text messages sometimes take a few minutes to arrive. If you do not receive the text message, you can resend it.

-

If you no longer have access to your mobile phone and registered number, get in contact with your service manager to reset your number.

P> +

If you no longer have access to the phone with the number you registered for this service, speak to your service manager to reset the number.

- Resend confirmation code + Resend verification code

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/verify-mobile.html b/app/templates/verify-mobile.html index 10a82b3d5..ae66bbecf 100644 --- a/app/templates/verify-mobile.html +++ b/app/templates/verify-mobile.html @@ -10,12 +10,10 @@ GOV.UK Notify | Confirm mobile number

Confirm your mobile number

-

You need to prove the contact details you gave us are yours.

-

We've sent you an email and a text message containing confirmation codes.

+

We've sent you a confirmation code by text message.

- +

diff --git a/app/templates/verify.html b/app/templates/verify.html index 729f099c2..a6ab5b8f5 100644 --- a/app/templates/verify.html +++ b/app/templates/verify.html @@ -8,20 +8,17 @@ GOV.UK Notify | Confirm email address and mobile number
-

Confirm your email address and mobile number

+

Activate your account

-

You need to prove the contact details you gave us are yours.

-

We've sent you an email and a text message containing confirmation codes.

+

We've sent you confirmation codes by email and text message. You need to enter both codes here.


I haven't received an email


I haven't received a text

diff --git a/config.py b/config.py index 6c799b561..0bc3a4c4a 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,5 @@ +import os + class Config(object): DEBUG = False @@ -11,6 +13,14 @@ class Config(object): MAX_FAILED_LOGIN_COUNT = 10 PASS_SECRET_KEY = 'secret-key-unique-changeme' + SESSION_COOKIE_NAME = 'notify_admin_session' + SESSION_COOKIE_PATH = '/admin' + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SECURE = True + + NOTIFY_DATA_API_URL = os.getenv('NOTIFY_API_URL', "http://localhost:6001") + NOTIFY_DATA_API_AUTH_TOKEN = os.getenv('NOTIFY_API_TOKEN', "dev-token") + WTF_CSRF_ENABLED = True SECRET_KEY = 'secret-key' HTTP_PROTOCOL = 'http' diff --git a/requirements.txt b/requirements.txt index 9ac4a3971..aaa53cebc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ SQLAlchemy==1.0.5 SQLAlchemy-Utils==0.30.5 Flask-WTF==0.11 Flask-Login==0.2.11 -Flask-Bcrypt==0.6.2 \ No newline at end of file +Flask-Bcrypt==0.6.2 + +git+https://github.com/alphagov/notify-api-client.git@0.1.4#egg=notify-api-client==0.1.4 \ No newline at end of file diff --git a/requirements_for_test.txt b/requirements_for_test.txt index f8aeacc70..ebea6362b 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -1,3 +1,4 @@ -r requirements.txt pep8==1.5.7 -pytest==2.8.1 \ No newline at end of file +pytest==2.8.1 +pytest-mock==0.8.1 diff --git a/tests/app/main/test_validators.py b/tests/app/main/test_validators.py new file mode 100644 index 000000000..a1cc52192 --- /dev/null +++ b/tests/app/main/test_validators.py @@ -0,0 +1,17 @@ +from pytest import fail + +from app.main.forms import RegisterUserForm + + +def test_should_raise_validation_error_for_password(notifications_admin): + form = RegisterUserForm() + form.name.data = 'test' + form.email_address.data = 'teset@example.gov.uk' + form.mobile_number.data = '+441231231231' + form.password.data = 'password1234' + + try: + form.validate() + fail() + except: + assert 'That password is blacklisted, too common' in form.errors['password'] diff --git a/tests/app/main/views/test_register.py b/tests/app/main/views/test_register.py index 2c6e384a0..a40b93481 100644 --- a/tests/app/main/views/test_register.py +++ b/tests/app/main/views/test_register.py @@ -7,17 +7,22 @@ def test_render_register_returns_template_with_form(notifications_admin, notific assert 'Create an account' in response.get_data(as_text=True) -def test_process_register_creates_new_user(notifications_admin, notifications_admin_db): +def test_process_register_creates_new_user(notifications_admin, notifications_admin_db, mocker): + _set_up_mocker(mocker) + response = notifications_admin.test_client().post('/register', data={'name': 'Some One Valid', 'email_address': 'someone@example.gov.uk', 'mobile_number': '+441231231231', 'password': 'validPassword!'}) assert response.status_code == 302 - assert response.location == 'http://localhost/two-factor' + assert response.location == 'http://localhost/verify' -def test_process_register_returns_400_when_mobile_number_is_invalid(notifications_admin, notifications_admin_db): +def test_process_register_returns_400_when_mobile_number_is_invalid(notifications_admin, + notifications_admin_db, + mocker): + _set_up_mocker(mocker) response = notifications_admin.test_client().post('/register', data={'name': 'Bad Mobile', 'email_address': 'bad_mobile@example.gov.uk', @@ -28,7 +33,8 @@ def test_process_register_returns_400_when_mobile_number_is_invalid(notification assert 'Please enter a +44 mobile number' in response.get_data(as_text=True) -def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, notifications_admin_db): +def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, notifications_admin_db, mocker): + _set_up_mocker(mocker) response = notifications_admin.test_client().post('/register', data={'name': 'Bad Mobile', 'email_address': 'bad_mobile@example.not.right', @@ -37,3 +43,31 @@ def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, notific assert response.status_code == 400 assert 'Please enter a gov.uk email address' in response.get_data(as_text=True) + + +def test_should_add_verify_codes_on_session(notifications_admin, notifications_admin_db, mocker): + _set_up_mocker(mocker) + with notifications_admin.test_client() as client: + response = client.post('/register', + data={'name': 'Test Codes', + 'email_address': 'test_codes@example.gov.uk', + 'mobile_number': '+441234567890', + 'password': 'validPassword!'}) + assert response.status_code == 302 + assert 'notify_admin_session' in response.headers.get('Set-Cookie') + + +def _set_up_mocker(mocker): + mocker.patch("app.admin_api_client.send_sms") + mocker.patch("app.admin_api_client.send_email") + + +def test_should_return_400_if_password_is_blacklisted(notifications_admin, notifications_admin_db): + response = notifications_admin.test_client().post('/register', + data={'name': 'Bad Mobile', + 'email_address': 'bad_mobile@example.not.right', + 'mobile_number': '+44123412345', + 'password': 'password1234'}) + + response.status_code == 400 + assert 'That password is blacklisted, too common' in response.get_data(as_text=True) diff --git a/tests/conftest.py b/tests/conftest.py index 338e4d63b..8712733e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +from _pytest.monkeypatch import monkeypatch from sqlalchemy.schema import MetaData, DropConstraint from app import create_app, db