notify-api-412 use black to enforce python style standards

This commit is contained in:
Kenneth Kehl
2023-08-23 10:35:43 -07:00
parent a7898118d7
commit 026dc14021
586 changed files with 33990 additions and 23461 deletions

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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 = 'Cant send to this recipient using a team-only API key'
message = "Cant send to this recipient using a team-only API key"
else:
message = (
'Cant send to this recipient when service is in trial mode '
' see https://www.notifications.service.gov.uk/trial-mode'
"Cant 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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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 = 'Cant send to this recipient using a team-only API key'
message = "Cant send to this recipient using a team-only API key"
else:
message = (
'Cant send to this recipient when service is in trial mode '
' see https://www.notifications.service.gov.uk/trial-mode'
"Cant 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)