Files
notifications-admin/app/__init__.py
T

538 lines
17 KiB
Python
Raw Normal View History

2015-11-24 09:40:14 +00:00
import os
import urllib
from datetime import datetime, timedelta, timezone
2016-10-19 11:54:51 +01:00
from time import monotonic
import itertools
2016-10-19 11:54:51 +01:00
import ago
from itsdangerous import BadSignature
from flask import (
session,
render_template,
make_response,
current_app,
request,
g,
url_for,
flash
2017-10-18 14:51:26 +01:00
)
2015-11-24 09:40:14 +00:00
from flask._compat import string_types
2017-05-04 11:28:45 +01:00
from flask.globals import _lookup_req_object, _request_ctx_stack
from flask_login import LoginManager, current_user
from flask_wtf import CSRFProtect
2017-07-26 11:01:24 +01:00
from flask_wtf.csrf import CSRFError
2016-04-21 09:30:33 +01:00
from functools import partial
from notifications_python_client.errors import HTTPError
2017-11-14 14:38:26 +00:00
from notifications_utils import logging, request_helper, formatters
from notifications_utils.clients import DeskproClient
2016-12-01 17:21:06 +00:00
from notifications_utils.clients.statsd.statsd_client import StatsdClient
2017-06-06 12:57:55 +01:00
from notifications_utils.recipients import (
validate_phone_number,
InvalidPhoneError,
format_phone_number_human_readable,
)
2017-03-27 16:39:46 +01:00
from notifications_utils.formatters import formatted_list
from werkzeug.exceptions import abort
2016-04-21 09:30:33 +01:00
from werkzeug.local import LocalProxy
2017-10-18 14:51:26 +01:00
from app import proxy_fix
2017-11-06 13:07:21 +00:00
from app.config import configs
2016-04-21 09:30:33 +01:00
from app.asset_fingerprinter import AssetFingerprinter
2016-12-07 14:12:12 +00:00
from app.notify_client.service_api_client import ServiceAPIClient
2016-01-20 17:32:55 +00:00
from app.notify_client.api_key_api_client import ApiKeyApiClient
2016-04-21 09:30:33 +01:00
from app.notify_client.invite_api_client import InviteApiClient
2016-01-29 10:27:23 +00:00
from app.notify_client.job_api_client import JobApiClient
2016-03-02 15:37:35 +00:00
from app.notify_client.notification_api_client import NotificationApiClient
2016-04-21 09:30:33 +01:00
from app.notify_client.status_api_client import StatusApiClient
from app.notify_client.template_statistics_api_client import TemplateStatisticsApiClient
2016-04-21 09:30:33 +01:00
from app.notify_client.user_api_client import UserApiClient
from app.notify_client.events_api_client import EventsApiClient
2016-05-11 09:43:55 +01:00
from app.notify_client.provider_client import ProviderClient
2018-02-07 10:30:49 +00:00
from app.notify_client.email_branding_client import EmailBrandingClient
2017-02-17 14:06:09 +00:00
from app.notify_client.models import AnonymousUser
2018-02-08 12:18:37 +00:00
from app.notify_client.organisations_api_client import OrganisationsClient
2017-04-11 10:59:50 +01:00
from app.notify_client.letter_jobs_client import LetterJobsClient
2017-08-08 10:24:54 +01:00
from app.notify_client.inbound_number_client import InboundNumberClient
from app.notify_client.billing_api_client import BillingAPIClient
2017-11-06 13:07:21 +00:00
from app.commands import setup_commands
2017-07-24 15:20:40 +01:00
from app.utils import get_cdn_domain
from app.utils import gmt_timezones
login_manager = LoginManager()
csrf = CSRFProtect()
2015-11-20 16:22:44 +00:00
service_api_client = ServiceAPIClient()
2016-01-19 22:47:42 +00:00
user_api_client = UserApiClient()
2016-01-20 17:32:55 +00:00
api_key_api_client = ApiKeyApiClient()
2016-01-29 10:27:23 +00:00
job_api_client = JobApiClient()
2016-03-02 15:37:35 +00:00
notification_api_client = NotificationApiClient()
2016-01-29 14:41:07 +00:00
status_api_client = StatusApiClient()
invite_api_client = InviteApiClient()
template_statistics_client = TemplateStatisticsApiClient()
events_api_client = EventsApiClient()
2016-05-11 09:43:55 +01:00
provider_client = ProviderClient()
2018-02-07 10:30:49 +00:00
email_branding_client = EmailBrandingClient()
2018-02-08 12:18:37 +00:00
organisations_client = OrganisationsClient()
2016-02-10 15:47:00 +00:00
asset_fingerprinter = AssetFingerprinter()
2016-12-01 17:21:06 +00:00
statsd_client = StatsdClient()
deskpro_client = DeskproClient()
2017-04-11 10:59:50 +01:00
letter_jobs_client = LetterJobsClient()
2017-08-08 10:24:54 +01:00
inbound_number_client = InboundNumberClient()
billing_api_client = BillingAPIClient()
2016-01-15 15:15:35 +00:00
# The current service attached to the request stack.
current_service = LocalProxy(partial(_lookup_req_object, 'service'))
2015-11-20 16:22:44 +00:00
2017-11-06 13:07:21 +00:00
def create_app(application):
setup_commands(application)
2015-11-20 16:22:44 +00:00
2017-01-09 19:14:04 +00:00
notify_environment = os.environ['NOTIFY_ENVIRONMENT']
2016-12-08 16:50:37 +00:00
application.config.from_object(configs[notify_environment])
init_app(application)
2016-12-01 17:21:06 +00:00
statsd_client.init_app(application)
deskpro_client.init_app(application)
2016-12-01 17:21:06 +00:00
logging.init_app(application, statsd_client)
csrf.init_app(application)
2017-11-14 14:38:26 +00:00
request_helper.init_app(application)
service_api_client.init_app(application)
2016-01-19 22:47:42 +00:00
user_api_client.init_app(application)
2016-01-20 17:32:55 +00:00
api_key_api_client.init_app(application)
2016-01-29 10:27:23 +00:00
job_api_client.init_app(application)
2016-03-02 15:37:35 +00:00
notification_api_client.init_app(application)
2016-01-29 14:41:07 +00:00
status_api_client.init_app(application)
invite_api_client.init_app(application)
template_statistics_client.init_app(application)
events_api_client.init_app(application)
2016-05-11 09:43:55 +01:00
provider_client.init_app(application)
2018-02-07 10:30:49 +00:00
email_branding_client.init_app(application)
2018-02-08 12:18:37 +00:00
organisations_client.init_app(application)
2017-04-11 10:59:50 +01:00
letter_jobs_client.init_app(application)
2017-08-08 10:24:54 +01:00
inbound_number_client.init_app(application)
billing_api_client.init_app(application)
2016-01-15 15:15:35 +00:00
login_manager.init_app(application)
2016-01-06 17:17:02 +00:00
login_manager.login_view = 'main.sign_in'
login_manager.login_message_category = 'default'
2016-12-14 14:07:08 +00:00
login_manager.session_protection = None
2017-02-17 14:06:09 +00:00
login_manager.anonymous_user = AnonymousUser
2015-11-24 09:40:14 +00:00
2015-11-20 16:33:11 +00:00
from app.main import main as main_blueprint
2015-11-20 16:22:44 +00:00
application.register_blueprint(main_blueprint)
2016-01-11 14:54:23 +00:00
from .status import status as status_blueprint
application.register_blueprint(status_blueprint)
2015-11-30 14:32:58 +00:00
proxy_fix.init_app(application)
2017-11-06 13:07:21 +00:00
add_template_filters(application)
2016-04-21 09:30:33 +01:00
2016-01-07 15:48:29 +00:00
register_errorhandlers(application)
2016-01-07 13:58:38 +00:00
setup_event_handlers()
2015-11-24 09:40:14 +00:00
def init_app(application):
2017-11-06 13:07:21 +00:00
application.after_request(useful_headers_after_request)
application.after_request(save_service_after_request)
application.before_request(load_service_before_request)
application.before_request(request_helper.check_proxy_header_before_request)
2017-11-06 13:07:21 +00:00
@application.before_request
def make_session_permanent():
# this is dumb. You'd think, given that there's `config['PERMANENT_SESSION_LIFETIME']`, that you'd enable
# permanent sessions in the config too - but no, you have to declare it for each request.
# https://stackoverflow.com/questions/34118093/flask-permanent-session-where-to-define-them
# session.permanent is also, helpfully, a way of saying that the session isn't permanent - in that, it will
# expire on its own, as opposed to being controlled by the browser's session. Because session is a proxy, it's
# only accessible from within a request context, so we need to set this before every request :rolls_eyes:
session.permanent = True
2017-11-06 13:07:21 +00:00
@application.context_processor
def _attach_current_service():
return {'current_service': current_service}
@application.context_processor
def _attach_current_user():
return{'current_user': current_user}
@application.before_request
def record_start_time():
g.start = monotonic()
2016-12-01 17:21:06 +00:00
g.endpoint = request.endpoint
@application.context_processor
2015-12-15 08:20:25 +00:00
def inject_global_template_variables():
2016-02-01 14:46:12 +00:00
return {
'asset_path': '/static/',
'header_colour': application.config['HEADER_COLOUR'],
2016-02-10 15:47:00 +00:00
'asset_url': asset_fingerprinter.get_url
2016-02-01 14:46:12 +00:00
}
2015-12-15 08:20:25 +00:00
2015-11-24 09:40:14 +00:00
def convert_to_boolean(value):
if isinstance(value, string_types):
if value.lower() in ['t', 'true', 'on', 'yes', '1']:
return True
elif value.lower() in ['f', 'false', 'off', 'no', '0']:
return False
return value
def linkable_name(value):
return urllib.parse.quote_plus(value)
2016-05-17 14:00:40 +01:00
def format_datetime(date):
2016-08-26 08:06:28 +01:00
return '{} at {}'.format(
format_date(date),
format_time(date)
)
def format_datetime_24h(date):
return '{} at {}'.format(
format_date(date),
format_time_24h(date),
2016-08-26 08:06:28 +01:00
)
2016-01-21 12:28:05 +00:00
def format_datetime_normal(date):
2016-08-26 08:06:28 +01:00
return '{} at {}'.format(
format_date_normal(date),
format_time(date)
)
def format_datetime_short(date):
2016-08-26 08:06:28 +01:00
return '{} at {}'.format(
format_date_short(date),
format_time(date)
)
def format_datetime_relative(date):
return '{} at {}'.format(
get_human_day(date),
format_time(date)
)
def format_datetime_numeric(date):
return '{} {}'.format(
format_date_numeric(date),
format_time_24h(date),
)
def format_date_numeric(date):
return gmt_timezones(date).strftime('%Y-%m-%d')
def format_time_24h(date):
return gmt_timezones(date).strftime('%H:%M')
def get_human_day(time):
2017-05-24 13:19:31 +01:00
# Add 1 hour to get midnight today instead of midnight tomorrow
2017-05-24 13:19:31 +01:00
time_as_day = (gmt_timezones(time) - timedelta(hours=1)).strftime('%A')
six_days_ago = gmt_timezones((datetime.utcnow() + timedelta(days=-6)).isoformat())
if gmt_timezones(time) < six_days_ago:
return format_date_short(time)
if time_as_day == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
return 'tomorrow'
2017-05-24 13:19:31 +01:00
if time_as_day == datetime.utcnow().strftime('%A'):
return 'today'
if time_as_day == (datetime.utcnow() + timedelta(days=-1)).strftime('%A'):
return 'yesterday'
return format_date_short(time)
2016-03-02 16:15:15 +00:00
def format_time(date):
2016-08-26 08:06:28 +01:00
return {
'12:00AM': 'Midnight',
'12:00PM': 'Midday'
}.get(
gmt_timezones(date).strftime('%-I:%M%p'),
gmt_timezones(date).strftime('%-I:%M%p')
).lower()
2016-03-02 16:15:15 +00:00
def format_date(date):
2016-05-17 14:00:40 +01:00
return gmt_timezones(date).strftime('%A %d %B %Y')
def format_date_normal(date):
return gmt_timezones(date).strftime('%d %B %Y').lstrip('0')
def format_date_short(date):
2016-05-17 14:00:40 +01:00
return gmt_timezones(date).strftime('%d %B').lstrip('0')
def format_delta(date):
2017-04-06 11:03:05 +01:00
delta = (
datetime.now(timezone.utc)
) - (
gmt_timezones(date)
)
if delta < timedelta(seconds=30):
return "just now"
2017-06-26 15:41:08 +01:00
if delta < timedelta(seconds=60):
return "in the last minute"
return ago.human(
2017-04-06 11:03:05 +01:00
delta,
future_tense='{} from now', # No-one should ever see this
past_tense='{} ago',
precision=1
)
2016-02-17 15:49:07 +00:00
def valid_phone_number(phone_number):
try:
validate_phone_number(phone_number)
return True
except InvalidPhoneError:
return False
2016-06-07 16:35:03 +01:00
def format_notification_status(status, template_type):
return {
'email': {
'failed': 'Failed',
'technical-failure': 'Technical failure',
'temporary-failure': 'Inbox not accepting messages right now',
'permanent-failure': 'Email address doesnt exist',
2016-06-07 16:35:03 +01:00
'delivered': 'Delivered',
'sending': 'Sending',
2017-04-27 16:02:49 +01:00
'created': 'Sending',
'sent': 'Delivered'
2016-06-07 16:35:03 +01:00
},
'sms': {
'failed': 'Failed',
'technical-failure': 'Technical failure',
'temporary-failure': 'Phone not accepting messages right now',
'permanent-failure': 'Phone number doesnt exist',
2016-06-07 16:35:03 +01:00
'delivered': 'Delivered',
'sending': 'Sending',
2017-04-27 16:02:49 +01:00
'created': 'Sending',
'sent': 'Sent internationally'
},
'letter': {
'failed': 'Failed',
'technical-failure': 'Technical failure',
'temporary-failure': 'Temporary failure',
'permanent-failure': 'Permanent failure',
'delivered': 'Delivered',
'sending': 'Sending',
2017-04-27 16:02:49 +01:00
'created': 'Sending',
'sent': 'Delivered'
2016-06-07 16:35:03 +01:00
}
}[template_type].get(status, status)
2016-09-09 15:57:05 +01:00
def format_notification_status_as_time(status, created, updated):
return {
2016-09-09 15:57:05 +01:00
'sending': ' since {}'.format(created),
'created': ' since {}'.format(created)
}.get(status, updated)
2016-06-09 10:15:37 +01:00
def format_notification_status_as_field_status(status):
return {
'failed': 'error',
'technical-failure': 'error',
'temporary-failure': 'error',
'permanent-failure': 'error',
'delivered': None,
2017-04-27 16:02:49 +01:00
'sent': None,
'sending': 'default',
'created': 'default'
2016-06-09 10:15:37 +01:00
}.get(status, 'error')
def format_notification_status_as_url(status):
2017-08-30 15:28:55 +01:00
url = partial(url_for, "main.using_notify")
return {
'technical-failure': url(_anchor='technical-failure'),
'temporary-failure': url(_anchor='not-accepting-messages'),
'permanent-failure': url(_anchor='does-not-exist')
}.get(status)
2017-03-02 15:56:28 +00:00
def nl2br(value):
return formatters.nl2br(value) if value else ''
2016-03-30 09:58:10 +01:00
@login_manager.user_loader
def load_user(user_id):
return user_api_client.get_user(user_id)
def load_service_before_request():
if '/static/' in request.url:
2017-05-04 11:28:45 +01:00
_request_ctx_stack.top.service = None
return
2016-04-13 16:19:34 +01:00
if _request_ctx_stack.top is not None:
_request_ctx_stack.top.service = None
if request.view_args:
service_id = request.view_args.get('service_id', session.get('service_id'))
else:
service_id = session.get('service_id')
if service_id:
try:
_request_ctx_stack.top.service = service_api_client.get_service(service_id)['data']
except HTTPError as exc:
# if service id isn't real, then 404 rather than 500ing later because we expect service to be set
if exc.status_code == 404:
abort(404)
else:
raise
def save_service_after_request(response):
# Only save the current session if the request is 200
service_id = request.view_args.get('service_id', None) if request.view_args else None
if response.status_code == 200 and service_id:
session['service_id'] = service_id
return response
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
2016-01-07 13:58:38 +00:00
def useful_headers_after_request(response):
response.headers.add('X-Frame-Options', 'deny')
response.headers.add('X-Content-Type-Options', 'nosniff')
response.headers.add('X-XSS-Protection', '1; mode=block')
2016-07-05 07:12:21 +01:00
response.headers.add('Content-Security-Policy', (
"default-src 'self' 'unsafe-inline';"
2016-10-05 10:55:04 +01:00
"script-src 'self' *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
2017-11-06 10:25:30 +00:00
"connect-src 'self' *.google-analytics.com;"
2016-07-05 07:12:21 +01:00
"object-src 'self';"
"font-src 'self' data:;"
2017-07-24 15:20:40 +01:00
"img-src 'self' *.google-analytics.com *.notifications.service.gov.uk {} data:;"
"frame-src www.youtube.com;".format(get_cdn_domain())
2016-07-05 07:12:21 +01:00
))
2016-02-02 14:02:10 +00:00
if 'Cache-Control' in response.headers:
del response.headers['Cache-Control']
response.headers.add(
'Cache-Control', 'no-store, no-cache, private, must-revalidate')
2016-01-07 13:58:38 +00:00
return response
2016-01-07 15:48:29 +00:00
2016-01-07 15:55:55 +00:00
def register_errorhandlers(application): # noqa (C901 too complex)
def _error_response(error_code):
resp = make_response(render_template("error/{0}.html".format(error_code)), error_code)
return useful_headers_after_request(resp)
@application.errorhandler(HTTPError)
def render_http_error(error):
2016-07-19 13:53:27 +01:00
application.logger.error("API {} failed with status {} message {}".format(
2016-07-21 17:32:28 +01:00
error.response.url if error.response else 'unknown',
2016-07-19 13:53:27 +01:00
error.status_code,
error.message
))
2016-03-11 10:16:06 +00:00
error_code = error.status_code
if error_code == 400:
if isinstance(error.message, str):
msg = [error.message]
else:
msg = list(itertools.chain(*[error.message[x] for x in error.message.keys()]))
resp = make_response(render_template("error/400.html", message=msg))
return useful_headers_after_request(resp)
elif error_code not in [401, 404, 403, 410, 500]:
error_code = 500
return _error_response(error_code)
2016-10-10 11:36:12 +01:00
@application.errorhandler(410)
def handle_gone(error):
return _error_response(410)
@application.errorhandler(404)
def handle_not_found(error):
return _error_response(404)
@application.errorhandler(403)
def handle_not_authorized(error):
return _error_response(403)
@application.errorhandler(401)
def handle_no_permissions(error):
return _error_response(401)
@application.errorhandler(BadSignature)
def handle_bad_token(error):
# if someone has a malformed token
flash('Theres something wrong with the link youve used.')
return _error_response(404)
@application.errorhandler(CSRFError)
def handle_csrf(reason):
application.logger.warning('csrf.error_message: {}'.format(reason))
if 'user_id' not in session:
application.logger.warning(
u'csrf.session_expired: Redirecting user to log in page'
)
return application.login_manager.unauthorized()
application.logger.warning(
u'csrf.invalid_token: Aborting request, user_id: {user_id}',
extra={'user_id': session['user_id']})
resp = make_response(render_template(
"error/400.html",
message=['Something went wrong, please go back and try again.']
), 400)
return useful_headers_after_request(resp)
2018-02-12 16:02:33 +00:00
@application.errorhandler(500)
@application.errorhandler(Exception)
def handle_bad_request(error):
current_app.logger.exception(error)
# We want the Flask in browser stacktrace
if current_app.config.get('DEBUG', None):
raise error
return _error_response(500)
def setup_event_handlers():
2017-07-26 11:02:57 +01:00
from flask_login import user_logged_in
2017-02-23 16:43:09 +00:00
from app.event_handlers import on_user_logged_in
user_logged_in.connect(on_user_logged_in)
2017-11-06 13:07:21 +00:00
def add_template_filters(application):
2017-11-09 15:54:49 +00:00
for fn in [
format_datetime,
format_datetime_24h,
format_datetime_normal,
format_datetime_short,
format_time,
valid_phone_number,
linkable_name,
format_date,
format_date_normal,
format_date_short,
format_datetime_relative,
format_delta,
format_notification_status,
format_notification_status_as_time,
format_notification_status_as_field_status,
format_notification_status_as_url,
formatted_list,
nl2br,
format_phone_number_human_readable,
]:
application.add_template_filter(fn)