mirror of
https://github.com/GSA/notifications-api.git
synced 2026-07-31 19:58:46 -04:00
Merge branch 'main' into stvnrlly-remove-broadcasts
This commit is contained in:
@@ -192,7 +192,7 @@ def register_blueprint(application):
|
||||
|
||||
status_blueprint.before_request(requires_no_auth)
|
||||
application.register_blueprint(status_blueprint)
|
||||
|
||||
|
||||
# delivery receipts
|
||||
ses_callback_blueprint.before_request(requires_no_auth)
|
||||
application.register_blueprint(ses_callback_blueprint)
|
||||
|
||||
@@ -18,7 +18,11 @@ from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from app.serialised_models import SerialisedService
|
||||
|
||||
GENERAL_TOKEN_ERROR_MESSAGE = 'Invalid token: make sure your API token matches the example at https://docs.notifications.service.gov.uk/rest-api.html#authorisation-header' # nosec B105
|
||||
# stvnrlly - this is silly, but bandit has a multiline string bug (https://github.com/PyCQA/bandit/issues/658)
|
||||
# and flake8 wants a multiline quote here. TODO: check on bug status and restore sanity once possible
|
||||
TOKEN_MESSAGE_ONE = "Invalid token: make sure your API token matches the example " # nosec B105
|
||||
TOKEN_MESSAGE_TWO = "at https://docs.notifications.service.gov.uk/rest-api.html#authorisation-header" # nosec B105
|
||||
GENERAL_TOKEN_ERROR_MESSAGE = TOKEN_MESSAGE_ONE + TOKEN_MESSAGE_TWO
|
||||
|
||||
AUTH_DB_CONNECTION_DURATION_SECONDS = Histogram(
|
||||
'auth_db_connection_duration_seconds',
|
||||
|
||||
@@ -10,18 +10,25 @@ default_access_key = os.environ.get('AWS_ACCESS_KEY_ID')
|
||||
default_secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
|
||||
default_region = os.environ.get('AWS_REGION')
|
||||
|
||||
def get_s3_file(bucket_name, file_location, access_key=default_access_key, secret_key=default_secret_key, region=default_region):
|
||||
|
||||
def get_s3_file(
|
||||
bucket_name, file_location, access_key=default_access_key, secret_key=default_secret_key, region=default_region
|
||||
):
|
||||
s3_file = get_s3_object(bucket_name, file_location, access_key, secret_key, region)
|
||||
return s3_file.get()['Body'].read().decode('utf-8')
|
||||
|
||||
|
||||
def get_s3_object(bucket_name, file_location, access_key=default_access_key, secret_key=default_secret_key, region=default_region):
|
||||
def get_s3_object(
|
||||
bucket_name, file_location, access_key=default_access_key, secret_key=default_secret_key, region=default_region
|
||||
):
|
||||
session = Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key, region_name=region)
|
||||
s3 = session.resource('s3')
|
||||
return s3.Object(bucket_name, file_location)
|
||||
|
||||
|
||||
def file_exists(bucket_name, file_location, access_key=default_access_key, secret_key=default_secret_key, region=default_region):
|
||||
def file_exists(
|
||||
bucket_name, file_location, access_key=default_access_key, secret_key=default_secret_key, region=default_region
|
||||
):
|
||||
try:
|
||||
# try and access metadata of object
|
||||
get_s3_object(bucket_name, file_location, access_key, secret_key, region).metadata
|
||||
|
||||
@@ -29,7 +29,10 @@ def process_ses_results(self, response):
|
||||
ses_message = json.loads(response["Message"])
|
||||
notification_type = ses_message["notificationType"]
|
||||
# TODO remove after smoke testing on prod is implemented
|
||||
current_app.logger.info(f"Attempting to process SES delivery status message from SNS with type: {notification_type} and body: {ses_message}")
|
||||
current_app.logger.info(
|
||||
f"Attempting to process SES delivery status message "
|
||||
f"from SNS with type: {notification_type} and body: {ses_message}"
|
||||
)
|
||||
bounce_message = None
|
||||
|
||||
if notification_type == 'Bounce':
|
||||
@@ -49,13 +52,16 @@ def process_ses_results(self, response):
|
||||
message_time = iso8601.parse_date(ses_message["mail"]["timestamp"]).replace(tzinfo=None)
|
||||
if datetime.utcnow() - message_time < timedelta(minutes=5):
|
||||
current_app.logger.info(
|
||||
f"notification not found for reference: {reference} (while attempting update to {notification_status}). "
|
||||
f"Callback may have arrived before notification was persisted to the DB. Adding task to retry queue"
|
||||
f"Notification not found for reference: {reference}"
|
||||
f"(while attempting update to {notification_status}). "
|
||||
f"Callback may have arrived before notification was"
|
||||
f"persisted to the DB. Adding task to retry queue"
|
||||
)
|
||||
self.retry(queue=QueueNames.RETRY)
|
||||
else:
|
||||
current_app.logger.warning(
|
||||
"notification not found for reference: {} (while attempting update to {})".format(reference, notification_status)
|
||||
f"Notification not found for reference: {reference} "
|
||||
f"(while attempting update to {notification_status})"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -64,7 +70,7 @@ def process_ses_results(self, response):
|
||||
|
||||
if notification.status not in {NOTIFICATION_SENDING, NOTIFICATION_PENDING}:
|
||||
notifications_dao._duplicate_update_warning(
|
||||
notification,
|
||||
notification,
|
||||
notification_status
|
||||
)
|
||||
return
|
||||
@@ -102,6 +108,7 @@ def process_ses_results(self, response):
|
||||
current_app.logger.exception("Error processing SES results: {}".format(type(e)))
|
||||
self.retry(queue=QueueNames.RETRY)
|
||||
|
||||
|
||||
def determine_notification_bounce_type(ses_message):
|
||||
notification_type = ses_message["notificationType"]
|
||||
if notification_type in ["Delivery", "Complaint"]:
|
||||
@@ -116,14 +123,16 @@ def determine_notification_bounce_type(ses_message):
|
||||
return "Permanent"
|
||||
return "Temporary"
|
||||
|
||||
|
||||
def determine_notification_type(ses_message):
|
||||
notification_type = ses_message["notificationType"]
|
||||
if notification_type not in ["Bounce","Complaint","Delivery"]:
|
||||
if notification_type not in ["Bounce", "Complaint", "Delivery"]:
|
||||
raise KeyError(f"Unhandled sns notification type {notification_type}")
|
||||
if notification_type == 'Bounce':
|
||||
return determine_notification_bounce_type(ses_message)
|
||||
return notification_type
|
||||
|
||||
|
||||
def _determine_provider_response(ses_message):
|
||||
if ses_message["notificationType"] != "Bounce":
|
||||
return None
|
||||
@@ -175,7 +184,9 @@ def get_aws_responses(ses_message):
|
||||
|
||||
def handle_complaint(ses_message):
|
||||
recipient_email = remove_emails_from_complaint(ses_message)[0]
|
||||
current_app.logger.info("Complaint from SES: \n{}".format(json.dumps(ses_message).replace("{", "(").replace("}", ")")))
|
||||
current_app.logger.info(
|
||||
"Complaint from SES: \n{}".format(json.dumps(ses_message).replace("{", "(").replace("}", ")"))
|
||||
)
|
||||
try:
|
||||
reference = ses_message["mail"]["messageId"]
|
||||
except KeyError as e:
|
||||
@@ -219,7 +230,9 @@ def check_and_queue_callback_task(notification):
|
||||
service_callback_api = get_service_delivery_status_callback_api_for_service(service_id=notification.service_id)
|
||||
if service_callback_api:
|
||||
notification_data = create_delivery_status_callback_data(notification, service_callback_api)
|
||||
send_delivery_status_to_service.apply_async([str(notification.id), notification_data], queue=QueueNames.CALLBACKS)
|
||||
send_delivery_status_to_service.apply_async(
|
||||
[str(notification.id), notification_data], queue=QueueNames.CALLBACKS
|
||||
)
|
||||
|
||||
|
||||
def _check_and_queue_complaint_callback_task(complaint, notification, recipient):
|
||||
@@ -228,4 +241,3 @@ def _check_and_queue_complaint_callback_task(complaint, notification, recipient)
|
||||
if service_callback_api:
|
||||
complaint_data = create_complaint_callback_data(complaint, notification, service_callback_api, recipient)
|
||||
send_complaint_to_service.apply_async([complaint_data], queue=QueueNames.CALLBACKS)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from flask import current_app
|
||||
from notifications_utils.template import SMSMessageTemplate
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def create_fake_letter_response_file(self, reference):
|
||||
dvla_response_data = '{}|Sent|0|Sorted'.format(reference)
|
||||
|
||||
# try and find a filename that hasn't been taken yet - from a random time within the last 30 seconds
|
||||
for i in sorted(range(30), key=lambda _: random.random()): # nosec B311 - not security related
|
||||
for i in sorted(range(30), key=lambda _: random.random()): # nosec B311 - not security related
|
||||
upload_file_name = 'NOTIFY-{}-RSP.TXT'.format((now - timedelta(seconds=i)).strftime('%Y%m%d%H%M%S'))
|
||||
if not file_exists(current_app.config['DVLA_RESPONSE_BUCKET_NAME'], upload_file_name):
|
||||
break
|
||||
|
||||
@@ -114,7 +114,7 @@ def create_delivery_status_callback_data(notification, service_callback_api):
|
||||
"notification_client_reference": notification.client_reference,
|
||||
"notification_to": notification.to,
|
||||
"notification_status": notification.status,
|
||||
"notification_provider_response": notification.provider_response, # TODO do we have a test for provider_response
|
||||
"notification_provider_response": notification.provider_response, # TODO do we test for provider_response?
|
||||
"notification_created_at": notification.created_at.strftime(DATETIME_FORMAT),
|
||||
"notification_updated_at":
|
||||
notification.updated_at.strftime(DATETIME_FORMAT) if notification.updated_at else None,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from celery import current_app
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
'''
|
||||
Base Exceptions for sending notifications that fail
|
||||
@@ -38,7 +35,7 @@ class NotificationProviderClients(object):
|
||||
return self.email_clients.get(name)
|
||||
|
||||
def get_client_by_name_and_type(self, name, notification_type):
|
||||
assert notification_type in ['email', 'sms'] # nosec B101
|
||||
assert notification_type in ['email', 'sms'] # nosec B101
|
||||
|
||||
if notification_type == 'email':
|
||||
return self.get_email_client(name)
|
||||
|
||||
@@ -25,4 +25,4 @@ class SmsClient(Client):
|
||||
raise NotImplementedError("TODO Need to implement.")
|
||||
|
||||
def get_name(self):
|
||||
raise NotImplementedError("TODO Need to implement.")
|
||||
raise NotImplementedError("TODO Need to implement.")
|
||||
|
||||
@@ -20,7 +20,7 @@ class AwsSnsClient(SmsClient):
|
||||
self.current_app = current_app
|
||||
self.statsd_client = statsd_client
|
||||
self.long_code_regex = re.compile(r"^\+1\d{10}$")
|
||||
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return 'sns'
|
||||
@@ -86,4 +86,4 @@ class AwsSnsClient(SmsClient):
|
||||
raise ValueError("No valid numbers found for SMS delivery")
|
||||
|
||||
def _send_with_dedicated_phone_number(self, sender):
|
||||
return sender and re.match(self.long_code_regex, sender)
|
||||
return sender and re.match(self.long_code_regex, sender)
|
||||
|
||||
@@ -13,9 +13,11 @@ def extract_cloudfoundry_config():
|
||||
vcap_services = json.loads(os.environ['VCAP_SERVICES'])
|
||||
|
||||
# Postgres config
|
||||
os.environ['SQLALCHEMY_DATABASE_URI'] = vcap_services['aws-rds'][0]['credentials']['uri'].replace('postgres','postgresql')
|
||||
os.environ['SQLALCHEMY_DATABASE_URI'] = \
|
||||
vcap_services['aws-rds'][0]['credentials']['uri'].replace('postgres', 'postgresql')
|
||||
# Redis config
|
||||
os.environ['REDIS_URL'] = vcap_services['aws-elasticache-redis'][0]['credentials']['uri'].replace('redis://','rediss://')
|
||||
os.environ['REDIS_URL'] = \
|
||||
vcap_services['aws-elasticache-redis'][0]['credentials']['uri'].replace('redis://', 'rediss://')
|
||||
|
||||
# CSV Upload Bucket Name
|
||||
bucket_service = find_by_service_name(
|
||||
|
||||
@@ -140,86 +140,6 @@ def purge_functional_test_data(user_email_prefix):
|
||||
delete_model_user(usr)
|
||||
|
||||
|
||||
@notify_command()
|
||||
def backfill_notification_statuses():
|
||||
"""
|
||||
DEPRECATED. Populates notification_status.
|
||||
|
||||
This will be used to populate the new `Notification._status_fkey` with the old
|
||||
`Notification._status_enum`
|
||||
"""
|
||||
LIMIT = 250000
|
||||
subq = "SELECT id FROM notification_history WHERE notification_status is NULL LIMIT {}".format(LIMIT) # nosec B608 no user-controlled input
|
||||
update = "UPDATE notification_history SET notification_status = status WHERE id in ({})".format(subq) # nosec B608 no user-controlled input
|
||||
result = db.session.execute(subq).fetchall()
|
||||
|
||||
while len(result) > 0:
|
||||
db.session.execute(update)
|
||||
print('commit {} updates at {}'.format(LIMIT, datetime.utcnow()))
|
||||
db.session.commit()
|
||||
result = db.session.execute(subq).fetchall()
|
||||
|
||||
|
||||
@notify_command()
|
||||
def update_notification_international_flag():
|
||||
"""
|
||||
DEPRECATED. Set notifications.international=false.
|
||||
"""
|
||||
# 250,000 rows takes 30 seconds to update.
|
||||
subq = "select id from notifications where international is null limit 250000"
|
||||
update = "update notifications set international = False where id in ({})".format(subq) # nosec B608 no user-controlled input
|
||||
result = db.session.execute(subq).fetchall()
|
||||
|
||||
while len(result) > 0:
|
||||
db.session.execute(update)
|
||||
print('commit 250000 updates at {}'.format(datetime.utcnow()))
|
||||
db.session.commit()
|
||||
result = db.session.execute(subq).fetchall()
|
||||
|
||||
# Now update notification_history
|
||||
subq_history = "select id from notification_history where international is null limit 250000"
|
||||
update_history = "update notification_history set international = False where id in ({})".format(subq_history) # nosec B608 no user-controlled input
|
||||
result_history = db.session.execute(subq_history).fetchall()
|
||||
while len(result_history) > 0:
|
||||
db.session.execute(update_history)
|
||||
print('commit 250000 updates at {}'.format(datetime.utcnow()))
|
||||
db.session.commit()
|
||||
result_history = db.session.execute(subq_history).fetchall()
|
||||
|
||||
|
||||
@notify_command()
|
||||
def fix_notification_statuses_not_in_sync():
|
||||
"""
|
||||
DEPRECATED.
|
||||
This will be used to correct an issue where Notification._status_enum and NotificationHistory._status_fkey
|
||||
became out of sync. See 979e90a.
|
||||
|
||||
Notification._status_enum is the source of truth so NotificationHistory._status_fkey will be updated with
|
||||
these values.
|
||||
"""
|
||||
MAX = 10000
|
||||
|
||||
subq = "SELECT id FROM notifications WHERE cast (status as text) != notification_status LIMIT {}".format(MAX) # nosec B608 no user-controlled input
|
||||
update = "UPDATE notifications SET notification_status = status WHERE id in ({})".format(subq) # nosec B608 no user-controlled input
|
||||
result = db.session.execute(subq).fetchall()
|
||||
|
||||
while len(result) > 0:
|
||||
db.session.execute(update)
|
||||
print('Committed {} updates at {}'.format(len(result), datetime.utcnow()))
|
||||
db.session.commit()
|
||||
result = db.session.execute(subq).fetchall()
|
||||
|
||||
subq_hist = "SELECT id FROM notification_history WHERE cast (status as text) != notification_status LIMIT {}".format(MAX) # nosec B608
|
||||
update = "UPDATE notification_history SET notification_status = status WHERE id in ({})".format(subq_hist) # nosec B608 no user-controlled input
|
||||
result = db.session.execute(subq_hist).fetchall()
|
||||
|
||||
while len(result) > 0:
|
||||
db.session.execute(update)
|
||||
print('Committed {} updates at {}'.format(len(result), datetime.utcnow()))
|
||||
db.session.commit()
|
||||
result = db.session.execute(subq_hist).fetchall()
|
||||
|
||||
|
||||
@notify_command(name='insert-inbound-numbers')
|
||||
@click.option('-f', '--file_name', required=True,
|
||||
help="""Full path of the file to upload, file is a contains inbound numbers,
|
||||
|
||||
@@ -79,7 +79,7 @@ class Config(object):
|
||||
|
||||
INTERNAL_CLIENT_API_KEYS = json.loads(
|
||||
os.environ.get('INTERNAL_CLIENT_API_KEYS', '{"notify-admin":["dev-notify-secret-key"]}')
|
||||
) # TODO: handled by varsfile?
|
||||
) # TODO: handled by varsfile?
|
||||
|
||||
# encyption secret/salt
|
||||
ADMIN_CLIENT_SECRET = os.environ.get('ADMIN_CLIENT_SECRET')
|
||||
@@ -99,13 +99,13 @@ class Config(object):
|
||||
# Firetext API Key
|
||||
FIRETEXT_API_KEY = os.environ.get("FIRETEXT_API_KEY", "placeholder")
|
||||
FIRETEXT_INTERNATIONAL_API_KEY = os.environ.get("FIRETEXT_INTERNATIONAL_API_KEY", "placeholder")
|
||||
|
||||
|
||||
# Whether to ignore POSTs from SNS for replies to SMS we sent
|
||||
RECEIVE_INBOUND_SMS = False
|
||||
|
||||
# Use notify.sandbox.10x sending domain unless overwritten by environment
|
||||
NOTIFY_EMAIL_DOMAIN = 'notify.sandbox.10x.gsa.gov'
|
||||
|
||||
|
||||
# AWS SNS topics for delivery receipts
|
||||
VALIDATE_SNS_TOPICS = True
|
||||
VALID_SNS_TOPICS = ['notify_test_bounce', 'notify_test_success', 'notify_test_complaint', 'notify_test_sms_inbound']
|
||||
@@ -402,7 +402,7 @@ class Development(Config):
|
||||
# Config.ADMIN_CLIENT_ID: ['dev-notify-secret-key'],
|
||||
# }
|
||||
|
||||
SECRET_KEY = 'dev-notify-secret-key' # nosec B105 - this is only used in development
|
||||
SECRET_KEY = 'dev-notify-secret-key' # nosec B105 - this is only used in development
|
||||
DANGEROUS_SALT = 'dev-notify-salt'
|
||||
|
||||
MMG_INBOUND_SMS_AUTH = ['testkey']
|
||||
@@ -413,7 +413,10 @@ class Development(Config):
|
||||
|
||||
NOTIFY_EMAIL_DOMAIN = os.getenv('NOTIFY_EMAIL_DOMAIN', 'notify.sandbox.10x.gsa.gov')
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://postgres:chummy@db:5432/notification_api')
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||
'SQLALCHEMY_DATABASE_URI',
|
||||
'postgresql://postgres:chummy@db:5432/notification_api'
|
||||
)
|
||||
|
||||
ANTIVIRUS_ENABLED = os.environ.get('ANTIVIRUS_ENABLED') == '1'
|
||||
|
||||
@@ -449,7 +452,10 @@ class Test(Development):
|
||||
# LETTER_SANITISE_BUCKET_NAME = 'test-letters-sanitise'
|
||||
|
||||
# this is overriden in CI
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_TEST_URI', 'postgresql://postgres:chummy@db:5432/test_notification_api')
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv(
|
||||
'SQLALCHEMY_DATABASE_TEST_URI',
|
||||
'postgresql://postgres:chummy@db:5432/test_notification_api'
|
||||
)
|
||||
|
||||
CELERY = {
|
||||
**Config.CELERY,
|
||||
@@ -508,11 +514,17 @@ class Staging(Config):
|
||||
class Live(Config):
|
||||
NOTIFY_ENVIRONMENT = 'live'
|
||||
# buckets
|
||||
CSV_UPLOAD_BUCKET_NAME = os.environ.get('CSV_UPLOAD_BUCKET_NAME', 'notifications-prototype-csv-upload') # created in gsa sandbox
|
||||
CSV_UPLOAD_BUCKET_NAME = os.environ.get(
|
||||
'CSV_UPLOAD_BUCKET_NAME',
|
||||
'notifications-prototype-csv-upload'
|
||||
) # created in gsa sandbox
|
||||
CSV_UPLOAD_ACCESS_KEY = os.environ.get('CSV_UPLOAD_ACCESS_KEY')
|
||||
CSV_UPLOAD_SECRET_KEY = os.environ.get('CSV_UPLOAD_SECRET_KEY')
|
||||
CSV_UPLOAD_REGION = os.environ.get('CSV_UPLOAD_REGION')
|
||||
CONTACT_LIST_BUCKET_NAME = os.environ.get('CONTACT_LIST_BUCKET_NAME', 'notifications-prototype-contact-list-upload') # created in gsa sandbox
|
||||
CONTACT_LIST_BUCKET_NAME = os.environ.get(
|
||||
'CONTACT_LIST_BUCKET_NAME',
|
||||
'notifications-prototype-contact-list-upload'
|
||||
) # created in gsa sandbox
|
||||
CONTACT_LIST_ACCESS_KEY = os.environ.get('CONTACT_LIST_ACCESS_KEY')
|
||||
CONTACT_LIST_SECRET_KEY = os.environ.get('CONTACT_LIST_SECRET_KEY')
|
||||
CONTACT_LIST_REGION = os.environ.get('CONTACT_LIST_REGION')
|
||||
|
||||
@@ -87,6 +87,7 @@ def country_records_delivery(phone_prefix):
|
||||
dlr = INTERNATIONAL_BILLING_RATES[phone_prefix]['attributes']['dlr']
|
||||
return dlr and dlr.lower() == 'yes'
|
||||
|
||||
|
||||
def _decide_permanent_temporary_failure(current_status, status):
|
||||
# If we go from pending to delivered we need to set failure type as temporary-failure
|
||||
if current_status == NOTIFICATION_PENDING and status == NOTIFICATION_PERMANENT_FAILURE:
|
||||
@@ -102,6 +103,7 @@ def _update_notification_status(notification, status, provider_response=None):
|
||||
dao_update_notification(notification)
|
||||
return notification
|
||||
|
||||
|
||||
@autocommit
|
||||
def update_notification_status_by_id(notification_id, status, sent_by=None, detailed_status_code=None):
|
||||
notification = Notification.query.with_for_update().filter(Notification.id == notification_id).first()
|
||||
|
||||
@@ -9,8 +9,6 @@ from sqlalchemy.sql.expression import and_, asc, case, func
|
||||
from app import db
|
||||
from app.dao.dao_utils import VersionOptions, autocommit, version_class
|
||||
from app.dao.date_util import get_current_financial_year
|
||||
from app.dao.email_branding_dao import dao_get_email_branding_by_name
|
||||
from app.dao.letter_branding_dao import dao_get_letter_branding_by_name
|
||||
from app.dao.organisation_dao import dao_get_organisation_by_email_address
|
||||
from app.dao.service_sms_sender_dao import insert_service_sms_sender
|
||||
from app.dao.service_user_dao import dao_get_service_user
|
||||
@@ -47,7 +45,6 @@ from app.models import (
|
||||
VerifyCode,
|
||||
)
|
||||
from app.utils import (
|
||||
email_address_is_nhs,
|
||||
escape_special_characters,
|
||||
get_archived_db_column_value,
|
||||
get_london_midnight_in_utc,
|
||||
|
||||
@@ -166,7 +166,7 @@ def update_notification_to_sending(notification, provider):
|
||||
# We currently have no callback method for SMS deliveries
|
||||
# TODO create celery task to request SMS delivery receipts from cloudwatch api
|
||||
notification.status = NOTIFICATION_SENT if notification.notification_type == "sms" else NOTIFICATION_SENDING
|
||||
|
||||
|
||||
dao_update_notification(notification)
|
||||
|
||||
|
||||
@@ -175,10 +175,12 @@ provider_cache = TTLCache(maxsize=8, ttl=10)
|
||||
|
||||
@cached(cache=provider_cache)
|
||||
def provider_to_use(notification_type, international=True):
|
||||
international = False # TODO: remove or resolve the functionality of this flag
|
||||
international = False # TODO: remove or resolve the functionality of this flag
|
||||
# TODO rip firetext and mmg out of early migrations and clean up the expression below
|
||||
active_providers = [
|
||||
p for p in get_provider_details_by_notification_type(notification_type, international) if p.active and p.identifier not in ['firetext','mmg']
|
||||
p for p in get_provider_details_by_notification_type(
|
||||
notification_type, international
|
||||
) if p.active and p.identifier not in ['firetext', 'mmg']
|
||||
]
|
||||
|
||||
if not active_providers:
|
||||
@@ -191,7 +193,7 @@ def provider_to_use(notification_type, international=True):
|
||||
chosen_provider = active_providers[0]
|
||||
else:
|
||||
weights = [p.priority for p in active_providers]
|
||||
chosen_provider = random.choices(active_providers, weights=weights)[0] # nosec B311 - this is not security/cryptography related
|
||||
chosen_provider = random.choices(active_providers, weights=weights)[0] # nosec B311 - not sec/crypto related
|
||||
|
||||
return notification_provider_clients.get_client_by_name_and_type(chosen_provider.identifier, notification_type)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from notifications_utils.recipients import try_validate_and_format_phone_number
|
||||
|
||||
from app.dao.inbound_sms_dao import (
|
||||
dao_count_inbound_sms_for_service,
|
||||
|
||||
@@ -120,7 +120,9 @@ class User(db.Model):
|
||||
state = db.Column(db.String, nullable=False, default='pending')
|
||||
platform_admin = db.Column(db.Boolean, nullable=False, default=False)
|
||||
current_session_id = db.Column(UUID(as_uuid=True), nullable=True)
|
||||
auth_type = db.Column(db.String, db.ForeignKey('auth_type.name'), index=True, nullable=False, default=EMAIL_AUTH_TYPE)
|
||||
auth_type = db.Column(
|
||||
db.String, db.ForeignKey('auth_type.name'), index=True, nullable=False, default=EMAIL_AUTH_TYPE
|
||||
)
|
||||
email_access_validated_at = db.Column(
|
||||
db.DateTime, index=False, unique=False, nullable=False, default=datetime.datetime.utcnow
|
||||
)
|
||||
@@ -594,7 +596,7 @@ class AnnualBilling(db.Model):
|
||||
"name": self.service.name
|
||||
}
|
||||
|
||||
return{
|
||||
return {
|
||||
"id": str(self.id),
|
||||
'free_sms_fragment_limit': self.free_sms_fragment_limit,
|
||||
'service_id': self.service_id,
|
||||
@@ -1628,7 +1630,7 @@ class Notification(db.Model):
|
||||
"""
|
||||
# this should only ever be called for letter notifications - it makes no sense otherwise and I'd rather not
|
||||
# get the two code flows mixed up at all
|
||||
assert self.notification_type == LETTER_TYPE # nosec B101 - current calling code already validates the correct type
|
||||
assert self.notification_type == LETTER_TYPE # nosec B101 - current calling code validates correct type
|
||||
|
||||
if self.status in [NOTIFICATION_CREATED, NOTIFICATION_SENDING]:
|
||||
return NOTIFICATION_STATUS_LETTER_ACCEPTED
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.notifications.sns_handlers import sns_notification_handler
|
||||
ses_callback_blueprint = Blueprint('notifications_ses_callback', __name__)
|
||||
DEFAULT_MAX_AGE = timedelta(days=10000)
|
||||
|
||||
|
||||
# 400 counts as a permanent failure so SNS will not retry.
|
||||
# 500 counts as a failed delivery attempt so SNS will retry.
|
||||
# See https://docs.aws.amazon.com/sns/latest/dg/DeliveryPolicies.html#DeliveryPolicies
|
||||
@@ -21,7 +22,7 @@ def email_ses_callback_handler():
|
||||
return jsonify(
|
||||
result="error", message=str(e.message)
|
||||
), e.status_code
|
||||
|
||||
|
||||
message = data.get("Message")
|
||||
if "mail" in message:
|
||||
process_ses_results.apply_async([{"Message": message}], queue=QueueNames.NOTIFY)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from flask import Blueprint, json, jsonify, request
|
||||
from flask import Blueprint
|
||||
|
||||
# from app.celery.process_sms_client_response_tasks import (
|
||||
# process_sms_client_response,
|
||||
# )
|
||||
from app.config import QueueNames
|
||||
from app.errors import InvalidRequest, register_errors
|
||||
from app.errors import register_errors
|
||||
|
||||
sms_callback_blueprint = Blueprint("sms_callback", __name__, url_prefix="/notifications/sms")
|
||||
register_errors(sms_callback_blueprint)
|
||||
|
||||
@@ -106,13 +106,13 @@ def persist_notification(
|
||||
updated_at=None
|
||||
):
|
||||
current_app.logger.info('Presisting notification')
|
||||
|
||||
|
||||
notification_created_at = created_at or datetime.utcnow()
|
||||
if not notification_id:
|
||||
notification_id = uuid.uuid4()
|
||||
|
||||
|
||||
current_app.logger.info('Presisting notification with id {}'.format(notification_id))
|
||||
|
||||
|
||||
notification = Notification(
|
||||
id=notification_id,
|
||||
template_id=template_id,
|
||||
@@ -135,7 +135,7 @@ def persist_notification(
|
||||
document_download_count=document_download_count,
|
||||
updated_at=updated_at
|
||||
)
|
||||
|
||||
|
||||
current_app.logger.info('Presisting notification with to address: {}'.format(notification.to))
|
||||
|
||||
if notification_type == SMS_TYPE:
|
||||
|
||||
@@ -24,6 +24,7 @@ INBOUND_SMS_COUNTER = Counter(
|
||||
['provider']
|
||||
)
|
||||
|
||||
|
||||
@receive_notifications_blueprint.route('/notifications/sms/receive/sns', methods=['POST'])
|
||||
def receive_sns_sms():
|
||||
"""
|
||||
@@ -37,13 +38,13 @@ def receive_sns_sms():
|
||||
"previousPublishedMessageId":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Whether or not to ignore inbound SMS replies
|
||||
if not current_app.config['RECEIVE_INBOUND_SMS']:
|
||||
return jsonify(
|
||||
result="success", message="SMS-SNS callback succeeded"
|
||||
), 200
|
||||
|
||||
|
||||
try:
|
||||
post_data = sns_notification_handler(request.data, request.headers)
|
||||
except Exception as e:
|
||||
@@ -53,13 +54,16 @@ def receive_sns_sms():
|
||||
# TODO wrap this up
|
||||
if "inboundMessageId" in message:
|
||||
# TODO use standard formatting we use for all US numbers
|
||||
inbound_number = message['destinationNumber'].replace('+','')
|
||||
inbound_number = message['destinationNumber'].replace('+', '')
|
||||
|
||||
service = fetch_potential_service(inbound_number, 'sns')
|
||||
if not service:
|
||||
# since this is an issue with our service <-> number mapping, or no inbound_sms service permission
|
||||
# we should still tell SNS that we received it successfully
|
||||
current_app.logger.warning(f"Mapping between service and inbound number: {inbound_number} is broken, or service does not have permission to receive inbound sms")
|
||||
current_app.logger.warning(
|
||||
f"Mapping between service and inbound number: {inbound_number} is broken, "
|
||||
f"or service does not have permission to receive inbound sms"
|
||||
)
|
||||
return jsonify(
|
||||
result="success", message="SMS-SNS callback succeeded"
|
||||
), 200
|
||||
@@ -79,7 +83,6 @@ def receive_sns_sms():
|
||||
date_received=date_received,
|
||||
provider_name=provider_name)
|
||||
|
||||
# TODO ensure inbound sms callback endpoints are accessible and functioning for notify api users, then uncomment the task below
|
||||
tasks.send_inbound_sms_to_service.apply_async([str(inbound.id), str(service.id)], queue=QueueNames.NOTIFY)
|
||||
|
||||
current_app.logger.debug(
|
||||
|
||||
@@ -19,6 +19,7 @@ _cert_url_re = re.compile(
|
||||
r'sns\.([a-z]{1,3}-[a-z]+-[0-9]{1,2})\.amazonaws\.com',
|
||||
)
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""
|
||||
ValidationError. Raised when a message fails integrity checks.
|
||||
@@ -56,7 +57,7 @@ def get_string_to_sign(sns_payload):
|
||||
for field in fields:
|
||||
field_value = sns_payload.get(field)
|
||||
if not isinstance(field_value, str):
|
||||
if field == 'Subject' and field_value == None:
|
||||
if field == 'Subject' and field_value is None:
|
||||
continue
|
||||
raise ValidationError(f"In {field}, found non-string value: {field_value}")
|
||||
string_to_sign += field + '\n' + field_value + '\n'
|
||||
@@ -77,25 +78,26 @@ def validate_sns_cert(sns_payload):
|
||||
# Amazon SNS currently supports signature version 1.
|
||||
if sns_payload.get('SignatureVersion') != '1':
|
||||
raise ValidationError("Wrong Signature Version (expected 1)")
|
||||
|
||||
|
||||
validate_arn(sns_payload)
|
||||
|
||||
|
||||
string_to_sign = get_string_to_sign(sns_payload)
|
||||
|
||||
# Key signing cert url via Lambda and via webhook are slightly different
|
||||
signing_cert_url = sns_payload.get('SigningCertUrl') if 'SigningCertUrl' in sns_payload else sns_payload.get('SigningCertURL')
|
||||
signing_cert_url = sns_payload.get('SigningCertUrl') if 'SigningCertUrl' in \
|
||||
sns_payload else sns_payload.get('SigningCertURL')
|
||||
if not isinstance(signing_cert_url, str):
|
||||
raise ValidationError("Signing cert url must be a string")
|
||||
cert_scheme, cert_netloc, *_ = urlparse(signing_cert_url)
|
||||
if cert_scheme != 'https' or not re.match(_cert_url_re, cert_netloc):
|
||||
raise ValidationError("Cert does not appear to be from AWS")
|
||||
|
||||
|
||||
certificate = _signing_cert_cache.get(signing_cert_url)
|
||||
if certificate is None:
|
||||
certificate = get_certificate(signing_cert_url)
|
||||
if isinstance(certificate, six.text_type):
|
||||
certificate = certificate.encode()
|
||||
|
||||
|
||||
signature = base64.b64decode(sns_payload["Signature"])
|
||||
|
||||
try:
|
||||
@@ -107,4 +109,4 @@ def validate_sns_cert(sns_payload):
|
||||
)
|
||||
return True
|
||||
except oscrypto.errors.SignatureError:
|
||||
raise ValidationError("Invalid signature")
|
||||
raise ValidationError("Invalid signature")
|
||||
|
||||
@@ -45,7 +45,9 @@ def sns_notification_handler(data, headers):
|
||||
try:
|
||||
validate_sns_cert(message)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"SES-SNS callback failed: validation failed with error: Signature validation failed with error {e}")
|
||||
current_app.logger.error(
|
||||
f"SES-SNS callback failed: validation failed with error: Signature validation failed with error {e}"
|
||||
)
|
||||
raise InvalidRequest("SES-SNS callback failed: validation failed", 400)
|
||||
|
||||
if message.get('Type') == 'SubscriptionConfirmation':
|
||||
@@ -55,12 +57,18 @@ def sns_notification_handler(data, headers):
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"Attempt to raise_for_status()SubscriptionConfirmation Type message files for response: {response.text} with error {e}")
|
||||
raise InvalidRequest("SES-SNS callback failed: attempt to raise_for_status()SubscriptionConfirmation Type message failed", 400)
|
||||
current_app.logger.warning(
|
||||
f"Attempt to raise_for_status()SubscriptionConfirmation Type "
|
||||
f"message files for response: {response.text} with error {e}"
|
||||
)
|
||||
raise InvalidRequest(
|
||||
"SES-SNS callback failed: attempt to raise_for_status()SubscriptionConfirmation "
|
||||
"Type message failed", 400
|
||||
)
|
||||
current_app.logger.info("SES-SNS auto-confirm subscription callback succeeded")
|
||||
return message
|
||||
|
||||
# TODO remove after smoke testing on prod is implemented
|
||||
current_app.logger.info(f"SNS message: {message} is a valid message. Attempting to process it now.")
|
||||
|
||||
|
||||
return message
|
||||
|
||||
@@ -103,8 +103,6 @@ def update_organisation(organisation_id):
|
||||
data = request.get_json()
|
||||
validate(data, post_update_organisation_schema)
|
||||
|
||||
organisation = dao_get_organisation_by_id(organisation_id)
|
||||
|
||||
result = dao_update_organisation(organisation_id, **data)
|
||||
|
||||
if data.get('agreement_signed') is True:
|
||||
|
||||
@@ -404,7 +404,7 @@ def send_new_user_email_verification(user_id):
|
||||
current_app.logger.info('Sending notification to queue')
|
||||
|
||||
send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY)
|
||||
|
||||
|
||||
current_app.logger.info('Sent notification to queue')
|
||||
|
||||
return jsonify({}), 204
|
||||
@@ -414,12 +414,12 @@ def send_new_user_email_verification(user_id):
|
||||
def send_already_registered_email(user_id):
|
||||
current_app.logger.info('Email already registered for user {}'.format(user_id))
|
||||
to = email_data_request_schema.load(request.get_json())
|
||||
|
||||
|
||||
current_app.logger.info('To email is {}'.format(to['email']))
|
||||
|
||||
template = dao_get_template_by_id(current_app.config['ALREADY_REGISTERED_EMAIL_TEMPLATE_ID'])
|
||||
service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID'])
|
||||
|
||||
|
||||
current_app.logger.info('template.id is {}'.format(template.id))
|
||||
current_app.logger.info('service.id is {}'.format(service.id))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user