mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-20 15:31:15 -05:00
notify-api-412 use black to enforce python style standards
This commit is contained in:
@@ -7,26 +7,22 @@ from app.config import QueueNames
|
||||
from app.errors import InvalidRequest
|
||||
from app.notifications.sns_handlers import sns_notification_handler
|
||||
|
||||
ses_callback_blueprint = Blueprint('notifications_ses_callback', __name__)
|
||||
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
|
||||
@ses_callback_blueprint.route('/notifications/email/ses', methods=['POST'])
|
||||
@ses_callback_blueprint.route("/notifications/email/ses", methods=["POST"])
|
||||
def email_ses_callback_handler():
|
||||
try:
|
||||
data = sns_notification_handler(request.data, request.headers)
|
||||
except InvalidRequest as e:
|
||||
return jsonify(
|
||||
result="error", message=str(e.message)
|
||||
), e.status_code
|
||||
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)
|
||||
|
||||
return jsonify(
|
||||
result="success", message="SES-SNS callback succeeded"
|
||||
), 200
|
||||
return jsonify(result="success", message="SES-SNS callback succeeded"), 200
|
||||
|
||||
@@ -34,17 +34,17 @@ def create_content_for_notification(template, personalisation):
|
||||
if template.template_type == EMAIL_TYPE:
|
||||
template_object = PlainTextEmailTemplate(
|
||||
{
|
||||
'content': template.content,
|
||||
'subject': template.subject,
|
||||
'template_type': template.template_type,
|
||||
"content": template.content,
|
||||
"subject": template.subject,
|
||||
"template_type": template.template_type,
|
||||
},
|
||||
personalisation,
|
||||
)
|
||||
if template.template_type == SMS_TYPE:
|
||||
template_object = SMSMessageTemplate(
|
||||
{
|
||||
'content': template.content,
|
||||
'template_type': template.template_type,
|
||||
"content": template.content,
|
||||
"template_type": template.template_type,
|
||||
},
|
||||
personalisation,
|
||||
)
|
||||
@@ -56,8 +56,10 @@ def create_content_for_notification(template, personalisation):
|
||||
|
||||
def check_placeholders(template_object):
|
||||
if template_object.missing_data:
|
||||
message = 'Missing personalisation: {}'.format(", ".join(template_object.missing_data))
|
||||
raise BadRequestError(fields=[{'template': message}], message=message)
|
||||
message = "Missing personalisation: {}".format(
|
||||
", ".join(template_object.missing_data)
|
||||
)
|
||||
raise BadRequestError(fields=[{"template": message}], message=message)
|
||||
|
||||
|
||||
def persist_notification(
|
||||
@@ -84,13 +86,15 @@ def persist_notification(
|
||||
document_download_count=None,
|
||||
updated_at=None
|
||||
):
|
||||
current_app.logger.info('Persisting notification')
|
||||
current_app.logger.info("Persisting notification")
|
||||
|
||||
notification_created_at = created_at or datetime.utcnow()
|
||||
if not notification_id:
|
||||
notification_id = uuid.uuid4()
|
||||
|
||||
current_app.logger.info('Persisting notification with id {}'.format(notification_id))
|
||||
current_app.logger.info(
|
||||
"Persisting notification with id {}".format(notification_id)
|
||||
)
|
||||
|
||||
notification = Notification(
|
||||
id=notification_id,
|
||||
@@ -112,37 +116,49 @@ def persist_notification(
|
||||
reply_to_text=reply_to_text,
|
||||
billable_units=billable_units,
|
||||
document_download_count=document_download_count,
|
||||
updated_at=updated_at
|
||||
updated_at=updated_at,
|
||||
)
|
||||
|
||||
if notification_type == SMS_TYPE:
|
||||
formatted_recipient = validate_and_format_phone_number(recipient, international=True)
|
||||
formatted_recipient = validate_and_format_phone_number(
|
||||
recipient, international=True
|
||||
)
|
||||
recipient_info = get_international_phone_info(formatted_recipient)
|
||||
notification.normalised_to = formatted_recipient
|
||||
notification.international = recipient_info.international
|
||||
notification.phone_prefix = recipient_info.country_prefix
|
||||
notification.rate_multiplier = recipient_info.billable_units
|
||||
elif notification_type == EMAIL_TYPE:
|
||||
current_app.logger.info('Persisting notification with type: {}'.format(EMAIL_TYPE))
|
||||
current_app.logger.info(
|
||||
"Persisting notification with type: {}".format(EMAIL_TYPE)
|
||||
)
|
||||
notification.normalised_to = format_email_address(notification.to)
|
||||
|
||||
# if simulated create a Notification model to return but do not persist the Notification to the dB
|
||||
if not simulated:
|
||||
current_app.logger.info('Firing dao_create_notification')
|
||||
current_app.logger.info("Firing dao_create_notification")
|
||||
dao_create_notification(notification)
|
||||
if key_type != KEY_TYPE_TEST and current_app.config['REDIS_ENABLED']:
|
||||
current_app.logger.info('Redis enabled, querying cache key for service id: {}'.format(service.id))
|
||||
if key_type != KEY_TYPE_TEST and current_app.config["REDIS_ENABLED"]:
|
||||
current_app.logger.info(
|
||||
"Redis enabled, querying cache key for service id: {}".format(
|
||||
service.id
|
||||
)
|
||||
)
|
||||
total_key = redis.daily_total_cache_key()
|
||||
if redis_store.get(total_key) is None:
|
||||
current_app.logger.info('Redis daily total cache key does not exist')
|
||||
current_app.logger.info("Redis daily total cache key does not exist")
|
||||
redis_store.set(total_key, 1, ex=86400)
|
||||
current_app.logger.info('Set redis daily total cache key to 1')
|
||||
current_app.logger.info("Set redis daily total cache key to 1")
|
||||
else:
|
||||
current_app.logger.info('Redis total limit cache key does exist')
|
||||
current_app.logger.info("Redis total limit cache key does exist")
|
||||
redis_store.incr(total_key)
|
||||
current_app.logger.info('Redis total limit cache key has been incremented')
|
||||
current_app.logger.info(
|
||||
"Redis total limit cache key has been incremented"
|
||||
)
|
||||
current_app.logger.info(
|
||||
"{} {} created at {}".format(notification_type, notification_id, notification_created_at)
|
||||
"{} {} created at {}".format(
|
||||
notification_type, notification_id, notification_created_at
|
||||
)
|
||||
)
|
||||
return notification
|
||||
|
||||
@@ -169,22 +185,28 @@ def send_notification_to_queue_detached(
|
||||
raise
|
||||
|
||||
current_app.logger.debug(
|
||||
"{} {} sent to the {} queue for delivery".format(notification_type,
|
||||
notification_id,
|
||||
queue))
|
||||
"{} {} sent to the {} queue for delivery".format(
|
||||
notification_type, notification_id, queue
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def send_notification_to_queue(notification, research_mode, queue=None):
|
||||
send_notification_to_queue_detached(
|
||||
notification.key_type, notification.notification_type, notification.id, research_mode, queue
|
||||
notification.key_type,
|
||||
notification.notification_type,
|
||||
notification.id,
|
||||
research_mode,
|
||||
queue,
|
||||
)
|
||||
|
||||
|
||||
def simulated_recipient(to_address, notification_type):
|
||||
if notification_type == SMS_TYPE:
|
||||
formatted_simulated_numbers = [
|
||||
validate_and_format_phone_number(number) for number in current_app.config['SIMULATED_SMS_NUMBERS']
|
||||
validate_and_format_phone_number(number)
|
||||
for number in current_app.config["SIMULATED_SMS_NUMBERS"]
|
||||
]
|
||||
return to_address in formatted_simulated_numbers
|
||||
else:
|
||||
return to_address in current_app.config['SIMULATED_EMAIL_ADDRESSES']
|
||||
return to_address in current_app.config["SIMULATED_EMAIL_ADDRESSES"]
|
||||
|
||||
@@ -9,11 +9,13 @@ from app.errors import InvalidRequest, register_errors
|
||||
from app.models import INBOUND_SMS_TYPE, SMS_TYPE, InboundSms
|
||||
from app.notifications.sns_handlers import sns_notification_handler
|
||||
|
||||
receive_notifications_blueprint = Blueprint('receive_notifications', __name__)
|
||||
receive_notifications_blueprint = Blueprint("receive_notifications", __name__)
|
||||
register_errors(receive_notifications_blueprint)
|
||||
|
||||
|
||||
@receive_notifications_blueprint.route('/notifications/sms/receive/sns', methods=['POST'])
|
||||
@receive_notifications_blueprint.route(
|
||||
"/notifications/sms/receive/sns", methods=["POST"]
|
||||
)
|
||||
def receive_sns_sms():
|
||||
"""
|
||||
Expected value of the 'Message' key in the incoming payload from SNS
|
||||
@@ -28,10 +30,8 @@ def receive_sns_sms():
|
||||
"""
|
||||
|
||||
# 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
|
||||
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)
|
||||
@@ -42,9 +42,9 @@ 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')
|
||||
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
|
||||
@@ -52,42 +52,47 @@ def receive_sns_sms():
|
||||
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
|
||||
return jsonify(result="success", message="SMS-SNS callback succeeded"), 200
|
||||
|
||||
content = message.get("messageBody")
|
||||
from_number = message.get('originationNumber')
|
||||
provider_ref = message.get('inboundMessageId')
|
||||
date_received = post_data.get('Timestamp')
|
||||
from_number = message.get("originationNumber")
|
||||
provider_ref = message.get("inboundMessageId")
|
||||
date_received = post_data.get("Timestamp")
|
||||
provider_name = "sns"
|
||||
|
||||
inbound = create_inbound_sms_object(service,
|
||||
content=content,
|
||||
from_number=from_number,
|
||||
provider_ref=provider_ref,
|
||||
date_received=date_received,
|
||||
provider_name=provider_name)
|
||||
inbound = create_inbound_sms_object(
|
||||
service,
|
||||
content=content,
|
||||
from_number=from_number,
|
||||
provider_ref=provider_ref,
|
||||
date_received=date_received,
|
||||
provider_name=provider_name,
|
||||
)
|
||||
|
||||
tasks.send_inbound_sms_to_service.apply_async([str(inbound.id), str(service.id)], queue=QueueNames.NOTIFY)
|
||||
tasks.send_inbound_sms_to_service.apply_async(
|
||||
[str(inbound.id), str(service.id)], queue=QueueNames.NOTIFY
|
||||
)
|
||||
|
||||
current_app.logger.debug(
|
||||
'{} received inbound SMS with reference {} from SNS'.format(service.id, inbound.provider_reference))
|
||||
"{} received inbound SMS with reference {} from SNS".format(
|
||||
service.id, inbound.provider_reference
|
||||
)
|
||||
)
|
||||
|
||||
return jsonify(
|
||||
result="success", message="SMS-SNS callback succeeded"
|
||||
), 200
|
||||
return jsonify(result="success", message="SMS-SNS callback succeeded"), 200
|
||||
|
||||
|
||||
def unescape_string(string):
|
||||
return string.encode('raw_unicode_escape').decode('unicode_escape')
|
||||
return string.encode("raw_unicode_escape").decode("unicode_escape")
|
||||
|
||||
|
||||
def create_inbound_sms_object(service, content, from_number, provider_ref, date_received, provider_name):
|
||||
def create_inbound_sms_object(
|
||||
service, content, from_number, provider_ref, date_received, provider_name
|
||||
):
|
||||
user_number = try_validate_and_format_phone_number(
|
||||
from_number,
|
||||
international=True,
|
||||
log_msg=f'Invalid from_number received for service "{service.id}"'
|
||||
log_msg=f'Invalid from_number received for service "{service.id}"',
|
||||
)
|
||||
|
||||
provider_date = date_received
|
||||
@@ -98,7 +103,7 @@ def create_inbound_sms_object(service, content, from_number, provider_ref, date_
|
||||
provider_date=provider_date,
|
||||
provider_reference=provider_ref,
|
||||
content=content,
|
||||
provider=provider_name
|
||||
provider=provider_name,
|
||||
)
|
||||
dao_create_inbound_sms(inbound)
|
||||
return inbound
|
||||
@@ -108,14 +113,17 @@ def fetch_potential_service(inbound_number, provider_name):
|
||||
service = dao_fetch_service_by_inbound_number(inbound_number)
|
||||
|
||||
if not service:
|
||||
current_app.logger.warning('Inbound number "{}" from {} not associated with a service'.format(
|
||||
inbound_number, provider_name
|
||||
))
|
||||
current_app.logger.warning(
|
||||
'Inbound number "{}" from {} not associated with a service'.format(
|
||||
inbound_number, provider_name
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
if not has_inbound_sms_permissions(service.permissions):
|
||||
current_app.logger.error(
|
||||
'Service "{}" does not allow inbound SMS'.format(service.id))
|
||||
'Service "{}" does not allow inbound SMS'.format(service.id)
|
||||
)
|
||||
return False
|
||||
|
||||
return service
|
||||
|
||||
@@ -26,28 +26,36 @@ from app.schemas import (
|
||||
from app.service.utils import service_allowed_to_send_to
|
||||
from app.utils import get_public_notify_type_text, pagination_links
|
||||
|
||||
notifications = Blueprint('notifications', __name__)
|
||||
notifications = Blueprint("notifications", __name__)
|
||||
|
||||
register_errors(notifications)
|
||||
|
||||
|
||||
@notifications.route('/notifications/<uuid:notification_id>', methods=['GET'])
|
||||
@notifications.route("/notifications/<uuid:notification_id>", methods=["GET"])
|
||||
def get_notification_by_id(notification_id):
|
||||
notification = notifications_dao.get_notification_with_personalisation(
|
||||
str(authenticated_service.id),
|
||||
notification_id,
|
||||
key_type=None)
|
||||
return jsonify(data={"notification": notification_with_personalisation_schema.dump(notification)}), 200
|
||||
str(authenticated_service.id), notification_id, key_type=None
|
||||
)
|
||||
return (
|
||||
jsonify(
|
||||
data={
|
||||
"notification": notification_with_personalisation_schema.dump(
|
||||
notification
|
||||
)
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@notifications.route('/notifications', methods=['GET'])
|
||||
@notifications.route("/notifications", methods=["GET"])
|
||||
def get_all_notifications():
|
||||
data = notifications_filter_schema.load(request.args)
|
||||
|
||||
include_jobs = data.get('include_jobs', False)
|
||||
page = data.get('page', 1)
|
||||
page_size = data.get('page_size', current_app.config.get('API_PAGE_SIZE'))
|
||||
limit_days = data.get('limit_days')
|
||||
include_jobs = data.get("include_jobs", False)
|
||||
page = data.get("page", 1)
|
||||
page_size = data.get("page_size", current_app.config.get("API_PAGE_SIZE"))
|
||||
limit_days = data.get("limit_days")
|
||||
|
||||
pagination = notifications_dao.get_notifications_for_service(
|
||||
str(authenticated_service.id),
|
||||
@@ -57,120 +65,141 @@ def get_all_notifications():
|
||||
page_size=page_size,
|
||||
limit_days=limit_days,
|
||||
key_type=api_user.key_type,
|
||||
include_jobs=include_jobs)
|
||||
return jsonify(
|
||||
notifications=notification_with_personalisation_schema.dump(pagination.items, many=True),
|
||||
page_size=page_size,
|
||||
total=pagination.total,
|
||||
links=pagination_links(
|
||||
pagination,
|
||||
'.get_all_notifications',
|
||||
**request.args.to_dict()
|
||||
)
|
||||
), 200
|
||||
include_jobs=include_jobs,
|
||||
)
|
||||
return (
|
||||
jsonify(
|
||||
notifications=notification_with_personalisation_schema.dump(
|
||||
pagination.items, many=True
|
||||
),
|
||||
page_size=page_size,
|
||||
total=pagination.total,
|
||||
links=pagination_links(
|
||||
pagination, ".get_all_notifications", **request.args.to_dict()
|
||||
),
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@notifications.route('/notifications/<string:notification_type>', methods=['POST'])
|
||||
@notifications.route("/notifications/<string:notification_type>", methods=["POST"])
|
||||
def send_notification(notification_type):
|
||||
|
||||
if notification_type not in [SMS_TYPE, EMAIL_TYPE]:
|
||||
msg = "{} notification type is not supported".format(notification_type)
|
||||
raise InvalidRequest(msg, 400)
|
||||
|
||||
notification_form = (
|
||||
sms_template_notification_schema if notification_type == SMS_TYPE else email_notification_schema
|
||||
sms_template_notification_schema
|
||||
if notification_type == SMS_TYPE
|
||||
else email_notification_schema
|
||||
).load(request.get_json())
|
||||
|
||||
check_rate_limiting(authenticated_service, api_user)
|
||||
|
||||
template, template_with_content = validate_template(
|
||||
template_id=notification_form['template'],
|
||||
personalisation=notification_form.get('personalisation', {}),
|
||||
template_id=notification_form["template"],
|
||||
personalisation=notification_form.get("personalisation", {}),
|
||||
service=authenticated_service,
|
||||
notification_type=notification_type
|
||||
notification_type=notification_type,
|
||||
)
|
||||
|
||||
_service_allowed_to_send_to(notification_form, authenticated_service)
|
||||
if not service_has_permission(notification_type, authenticated_service.permissions):
|
||||
raise InvalidRequest(
|
||||
{'service': ["Cannot send {}".format(get_public_notify_type_text(notification_type, plural=True))]},
|
||||
status_code=400
|
||||
{
|
||||
"service": [
|
||||
"Cannot send {}".format(
|
||||
get_public_notify_type_text(notification_type, plural=True)
|
||||
)
|
||||
]
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if notification_type == SMS_TYPE:
|
||||
check_if_service_can_send_to_number(authenticated_service, notification_form['to'])
|
||||
check_if_service_can_send_to_number(
|
||||
authenticated_service, notification_form["to"]
|
||||
)
|
||||
|
||||
# Do not persist or send notification to the queue if it is a simulated recipient
|
||||
simulated = simulated_recipient(notification_form['to'], notification_type)
|
||||
notification_model = persist_notification(template_id=template.id,
|
||||
template_version=template.version,
|
||||
recipient=request.get_json()['to'],
|
||||
service=authenticated_service,
|
||||
personalisation=notification_form.get('personalisation', None),
|
||||
notification_type=notification_type,
|
||||
api_key_id=api_user.id,
|
||||
key_type=api_user.key_type,
|
||||
simulated=simulated,
|
||||
reply_to_text=template.reply_to_text,
|
||||
)
|
||||
simulated = simulated_recipient(notification_form["to"], notification_type)
|
||||
notification_model = persist_notification(
|
||||
template_id=template.id,
|
||||
template_version=template.version,
|
||||
recipient=request.get_json()["to"],
|
||||
service=authenticated_service,
|
||||
personalisation=notification_form.get("personalisation", None),
|
||||
notification_type=notification_type,
|
||||
api_key_id=api_user.id,
|
||||
key_type=api_user.key_type,
|
||||
simulated=simulated,
|
||||
reply_to_text=template.reply_to_text,
|
||||
)
|
||||
if not simulated:
|
||||
queue_name = QueueNames.PRIORITY if template.process_type == PRIORITY else None
|
||||
send_notification_to_queue(notification=notification_model,
|
||||
research_mode=authenticated_service.research_mode,
|
||||
queue=queue_name)
|
||||
send_notification_to_queue(
|
||||
notification=notification_model,
|
||||
research_mode=authenticated_service.research_mode,
|
||||
queue=queue_name,
|
||||
)
|
||||
else:
|
||||
current_app.logger.debug("POST simulated notification for id: {}".format(notification_model.id))
|
||||
current_app.logger.debug(
|
||||
"POST simulated notification for id: {}".format(notification_model.id)
|
||||
)
|
||||
notification_form.update({"template_version": template.version})
|
||||
|
||||
return jsonify(
|
||||
data=get_notification_return_data(
|
||||
notification_model.id,
|
||||
notification_form,
|
||||
template_with_content)
|
||||
), 201
|
||||
return (
|
||||
jsonify(
|
||||
data=get_notification_return_data(
|
||||
notification_model.id, notification_form, template_with_content
|
||||
)
|
||||
),
|
||||
201,
|
||||
)
|
||||
|
||||
|
||||
def get_notification_return_data(notification_id, notification, template):
|
||||
output = {
|
||||
'template_version': notification['template_version'],
|
||||
'notification': {'id': notification_id},
|
||||
'body': template.content_with_placeholders_filled_in,
|
||||
"template_version": notification["template_version"],
|
||||
"notification": {"id": notification_id},
|
||||
"body": template.content_with_placeholders_filled_in,
|
||||
}
|
||||
|
||||
if hasattr(template, 'subject'):
|
||||
output['subject'] = template.subject
|
||||
if hasattr(template, "subject"):
|
||||
output["subject"] = template.subject
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _service_allowed_to_send_to(notification, service):
|
||||
if not service_allowed_to_send_to(notification['to'], service, api_user.key_type):
|
||||
if not service_allowed_to_send_to(notification["to"], service, api_user.key_type):
|
||||
if api_user.key_type == KEY_TYPE_TEAM:
|
||||
message = 'Can’t send to this recipient using a team-only API key'
|
||||
message = "Can’t send to this recipient using a team-only API key"
|
||||
else:
|
||||
message = (
|
||||
'Can’t send to this recipient when service is in trial mode '
|
||||
'– see https://www.notifications.service.gov.uk/trial-mode'
|
||||
"Can’t send to this recipient when service is in trial mode "
|
||||
"– see https://www.notifications.service.gov.uk/trial-mode"
|
||||
)
|
||||
raise InvalidRequest(
|
||||
{'to': [message]},
|
||||
status_code=400
|
||||
)
|
||||
raise InvalidRequest({"to": [message]}, status_code=400)
|
||||
|
||||
|
||||
def create_template_object_for_notification(template, personalisation):
|
||||
template_object = template._as_utils_template_with_personalisation(personalisation)
|
||||
|
||||
if template_object.missing_data:
|
||||
message = 'Missing personalisation: {}'.format(", ".join(template_object.missing_data))
|
||||
errors = {'template': [message]}
|
||||
message = "Missing personalisation: {}".format(
|
||||
", ".join(template_object.missing_data)
|
||||
)
|
||||
errors = {"template": [message]}
|
||||
raise InvalidRequest(errors, status_code=400)
|
||||
|
||||
if (
|
||||
template_object.template_type == SMS_TYPE and
|
||||
template_object.is_message_too_long()
|
||||
template_object.template_type == SMS_TYPE
|
||||
and template_object.is_message_too_long()
|
||||
):
|
||||
message = 'Content has a character count greater than the limit of {}'.format(SMS_CHAR_COUNT_LIMIT)
|
||||
errors = {'content': [message]}
|
||||
message = "Content has a character count greater than the limit of {}".format(
|
||||
SMS_CHAR_COUNT_LIMIT
|
||||
)
|
||||
errors = {"content": [message]}
|
||||
raise InvalidRequest(errors, status_code=400)
|
||||
return template_object
|
||||
|
||||
@@ -16,7 +16,7 @@ VALID_SNS_TOPICS = Config.VALID_SNS_TOPICS
|
||||
|
||||
_signing_cert_cache = {}
|
||||
_cert_url_re = re.compile(
|
||||
r'sns\.([a-z]{1,3}(?:-gov)?-[a-z]+-[0-9]{1,2})\.amazonaws\.com',
|
||||
r"sns\.([a-z]{1,3}(?:-gov)?-[a-z]+-[0-9]{1,2})\.amazonaws\.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -38,28 +38,36 @@ def get_certificate(url):
|
||||
|
||||
def validate_arn(sns_payload):
|
||||
if VALIDATE_SNS_TOPICS:
|
||||
arn = sns_payload.get('TopicArn')
|
||||
arn = sns_payload.get("TopicArn")
|
||||
if arn not in VALID_SNS_TOPICS:
|
||||
raise ValidationError("Invalid Topic Name")
|
||||
|
||||
|
||||
def get_string_to_sign(sns_payload):
|
||||
payload_type = sns_payload.get('Type')
|
||||
if payload_type in ['SubscriptionConfirmation', 'UnsubscribeConfirmation']:
|
||||
fields = ['Message', 'MessageId', 'SubscribeURL', 'Timestamp', 'Token', 'TopicArn', 'Type']
|
||||
elif payload_type == 'Notification':
|
||||
fields = ['Message', 'MessageId', 'Subject', 'Timestamp', 'TopicArn', 'Type']
|
||||
payload_type = sns_payload.get("Type")
|
||||
if payload_type in ["SubscriptionConfirmation", "UnsubscribeConfirmation"]:
|
||||
fields = [
|
||||
"Message",
|
||||
"MessageId",
|
||||
"SubscribeURL",
|
||||
"Timestamp",
|
||||
"Token",
|
||||
"TopicArn",
|
||||
"Type",
|
||||
]
|
||||
elif payload_type == "Notification":
|
||||
fields = ["Message", "MessageId", "Subject", "Timestamp", "TopicArn", "Type"]
|
||||
else:
|
||||
raise ValidationError("Unexpected Message Type")
|
||||
|
||||
string_to_sign = ''
|
||||
string_to_sign = ""
|
||||
for field in fields:
|
||||
field_value = sns_payload.get(field)
|
||||
if not isinstance(field_value, str):
|
||||
if field == 'Subject' and field_value is 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'
|
||||
string_to_sign += field + "\n" + field_value + "\n"
|
||||
if isinstance(string_to_sign, six.text_type):
|
||||
string_to_sign = string_to_sign.encode()
|
||||
return string_to_sign
|
||||
@@ -72,10 +80,12 @@ def validate_sns_cert(sns_payload):
|
||||
Modified to swap m2crypto for oscrypto
|
||||
"""
|
||||
if not isinstance(sns_payload, dict):
|
||||
raise ValidationError("Unexpected message type {!r}".format(type(sns_payload).__name__))
|
||||
raise ValidationError(
|
||||
"Unexpected message type {!r}".format(type(sns_payload).__name__)
|
||||
)
|
||||
|
||||
# Amazon SNS currently supports signature version 1.
|
||||
if sns_payload.get('SignatureVersion') != '1':
|
||||
if sns_payload.get("SignatureVersion") != "1":
|
||||
raise ValidationError("Wrong Signature Version (expected 1)")
|
||||
|
||||
validate_arn(sns_payload)
|
||||
@@ -83,12 +93,15 @@ def validate_sns_cert(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):
|
||||
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)
|
||||
@@ -104,7 +117,7 @@ def validate_sns_cert(sns_payload):
|
||||
oscrypto.asymmetric.load_certificate(certificate),
|
||||
signature,
|
||||
string_to_sign,
|
||||
"sha1"
|
||||
"sha1",
|
||||
)
|
||||
return True
|
||||
except oscrypto.errors.SignatureError:
|
||||
|
||||
@@ -12,9 +12,9 @@ DEFAULT_MAX_AGE = timedelta(days=10000)
|
||||
|
||||
|
||||
class SNSMessageType(enum.Enum):
|
||||
SubscriptionConfirmation = 'SubscriptionConfirmation'
|
||||
Notification = 'Notification'
|
||||
UnsubscribeConfirmation = 'UnsubscribeConfirmation'
|
||||
SubscriptionConfirmation = "SubscriptionConfirmation"
|
||||
Notification = "Notification"
|
||||
UnsubscribeConfirmation = "UnsubscribeConfirmation"
|
||||
|
||||
|
||||
class InvalidMessageTypeException(Exception):
|
||||
@@ -29,17 +29,21 @@ def verify_message_type(message_type: str):
|
||||
|
||||
|
||||
def sns_notification_handler(data, headers):
|
||||
message_type = headers.get('x-amz-sns-message-type')
|
||||
message_type = headers.get("x-amz-sns-message-type")
|
||||
try:
|
||||
verify_message_type(message_type)
|
||||
except InvalidMessageTypeException:
|
||||
current_app.logger.exception(f"Response headers: {headers}\nResponse data: {data}")
|
||||
current_app.logger.exception(
|
||||
f"Response headers: {headers}\nResponse data: {data}"
|
||||
)
|
||||
raise InvalidRequest("SES-SNS callback failed: invalid message type", 400)
|
||||
|
||||
try:
|
||||
message = json.loads(data.decode('utf-8'))
|
||||
message = json.loads(data.decode("utf-8"))
|
||||
except decoder.JSONDecodeError:
|
||||
current_app.logger.exception(f"Response headers: {headers}\nResponse data: {data}")
|
||||
current_app.logger.exception(
|
||||
f"Response headers: {headers}\nResponse data: {data}"
|
||||
)
|
||||
raise InvalidRequest("SES-SNS callback failed: invalid JSON given", 400)
|
||||
|
||||
try:
|
||||
@@ -50,9 +54,13 @@ def sns_notification_handler(data, headers):
|
||||
)
|
||||
raise InvalidRequest("SES-SNS callback failed: validation failed", 400)
|
||||
|
||||
if message.get('Type') == 'SubscriptionConfirmation':
|
||||
if message.get("Type") == "SubscriptionConfirmation":
|
||||
# NOTE once a request is sent to SubscribeURL, AWS considers Notify a confirmed subscriber to this topic
|
||||
url = message.get('SubscribeUrl') if 'SubscribeUrl' in message else message.get('SubscribeURL')
|
||||
url = (
|
||||
message.get("SubscribeUrl")
|
||||
if "SubscribeUrl" in message
|
||||
else message.get("SubscribeURL")
|
||||
)
|
||||
response = requests.get(url)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
@@ -63,12 +71,15 @@ def sns_notification_handler(data, headers):
|
||||
)
|
||||
raise InvalidRequest(
|
||||
"SES-SNS callback failed: attempt to raise_for_status()SubscriptionConfirmation "
|
||||
"Type message failed", 400
|
||||
"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.")
|
||||
current_app.logger.info(
|
||||
f"SNS message: {message} is a valid message. Attempting to process it now."
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
@@ -3,7 +3,7 @@ from flask import current_app
|
||||
|
||||
|
||||
def confirm_subscription(confirmation_request):
|
||||
url = confirmation_request.get('SubscribeURL')
|
||||
url = confirmation_request.get("SubscribeURL")
|
||||
if not url:
|
||||
current_app.logger.warning("SubscribeURL does not exist or empty")
|
||||
return
|
||||
@@ -15,11 +15,13 @@ def confirm_subscription(confirmation_request):
|
||||
current_app.logger.warning("Response: {}".format(response.text))
|
||||
raise e
|
||||
|
||||
return confirmation_request['TopicArn']
|
||||
return confirmation_request["TopicArn"]
|
||||
|
||||
|
||||
def autoconfirm_subscription(req_json):
|
||||
if req_json.get('Type') == 'SubscriptionConfirmation':
|
||||
current_app.logger.debug("SNS subscription confirmation url: {}".format(req_json['SubscribeURL']))
|
||||
if req_json.get("Type") == "SubscriptionConfirmation":
|
||||
current_app.logger.debug(
|
||||
"SNS subscription confirmation url: {}".format(req_json["SubscribeURL"])
|
||||
)
|
||||
subscribed_topic = confirm_subscription(req_json)
|
||||
return subscribed_topic
|
||||
|
||||
@@ -32,21 +32,26 @@ from app.v2.errors import BadRequestError, RateLimitError, TotalRequestsError
|
||||
|
||||
|
||||
def check_service_over_api_rate_limit(service, api_key):
|
||||
if current_app.config['API_RATE_LIMIT_ENABLED'] and current_app.config['REDIS_ENABLED']:
|
||||
if (
|
||||
current_app.config["API_RATE_LIMIT_ENABLED"]
|
||||
and current_app.config["REDIS_ENABLED"]
|
||||
):
|
||||
cache_key = rate_limit_cache_key(service.id, api_key.key_type)
|
||||
rate_limit = service.rate_limit
|
||||
interval = 60
|
||||
if redis_store.exceeded_rate_limit(cache_key, rate_limit, interval):
|
||||
current_app.logger.info("service {} has been rate limited for throughput".format(service.id))
|
||||
current_app.logger.info(
|
||||
"service {} has been rate limited for throughput".format(service.id)
|
||||
)
|
||||
raise RateLimitError(rate_limit, interval, api_key.key_type)
|
||||
|
||||
|
||||
def check_application_over_retention_limit(key_type, service):
|
||||
if key_type == KEY_TYPE_TEST or not current_app.config['REDIS_ENABLED']:
|
||||
if key_type == KEY_TYPE_TEST or not current_app.config["REDIS_ENABLED"]:
|
||||
return 0
|
||||
|
||||
cache_key = daily_total_cache_key()
|
||||
daily_message_limit = current_app.config['DAILY_MESSAGE_LIMIT']
|
||||
daily_message_limit = current_app.config["DAILY_MESSAGE_LIMIT"]
|
||||
total_stats = redis_store.get(cache_key)
|
||||
if total_stats is None:
|
||||
# first message of the day, set the cache to 0 and the expiry to 24 hours
|
||||
@@ -56,7 +61,8 @@ def check_application_over_retention_limit(key_type, service):
|
||||
if int(total_stats) >= daily_message_limit:
|
||||
current_app.logger.info(
|
||||
"while sending for service {}, daily message limit of {} reached".format(
|
||||
service.id, daily_message_limit)
|
||||
service.id, daily_message_limit
|
||||
)
|
||||
)
|
||||
raise TotalRequestsError(daily_message_limit)
|
||||
return int(total_stats)
|
||||
@@ -69,25 +75,32 @@ def check_rate_limiting(service, api_key):
|
||||
|
||||
def check_template_is_for_notification_type(notification_type, template_type):
|
||||
if notification_type != template_type:
|
||||
message = "{0} template is not suitable for {1} notification".format(template_type,
|
||||
notification_type)
|
||||
raise BadRequestError(fields=[{'template': message}], message=message)
|
||||
message = "{0} template is not suitable for {1} notification".format(
|
||||
template_type, notification_type
|
||||
)
|
||||
raise BadRequestError(fields=[{"template": message}], message=message)
|
||||
|
||||
|
||||
def check_template_is_active(template):
|
||||
if template.archived:
|
||||
raise BadRequestError(fields=[{'template': 'Template has been deleted'}],
|
||||
message="Template has been deleted")
|
||||
raise BadRequestError(
|
||||
fields=[{"template": "Template has been deleted"}],
|
||||
message="Template has been deleted",
|
||||
)
|
||||
|
||||
|
||||
def service_can_send_to_recipient(send_to, key_type, service, allow_guest_list_recipients=True):
|
||||
if not service_allowed_to_send_to(send_to, service, key_type, allow_guest_list_recipients):
|
||||
def service_can_send_to_recipient(
|
||||
send_to, key_type, service, allow_guest_list_recipients=True
|
||||
):
|
||||
if not service_allowed_to_send_to(
|
||||
send_to, service, key_type, allow_guest_list_recipients
|
||||
):
|
||||
if key_type == KEY_TYPE_TEAM:
|
||||
message = 'Can’t send to this recipient using a team-only API key'
|
||||
message = "Can’t send to this recipient using a team-only API key"
|
||||
else:
|
||||
message = (
|
||||
'Can’t send to this recipient when service is in trial mode '
|
||||
'– see https://www.notifications.service.gov.uk/trial-mode'
|
||||
"Can’t send to this recipient when service is in trial mode "
|
||||
"– see https://www.notifications.service.gov.uk/trial-mode"
|
||||
)
|
||||
raise BadRequestError(message=message)
|
||||
|
||||
@@ -98,9 +111,11 @@ def service_has_permission(notify_type, permissions):
|
||||
|
||||
def check_service_has_permission(notify_type, permissions):
|
||||
if not service_has_permission(notify_type, permissions):
|
||||
raise BadRequestError(message="Service is not allowed to send {}".format(
|
||||
get_public_notify_type_text(notify_type, plural=True)
|
||||
))
|
||||
raise BadRequestError(
|
||||
message="Service is not allowed to send {}".format(
|
||||
get_public_notify_type_text(notify_type, plural=True)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_if_service_can_send_files_by_email(service_contact_link, service_id):
|
||||
@@ -111,18 +126,21 @@ def check_if_service_can_send_files_by_email(service_contact_link, service_id):
|
||||
)
|
||||
|
||||
|
||||
def validate_and_format_recipient(send_to, key_type, service, notification_type, allow_guest_list_recipients=True):
|
||||
def validate_and_format_recipient(
|
||||
send_to, key_type, service, notification_type, allow_guest_list_recipients=True
|
||||
):
|
||||
if send_to is None:
|
||||
raise BadRequestError(message="Recipient can't be empty")
|
||||
|
||||
service_can_send_to_recipient(send_to, key_type, service, allow_guest_list_recipients)
|
||||
service_can_send_to_recipient(
|
||||
send_to, key_type, service, allow_guest_list_recipients
|
||||
)
|
||||
|
||||
if notification_type == SMS_TYPE:
|
||||
international_phone_info = check_if_service_can_send_to_number(service, send_to)
|
||||
|
||||
return validate_and_format_phone_number(
|
||||
number=send_to,
|
||||
international=international_phone_info.international
|
||||
number=send_to, international=international_phone_info.international
|
||||
)
|
||||
elif notification_type == EMAIL_TYPE:
|
||||
return validate_and_format_email_address(email_address=send_to)
|
||||
@@ -136,7 +154,10 @@ def check_if_service_can_send_to_number(service, number):
|
||||
else:
|
||||
permissions = service.permissions
|
||||
|
||||
if international_phone_info.international and INTERNATIONAL_SMS_TYPE not in permissions:
|
||||
if (
|
||||
international_phone_info.international
|
||||
and INTERNATIONAL_SMS_TYPE not in permissions
|
||||
):
|
||||
raise BadRequestError(message="Cannot send to international mobile numbers")
|
||||
else:
|
||||
return international_phone_info
|
||||
@@ -160,17 +181,18 @@ def check_is_message_too_long(template_with_content):
|
||||
|
||||
def check_notification_content_is_not_empty(template_with_content):
|
||||
if template_with_content.is_message_empty():
|
||||
message = 'Your message is empty.'
|
||||
message = "Your message is empty."
|
||||
raise BadRequestError(message=message)
|
||||
|
||||
|
||||
def validate_template(template_id, personalisation, service, notification_type, check_char_count=True):
|
||||
def validate_template(
|
||||
template_id, personalisation, service, notification_type, check_char_count=True
|
||||
):
|
||||
try:
|
||||
template = SerialisedTemplate.from_id_and_service_id(template_id, service.id)
|
||||
except NoResultFound:
|
||||
message = 'Template not found'
|
||||
raise BadRequestError(message=message,
|
||||
fields=[{'template': message}])
|
||||
message = "Template not found"
|
||||
raise BadRequestError(message=message, fields=[{"template": message}])
|
||||
|
||||
check_template_is_for_notification_type(notification_type, template.template_type)
|
||||
check_template_is_active(template)
|
||||
@@ -200,16 +222,22 @@ def check_service_email_reply_to_id(service_id, reply_to_id, notification_type):
|
||||
try:
|
||||
return dao_get_reply_to_by_id(service_id, reply_to_id).email_address
|
||||
except NoResultFound:
|
||||
message = 'email_reply_to_id {} does not exist in database for service id {}' \
|
||||
.format(reply_to_id, service_id)
|
||||
message = "email_reply_to_id {} does not exist in database for service id {}".format(
|
||||
reply_to_id, service_id
|
||||
)
|
||||
raise BadRequestError(message=message)
|
||||
|
||||
|
||||
def check_service_sms_sender_id(service_id, sms_sender_id, notification_type):
|
||||
if sms_sender_id:
|
||||
try:
|
||||
return dao_get_service_sms_senders_by_id(service_id, sms_sender_id).sms_sender
|
||||
return dao_get_service_sms_senders_by_id(
|
||||
service_id, sms_sender_id
|
||||
).sms_sender
|
||||
except NoResultFound:
|
||||
message = 'sms_sender_id {} does not exist in database for service id {}' \
|
||||
.format(sms_sender_id, service_id)
|
||||
message = (
|
||||
"sms_sender_id {} does not exist in database for service id {}".format(
|
||||
sms_sender_id, service_id
|
||||
)
|
||||
)
|
||||
raise BadRequestError(message=message)
|
||||
|
||||
Reference in New Issue
Block a user