2014-05-11 22:56:44 -04:00
import datetime
import uuid
2015-08-20 11:12:25 -04:00
import json
2014-11-16 18:35:11 -05:00
import requests
2017-11-13 18:27:11 +00:00
import re
2014-05-11 22:56:44 -04:00
2018-04-18 11:24:31 -07:00
from boto3 import Session
2021-08-04 17:24:26 +01:00
from collections import OrderedDict
2020-08-01 07:23:36 -07:00
from moto . core import BaseBackend , BaseModel , CloudFormationModel
2019-10-31 08:44:26 -07:00
from moto . core . utils import (
iso_8601_datetime_with_milliseconds ,
camelcase_to_underscores ,
)
2014-11-16 18:42:53 -05:00
from moto . sqs import sqs_backends
2017-09-27 16:04:58 -07:00
2017-03-16 22:28:30 -04:00
from . exceptions import (
2019-10-31 08:44:26 -07:00
SNSNotFoundError ,
DuplicateSnsEndpointError ,
SnsEndpointDisabled ,
SNSInvalidParameter ,
InvalidParameterValue ,
InternalError ,
ResourceNotFoundError ,
TagLimitExceededError ,
2017-03-16 22:28:30 -04:00
)
2019-11-04 22:57:53 +01:00
from . utils import make_arn_for_topic , make_arn_for_subscription , is_e164
2014-05-11 22:56:44 -04:00
2019-12-16 21:05:29 -05:00
from moto . core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
2019-12-16 21:25:20 -05:00
2014-11-29 22:37:48 -05:00
DEFAULT_PAGE_SIZE = 100
2018-06-04 12:53:24 +00:00
MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB
2020-08-25 14:05:49 +02:00
MAXIMUM_SMS_MESSAGE_BYTES = 1600 # Amazon limit for a single publish SMS action
2014-05-11 22:56:44 -04:00
2020-08-01 07:23:36 -07:00
class Topic ( CloudFormationModel ) :
2014-11-16 18:35:11 -05:00
def __init__ ( self , name , sns_backend ) :
2014-05-11 22:56:44 -04:00
self . name = name
2014-11-16 18:35:11 -05:00
self . sns_backend = sns_backend
2014-05-11 22:56:44 -04:00
self . account_id = DEFAULT_ACCOUNT_ID
self . display_name = " "
self . delivery_policy = " "
2020-10-16 04:30:07 -07:00
self . kms_master_key_id = " "
2017-02-27 20:53:57 -05:00
self . effective_delivery_policy = json . dumps ( DEFAULT_EFFECTIVE_DELIVERY_POLICY )
2019-10-31 08:44:26 -07:00
self . arn = make_arn_for_topic ( self . account_id , name , sns_backend . region_name )
2014-05-11 22:56:44 -04:00
self . subscriptions_pending = 0
self . subscriptions_confimed = 0
self . subscriptions_deleted = 0
2019-10-31 08:44:26 -07:00
self . _policy_json = self . _create_default_topic_policy (
sns_backend . region_name , self . account_id , name
)
2019-10-11 17:58:48 +02:00
self . _tags = { }
2018-03-21 15:49:11 +00:00
def publish ( self , message , subject = None , message_attributes = None ) :
2021-07-26 07:40:39 +01:00
message_id = str ( uuid . uuid4 ( ) )
2014-11-29 22:37:48 -05:00
subscriptions , _ = self . sns_backend . list_subscriptions ( self . arn )
2014-05-11 22:56:44 -04:00
for subscription in subscriptions :
2019-10-31 08:44:26 -07:00
subscription . publish (
message ,
message_id ,
subject = subject ,
message_attributes = message_attributes ,
)
2014-05-11 22:56:44 -04:00
return message_id
2014-10-21 12:45:03 -04:00
def get_cfn_attribute ( self , attribute_name ) :
2014-10-21 14:51:26 -04:00
from moto . cloudformation . exceptions import UnformattedGetAttTemplateException
2019-10-31 08:44:26 -07:00
if attribute_name == " TopicName " :
2014-10-21 12:45:03 -04:00
return self . name
2014-10-21 14:51:26 -04:00
raise UnformattedGetAttTemplateException ( )
2014-10-21 12:45:03 -04:00
2015-01-17 19:48:08 -05:00
@property
def physical_resource_id ( self ) :
return self . arn
2019-10-25 17:57:50 +02:00
@property
def policy ( self ) :
return json . dumps ( self . _policy_json )
@policy.setter
def policy ( self , policy ) :
self . _policy_json = json . loads ( policy )
2020-08-01 07:23:36 -07:00
@staticmethod
def cloudformation_name_type ( ) :
return " TopicName "
@staticmethod
def cloudformation_type ( ) :
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html
return " AWS::SNS::Topic "
2015-01-17 19:48:08 -05:00
@classmethod
2019-10-31 08:44:26 -07:00
def create_from_cloudformation_json (
cls , resource_name , cloudformation_json , region_name
) :
2015-01-17 19:48:08 -05:00
sns_backend = sns_backends [ region_name ]
2019-10-31 08:44:26 -07:00
properties = cloudformation_json [ " Properties " ]
2015-01-17 19:48:08 -05:00
2020-08-27 05:11:47 -04:00
topic = sns_backend . create_topic ( resource_name )
2015-01-17 19:48:08 -05:00
for subscription in properties . get ( " Subscription " , [ ] ) :
2019-10-31 08:44:26 -07:00
sns_backend . subscribe (
topic . arn , subscription [ " Endpoint " ] , subscription [ " Protocol " ]
)
2015-01-17 19:48:08 -05:00
return topic
2021-10-11 21:56:39 +00:00
@classmethod
def update_from_cloudformation_json (
cls , original_resource , new_resource_name , cloudformation_json , region_name
) :
cls . delete_from_cloudformation_json (
original_resource . name , cloudformation_json , region_name
)
return cls . create_from_cloudformation_json (
new_resource_name , cloudformation_json , region_name
)
@classmethod
def delete_from_cloudformation_json (
cls , resource_name , cloudformation_json , region_name
) :
sns_backend = sns_backends [ region_name ]
properties = cloudformation_json [ " Properties " ]
topic_name = properties . get ( cls . cloudformation_name_type ( ) ) or resource_name
topic_arn = make_arn_for_topic (
DEFAULT_ACCOUNT_ID , topic_name , sns_backend . region_name
)
subscriptions , _ = sns_backend . list_subscriptions ( topic_arn )
for subscription in subscriptions :
sns_backend . unsubscribe ( subscription . arn )
sns_backend . delete_topic ( topic_arn )
2019-10-25 17:57:50 +02:00
def _create_default_topic_policy ( self , region_name , account_id , name ) :
return {
" Version " : " 2008-10-17 " ,
" Id " : " __default_policy_ID " ,
2019-10-31 08:44:26 -07:00
" Statement " : [
{
" Effect " : " Allow " ,
" Sid " : " __default_statement_ID " ,
" Principal " : { " AWS " : " * " } ,
" Action " : [
" SNS:GetTopicAttributes " ,
" SNS:SetTopicAttributes " ,
" SNS:AddPermission " ,
" SNS:RemovePermission " ,
" SNS:DeleteTopic " ,
" SNS:Subscribe " ,
" SNS:ListSubscriptionsByTopic " ,
" SNS:Publish " ,
" SNS:Receive " ,
] ,
" Resource " : make_arn_for_topic ( self . account_id , name , region_name ) ,
" Condition " : { " StringEquals " : { " AWS:SourceOwner " : str ( account_id ) } } ,
2019-10-25 17:57:50 +02:00
}
2019-10-31 08:44:26 -07:00
] ,
2019-10-25 17:57:50 +02:00
}
2014-05-11 22:56:44 -04:00
2017-03-11 23:41:12 -05:00
class Subscription ( BaseModel ) :
2014-05-11 22:56:44 -04:00
def __init__ ( self , topic , endpoint , protocol ) :
self . topic = topic
self . endpoint = endpoint
self . protocol = protocol
self . arn = make_arn_for_subscription ( self . topic . arn )
2017-09-08 03:19:34 +09:00
self . attributes = { }
2018-03-21 15:49:11 +00:00
self . _filter_policy = None # filter policy as a dict, not json.
2017-09-20 21:47:02 +01:00
self . confirmed = False
2014-05-11 22:56:44 -04:00
2019-10-31 08:44:26 -07:00
def publish ( self , message , message_id , subject = None , message_attributes = None ) :
2018-03-21 15:49:11 +00:00
if not self . _matches_filter_policy ( message_attributes ) :
return
2019-10-31 08:44:26 -07:00
if self . protocol == " sqs " :
2014-05-11 22:56:44 -04:00
queue_name = self . endpoint . split ( " : " ) [ - 1 ]
2014-11-16 18:42:53 -05:00
region = self . endpoint . split ( " : " ) [ 3 ]
2019-10-31 08:44:26 -07:00
if self . attributes . get ( " RawMessageDelivery " ) != " true " :
2020-03-21 19:25:25 +01:00
sqs_backends [ region ] . send_message (
queue_name ,
json . dumps (
self . get_post_data (
message ,
message_id ,
subject ,
message_attributes = message_attributes ,
) ,
sort_keys = True ,
indent = 2 ,
separators = ( " , " , " : " ) ,
2019-10-31 08:44:26 -07:00
) ,
)
2018-05-29 16:06:25 +02:00
else :
2020-03-21 19:25:25 +01:00
raw_message_attributes = { }
for key , value in message_attributes . items ( ) :
type = " string_value "
type_value = value [ " Value " ]
if value [ " Type " ] . startswith ( " Binary " ) :
type = " binary_value "
elif value [ " Type " ] . startswith ( " Number " ) :
type_value = " {0:g} " . format ( value [ " Value " ] )
raw_message_attributes [ key ] = {
" data_type " : value [ " Type " ] ,
type : type_value ,
}
sqs_backends [ region ] . send_message (
queue_name , message , message_attributes = raw_message_attributes
)
2019-10-31 08:44:26 -07:00
elif self . protocol in [ " http " , " https " ] :
2017-12-10 13:59:04 -08:00
post_data = self . get_post_data ( message , message_id , subject )
2019-10-31 08:44:26 -07:00
requests . post (
self . endpoint ,
json = post_data ,
headers = { " Content-Type " : " text/plain; charset=UTF-8 " } ,
)
elif self . protocol == " lambda " :
2017-09-27 16:04:58 -07:00
# TODO: support bad function name
2018-03-21 22:14:10 -07:00
# http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
arr = self . endpoint . split ( " : " )
region = arr [ 3 ]
qualifier = None
if len ( arr ) == 7 :
2019-10-31 08:44:26 -07:00
assert arr [ 5 ] == " function "
2018-03-21 22:14:10 -07:00
function_name = arr [ - 1 ]
elif len ( arr ) == 8 :
2019-10-31 08:44:26 -07:00
assert arr [ 5 ] == " function "
2018-03-21 22:14:10 -07:00
qualifier = arr [ - 1 ]
function_name = arr [ - 2 ]
else :
assert False
2021-07-29 06:38:16 +01:00
from moto . awslambda import lambda_backends
2019-10-31 08:44:26 -07:00
lambda_backends [ region ] . send_sns_message (
function_name , message , subject = subject , qualifier = qualifier
)
2014-05-11 22:56:44 -04:00
2018-03-21 15:49:11 +00:00
def _matches_filter_policy ( self , message_attributes ) :
# TODO: support Anything-but matching, prefix matching and
# numeric value matching.
if not self . _filter_policy :
return True
if message_attributes is None :
message_attributes = { }
def _field_match ( field , rules , message_attributes ) :
for rule in rules :
2019-08-25 16:48:14 +02:00
# TODO: boolean value matching is not supported, SNS behavior unknown
2021-07-26 07:40:39 +01:00
if isinstance ( rule , str ) :
2019-08-25 16:48:14 +02:00
if field not in message_attributes :
return False
2019-10-31 08:44:26 -07:00
if message_attributes [ field ] [ " Value " ] == rule :
2018-03-21 15:49:11 +00:00
return True
2019-08-25 16:48:14 +02:00
try :
2019-10-31 08:44:26 -07:00
json_data = json . loads ( message_attributes [ field ] [ " Value " ] )
2019-08-25 16:48:14 +02:00
if rule in json_data :
return True
except ( ValueError , TypeError ) :
pass
2021-07-26 07:40:39 +01:00
if isinstance ( rule , ( int , float ) ) :
2019-08-25 16:48:14 +02:00
if field not in message_attributes :
return False
2019-10-31 08:44:26 -07:00
if message_attributes [ field ] [ " Type " ] == " Number " :
attribute_values = [ message_attributes [ field ] [ " Value " ] ]
elif message_attributes [ field ] [ " Type " ] == " String.Array " :
2019-08-25 16:48:14 +02:00
try :
2019-10-31 08:44:26 -07:00
attribute_values = json . loads (
message_attributes [ field ] [ " Value " ]
)
2019-08-25 16:48:14 +02:00
if not isinstance ( attribute_values , list ) :
attribute_values = [ attribute_values ]
except ( ValueError , TypeError ) :
return False
else :
return False
for attribute_values in attribute_values :
2019-11-16 12:31:45 -08:00
# Even the official documentation states a 5 digits of accuracy after the decimal point for numerics, in reality it is 6
2019-08-25 16:48:14 +02:00
# https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html#subscription-filter-policy-constraints
if int ( attribute_values * 1000000 ) == int ( rule * 1000000 ) :
return True
if isinstance ( rule , dict ) :
keyword = list ( rule . keys ( ) ) [ 0 ]
attributes = list ( rule . values ( ) ) [ 0 ]
2019-10-31 08:44:26 -07:00
if keyword == " exists " :
2019-08-25 16:48:14 +02:00
if attributes and field in message_attributes :
return True
elif not attributes and field not in message_attributes :
return True
2018-03-21 15:49:11 +00:00
return False
2019-10-31 08:44:26 -07:00
return all (
_field_match ( field , rules , message_attributes )
2021-07-26 07:40:39 +01:00
for field , rules in self . _filter_policy . items ( )
2019-10-31 08:44:26 -07:00
)
2018-03-21 15:49:11 +00:00
2019-10-31 08:44:26 -07:00
def get_post_data ( self , message , message_id , subject , message_attributes = None ) :
2018-04-17 16:27:48 +00:00
post_data = {
2014-05-11 22:56:44 -04:00
" Type " : " Notification " ,
" MessageId " : message_id ,
" TopicArn " : self . topic . arn ,
2017-12-10 13:59:04 -08:00
" Subject " : subject or " my subject " ,
2014-05-11 22:56:44 -04:00
" Message " : message ,
2019-10-31 08:44:26 -07:00
" Timestamp " : iso_8601_datetime_with_milliseconds (
datetime . datetime . utcnow ( )
) ,
2014-05-11 22:56:44 -04:00
" SignatureVersion " : " 1 " ,
" Signature " : " EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn/49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7NFhUzLiieYr4BKHpdTmdD6c0esKEYBpabxDSc= " ,
" SigningCertURL " : " https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem " ,
2019-12-16 21:25:20 -05:00
" UnsubscribeURL " : " https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1: {} :some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55 " . format (
DEFAULT_ACCOUNT_ID
) ,
2014-05-11 22:56:44 -04:00
}
2018-04-17 16:27:48 +00:00
if message_attributes :
post_data [ " MessageAttributes " ] = message_attributes
return post_data
2014-05-11 22:56:44 -04:00
2017-03-11 23:41:12 -05:00
class PlatformApplication ( BaseModel ) :
2015-03-14 09:06:31 -04:00
def __init__ ( self , region , name , platform , attributes ) :
self . region = region
self . name = name
self . platform = platform
self . attributes = attributes
@property
def arn ( self ) :
2019-12-15 19:22:26 -05:00
return " arn:aws:sns: {region} : {AccountId} :app/ {platform} / {name} " . format (
2019-12-16 21:25:20 -05:00
region = self . region ,
platform = self . platform ,
name = self . name ,
AccountId = DEFAULT_ACCOUNT_ID ,
2015-03-14 09:06:31 -04:00
)
2017-03-11 23:41:12 -05:00
class PlatformEndpoint ( BaseModel ) :
2015-03-14 09:06:31 -04:00
def __init__ ( self , region , application , custom_user_data , token , attributes ) :
self . region = region
self . application = application
self . custom_user_data = custom_user_data
self . token = token
self . attributes = attributes
self . id = uuid . uuid4 ( )
2016-06-02 11:02:43 +02:00
self . messages = OrderedDict ( )
2016-11-11 17:01:47 -05:00
self . __fixup_attributes ( )
def __fixup_attributes ( self ) :
# When AWS returns the attributes dict, it always contains these two elements, so we need to
# automatically ensure they exist as well.
2019-10-31 08:44:26 -07:00
if " Token " not in self . attributes :
self . attributes [ " Token " ] = self . token
2020-12-14 17:08:33 +02:00
if " Enabled " in self . attributes :
enabled = self . attributes [ " Enabled " ]
self . attributes [ " Enabled " ] = enabled . lower ( )
else :
self . attributes [ " Enabled " ] = " true "
2015-03-14 09:06:31 -04:00
2017-03-16 22:28:30 -04:00
@property
def enabled ( self ) :
2019-10-31 08:44:26 -07:00
return json . loads ( self . attributes . get ( " Enabled " , " true " ) . lower ( ) )
2017-03-16 22:28:30 -04:00
2015-03-14 09:06:31 -04:00
@property
def arn ( self ) :
2019-12-15 19:22:26 -05:00
return " arn:aws:sns: {region} : {AccountId} :endpoint/ {platform} / {name} / {id} " . format (
2015-03-14 09:06:31 -04:00
region = self . region ,
2019-12-16 21:05:29 -05:00
AccountId = DEFAULT_ACCOUNT_ID ,
2015-03-14 09:06:31 -04:00
platform = self . application . platform ,
name = self . application . name ,
id = self . id ,
)
def publish ( self , message ) :
2017-03-16 22:28:30 -04:00
if not self . enabled :
raise SnsEndpointDisabled ( " Endpoint %s disabled " % self . id )
2015-03-14 09:06:31 -04:00
# This is where we would actually send a message
2021-07-26 07:40:39 +01:00
message_id = str ( uuid . uuid4 ( ) )
2016-06-02 11:02:43 +02:00
self . messages [ message_id ] = message
2015-03-14 09:06:31 -04:00
return message_id
2014-05-11 22:56:44 -04:00
class SNSBackend ( BaseBackend ) :
2015-07-31 16:01:07 -04:00
def __init__ ( self , region_name ) :
super ( SNSBackend , self ) . __init__ ( )
2014-11-29 22:37:48 -05:00
self . topics = OrderedDict ( )
2021-08-27 00:23:17 +09:00
self . subscriptions : OrderedDict [ str , Subscription ] = OrderedDict ( )
2015-03-14 09:06:31 -04:00
self . applications = { }
self . platform_endpoints = { }
2015-07-31 16:01:07 -04:00
self . region_name = region_name
2017-09-19 22:48:46 +01:00
self . sms_attributes = { }
2020-08-25 14:05:49 +02:00
self . sms_messages = OrderedDict ( )
2019-10-31 08:44:26 -07:00
self . opt_out_numbers = [
" +447420500600 " ,
" +447420505401 " ,
" +447632960543 " ,
" +447632960028 " ,
" +447700900149 " ,
" +447700900550 " ,
" +447700900545 " ,
" +447700900907 " ,
]
2015-07-31 16:01:07 -04:00
def reset ( self ) :
region_name = self . region_name
self . __dict__ = { }
self . __init__ ( region_name )
2014-05-11 22:56:44 -04:00
2021-09-24 12:01:09 -04:00
@staticmethod
def default_vpc_endpoint_service ( service_region , zones ) :
""" List of dicts representing default VPC endpoints for this service. """
return BaseBackend . default_vpc_endpoint_service_factory (
service_region , zones , " sns "
)
2017-09-19 22:48:46 +01:00
def update_sms_attributes ( self , attrs ) :
self . sms_attributes . update ( attrs )
2019-10-11 17:58:48 +02:00
def create_topic ( self , name , attributes = None , tags = None ) :
2021-01-26 20:34:52 +05:30
if attributes is None :
attributes = { }
if (
attributes . get ( " FifoTopic " )
and attributes . get ( " FifoTopic " ) . lower ( ) == " true "
) :
fails_constraints = not re . match ( r " ^[a-zA-Z0-9_-] { 1,256} \ .fifo$ " , name )
msg = " Fifo Topic names must end with .fifo and must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long. "
else :
fails_constraints = not re . match ( r " ^[a-zA-Z0-9_-] { 1,256}$ " , name )
msg = " Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long. "
2017-11-13 18:27:11 +00:00
if fails_constraints :
2021-01-26 20:34:52 +05:30
raise InvalidParameterValue ( msg )
2017-11-06 19:06:55 +00:00
candidate_topic = Topic ( name , self )
2019-02-21 22:08:46 +01:00
if attributes :
for attribute in attributes :
2019-10-31 08:44:26 -07:00
setattr (
candidate_topic ,
camelcase_to_underscores ( attribute ) ,
attributes [ attribute ] ,
)
2019-10-11 17:58:48 +02:00
if tags :
candidate_topic . _tags = tags
2017-11-06 19:06:55 +00:00
if candidate_topic . arn in self . topics :
return self . topics [ candidate_topic . arn ]
else :
self . topics [ candidate_topic . arn ] = candidate_topic
return candidate_topic
2014-05-11 22:56:44 -04:00
2014-11-29 22:37:48 -05:00
def _get_values_nexttoken ( self , values_map , next_token = None ) :
2019-05-25 04:24:46 -05:00
if next_token is None or not next_token :
2014-11-29 22:37:48 -05:00
next_token = 0
next_token = int ( next_token )
2019-10-31 08:44:26 -07:00
values = list ( values_map . values ( ) ) [ next_token : next_token + DEFAULT_PAGE_SIZE ]
2014-11-29 22:37:48 -05:00
if len ( values ) == DEFAULT_PAGE_SIZE :
next_token = next_token + DEFAULT_PAGE_SIZE
else :
next_token = None
return values , next_token
2017-07-31 13:37:29 +02:00
def _get_topic_subscriptions ( self , topic ) :
return [ sub for sub in self . subscriptions . values ( ) if sub . topic == topic ]
2014-11-29 22:37:48 -05:00
def list_topics ( self , next_token = None ) :
return self . _get_values_nexttoken ( self . topics , next_token )
2014-05-11 22:56:44 -04:00
2020-10-29 14:22:02 +05:30
def delete_topic_subscriptions ( self , topic ) :
2021-07-26 16:21:17 +02:00
for key , value in dict ( self . subscriptions ) . items ( ) :
2020-10-29 14:22:02 +05:30
if value . topic == topic :
self . subscriptions . pop ( key )
2014-05-11 22:56:44 -04:00
def delete_topic ( self , arn ) :
2020-09-10 00:32:41 -07:00
try :
2020-10-29 14:22:02 +05:30
topic = self . get_topic ( arn )
self . delete_topic_subscriptions ( topic )
2020-09-10 00:32:41 -07:00
self . topics . pop ( arn )
except KeyError :
raise SNSNotFoundError ( " Topic with arn {0} not found " . format ( arn ) )
2014-05-11 22:56:44 -04:00
def get_topic ( self , arn ) :
2015-03-14 09:06:31 -04:00
try :
return self . topics [ arn ]
except KeyError :
2015-03-14 09:19:36 -04:00
raise SNSNotFoundError ( " Topic with arn {0} not found " . format ( arn ) )
2014-05-11 22:56:44 -04:00
def set_topic_attribute ( self , topic_arn , attribute_name , attribute_value ) :
topic = self . get_topic ( topic_arn )
setattr ( topic , attribute_name , attribute_value )
def subscribe ( self , topic_arn , endpoint , protocol ) :
2019-11-04 22:57:53 +01:00
if protocol == " sms " :
if re . search ( r " [./-] { 2,} " , endpoint ) or re . search (
r " (^[./-]|[./-]$) " , endpoint
) :
raise SNSInvalidParameter ( " Invalid SMS endpoint: {} " . format ( endpoint ) )
reduced_endpoint = re . sub ( r " [./-] " , " " , endpoint )
if not is_e164 ( reduced_endpoint ) :
raise SNSInvalidParameter ( " Invalid SMS endpoint: {} " . format ( endpoint ) )
2018-01-02 11:30:39 +11:00
# AWS doesn't create duplicates
old_subscription = self . _find_subscription ( topic_arn , endpoint , protocol )
if old_subscription :
return old_subscription
2014-05-11 22:56:44 -04:00
topic = self . get_topic ( topic_arn )
subscription = Subscription ( topic , endpoint , protocol )
2019-10-02 18:06:34 +02:00
attributes = {
2019-10-31 08:44:26 -07:00
" PendingConfirmation " : " false " ,
2019-11-30 15:51:43 +01:00
" ConfirmationWasAuthenticated " : " true " ,
2019-10-31 08:44:26 -07:00
" Endpoint " : endpoint ,
" TopicArn " : topic_arn ,
" Protocol " : protocol ,
" SubscriptionArn " : subscription . arn ,
2019-11-30 15:51:43 +01:00
" Owner " : DEFAULT_ACCOUNT_ID ,
" RawMessageDelivery " : " false " ,
2019-10-02 18:06:34 +02:00
}
2019-11-30 15:51:43 +01:00
if protocol in [ " http " , " https " ] :
attributes [ " EffectiveDeliveryPolicy " ] = topic . effective_delivery_policy
2019-10-02 18:06:34 +02:00
subscription . attributes = attributes
2014-05-11 22:56:44 -04:00
self . subscriptions [ subscription . arn ] = subscription
return subscription
2018-01-02 11:30:39 +11:00
def _find_subscription ( self , topic_arn , endpoint , protocol ) :
for subscription in self . subscriptions . values ( ) :
2019-10-31 08:44:26 -07:00
if (
subscription . topic . arn == topic_arn
and subscription . endpoint == endpoint
and subscription . protocol == protocol
) :
2018-01-02 11:30:39 +11:00
return subscription
return None
2014-05-11 22:56:44 -04:00
def unsubscribe ( self , subscription_arn ) :
2019-12-27 16:04:12 +01:00
self . subscriptions . pop ( subscription_arn , None )
2014-05-11 22:56:44 -04:00
2014-11-29 22:37:48 -05:00
def list_subscriptions ( self , topic_arn = None , next_token = None ) :
2014-05-11 22:56:44 -04:00
if topic_arn :
topic = self . get_topic ( topic_arn )
2017-02-23 21:37:43 -05:00
filtered = OrderedDict (
2019-10-31 08:44:26 -07:00
[ ( sub . arn , sub ) for sub in self . _get_topic_subscriptions ( topic ) ]
)
2014-11-29 22:37:48 -05:00
return self . _get_values_nexttoken ( filtered , next_token )
2014-05-11 22:56:44 -04:00
else :
2014-11-29 22:37:48 -05:00
return self . _get_values_nexttoken ( self . subscriptions , next_token )
2014-05-11 22:56:44 -04:00
2020-08-25 14:05:49 +02:00
def publish (
self ,
message ,
arn = None ,
phone_number = None ,
subject = None ,
message_attributes = None ,
) :
2018-04-13 15:17:38 -04:00
if subject is not None and len ( subject ) > 100 :
# Note that the AWS docs around length are wrong: https://github.com/spulec/moto/issues/1503
2019-10-31 08:44:26 -07:00
raise ValueError ( " Subject must be less than 100 characters " )
2017-10-20 13:19:55 +01:00
2020-08-25 14:05:49 +02:00
if phone_number :
# This is only an approximation. In fact, we should try to use GSM-7 or UCS-2 encoding to count used bytes
if len ( message ) > MAXIMUM_SMS_MESSAGE_BYTES :
raise ValueError ( " SMS message must be less than 1600 bytes " )
2021-07-26 07:40:39 +01:00
message_id = str ( uuid . uuid4 ( ) )
2020-08-25 14:05:49 +02:00
self . sms_messages [ message_id ] = ( phone_number , message )
return message_id
2018-06-04 12:53:24 +00:00
if len ( message ) > MAXIMUM_MESSAGE_LENGTH :
2019-10-31 08:44:26 -07:00
raise InvalidParameterValue (
" An error occurred (InvalidParameter) when calling the Publish operation: Invalid parameter: Message too long "
)
2018-06-04 12:53:24 +00:00
2015-03-14 09:06:31 -04:00
try :
topic = self . get_topic ( arn )
2019-10-31 08:44:26 -07:00
message_id = topic . publish (
message , subject = subject , message_attributes = message_attributes
)
2015-03-14 09:06:31 -04:00
except SNSNotFoundError :
endpoint = self . get_endpoint ( arn )
message_id = endpoint . publish ( message )
2014-05-11 22:56:44 -04:00
return message_id
2015-03-14 09:06:31 -04:00
def create_platform_application ( self , region , name , platform , attributes ) :
application = PlatformApplication ( region , name , platform , attributes )
self . applications [ application . arn ] = application
return application
def get_application ( self , arn ) :
try :
return self . applications [ arn ]
except KeyError :
2019-10-31 08:44:26 -07:00
raise SNSNotFoundError ( " Application with arn {0} not found " . format ( arn ) )
2015-03-14 09:06:31 -04:00
def set_application_attributes ( self , arn , attributes ) :
application = self . get_application ( arn )
application . attributes . update ( attributes )
return application
def list_platform_applications ( self ) :
return self . applications . values ( )
def delete_platform_application ( self , platform_arn ) :
self . applications . pop ( platform_arn )
2019-10-31 08:44:26 -07:00
def create_platform_endpoint (
self , region , application , custom_user_data , token , attributes
) :
2021-07-04 15:43:22 +09:00
for endpoint in self . platform_endpoints . values ( ) :
if token == endpoint . token :
2021-07-26 16:21:17 +02:00
if (
attributes . get ( " Enabled " , " " ) . lower ( )
== endpoint . attributes [ " Enabled " ]
) :
2021-07-04 15:43:22 +09:00
return endpoint
raise DuplicateSnsEndpointError (
" Duplicate endpoint token with different attributes: %s " % token
)
2017-02-23 21:37:43 -05:00
platform_endpoint = PlatformEndpoint (
2019-10-31 08:44:26 -07:00
region , application , custom_user_data , token , attributes
)
2015-03-14 09:06:31 -04:00
self . platform_endpoints [ platform_endpoint . arn ] = platform_endpoint
return platform_endpoint
def list_endpoints_by_platform_application ( self , application_arn ) :
return [
2019-10-31 08:44:26 -07:00
endpoint
for endpoint in self . platform_endpoints . values ( )
2015-03-14 09:06:31 -04:00
if endpoint . application . arn == application_arn
]
def get_endpoint ( self , arn ) :
try :
return self . platform_endpoints [ arn ]
except KeyError :
2021-01-31 14:29:10 +02:00
raise SNSNotFoundError ( " Endpoint does not exist " )
2015-03-14 09:06:31 -04:00
def set_endpoint_attributes ( self , arn , attributes ) :
endpoint = self . get_endpoint ( arn )
2020-12-14 17:08:33 +02:00
if " Enabled " in attributes :
attributes [ " Enabled " ] = attributes [ " Enabled " ] . lower ( )
2015-03-14 09:06:31 -04:00
endpoint . attributes . update ( attributes )
return endpoint
2016-05-02 14:34:51 +02:00
def delete_endpoint ( self , arn ) :
try :
del self . platform_endpoints [ arn ]
except KeyError :
2019-10-31 08:44:26 -07:00
raise SNSNotFoundError ( " Endpoint with arn {0} not found " . format ( arn ) )
2016-05-02 14:34:51 +02:00
2017-09-08 03:19:34 +09:00
def get_subscription_attributes ( self , arn ) :
2021-08-27 00:23:17 +09:00
subscription = self . subscriptions . get ( arn )
if not subscription :
raise SNSNotFoundError (
" Subscription does not exist " , template = " wrapped_single_error "
)
2017-09-08 03:19:34 +09:00
return subscription . attributes
def set_subscription_attributes ( self , arn , name , value ) :
2020-07-27 23:23:15 +05:30
if name not in [
" RawMessageDelivery " ,
" DeliveryPolicy " ,
" FilterPolicy " ,
" RedrivePolicy " ,
] :
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter ( " AttributeName " )
2017-09-08 03:19:34 +09:00
# TODO: should do validation
_subscription = [ _ for _ in self . subscriptions . values ( ) if _ . arn == arn ]
if not _subscription :
raise SNSNotFoundError ( " Subscription with arn {0} not found " . format ( arn ) )
subscription = _subscription [ 0 ]
subscription . attributes [ name ] = value
2019-10-31 08:44:26 -07:00
if name == " FilterPolicy " :
2019-08-25 16:48:14 +02:00
filter_policy = json . loads ( value )
self . _validate_filter_policy ( filter_policy )
subscription . _filter_policy = filter_policy
def _validate_filter_policy ( self , value ) :
# TODO: extend validation checks
combinations = 1
2021-07-26 07:40:39 +01:00
for rules in value . values ( ) :
2019-08-25 16:48:14 +02:00
combinations * = len ( rules )
2019-11-16 12:31:45 -08:00
# Even the official documentation states the total combination of values must not exceed 100, in reality it is 150
2019-08-25 16:48:14 +02:00
# https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html#subscription-filter-policy-constraints
if combinations > 150 :
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter (
" Invalid parameter: FilterPolicy: Filter policy is too complex "
)
2019-08-25 16:48:14 +02:00
2021-07-26 07:40:39 +01:00
for field , rules in value . items ( ) :
2019-08-25 16:48:14 +02:00
for rule in rules :
if rule is None :
continue
2021-07-26 07:40:39 +01:00
if isinstance ( rule , str ) :
2019-08-25 16:48:14 +02:00
continue
if isinstance ( rule , bool ) :
continue
2021-07-26 07:40:39 +01:00
if isinstance ( rule , ( int , float ) ) :
2019-08-25 16:48:14 +02:00
if rule < = - 1000000000 or rule > = 1000000000 :
raise InternalError ( " Unknown " )
continue
if isinstance ( rule , dict ) :
keyword = list ( rule . keys ( ) ) [ 0 ]
attributes = list ( rule . values ( ) ) [ 0 ]
2019-10-31 08:44:26 -07:00
if keyword == " anything-but " :
2019-08-25 16:48:14 +02:00
continue
2019-10-31 08:44:26 -07:00
elif keyword == " exists " :
2019-08-25 16:48:14 +02:00
if not isinstance ( attributes , bool ) :
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter (
" Invalid parameter: FilterPolicy: exists match pattern must be either true or false. "
)
2019-08-25 16:48:14 +02:00
continue
2019-10-31 08:44:26 -07:00
elif keyword == " numeric " :
2019-08-25 16:48:14 +02:00
continue
2019-10-31 08:44:26 -07:00
elif keyword == " prefix " :
2019-08-25 16:48:14 +02:00
continue
else :
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter (
" Invalid parameter: FilterPolicy: Unrecognized match type {type} " . format (
type = keyword
)
)
2019-08-25 16:48:14 +02:00
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter (
" Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null "
)
2018-03-21 15:49:11 +00:00
2019-10-25 17:57:50 +02:00
def add_permission ( self , topic_arn , label , aws_account_ids , action_names ) :
if topic_arn not in self . topics :
2019-10-31 08:44:26 -07:00
raise SNSNotFoundError ( " Topic does not exist " )
2019-10-25 17:57:50 +02:00
policy = self . topics [ topic_arn ] . _policy_json
2019-10-31 08:44:26 -07:00
statement = next (
(
statement
for statement in policy [ " Statement " ]
if statement [ " Sid " ] == label
) ,
None ,
)
2019-10-25 17:57:50 +02:00
if statement :
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter ( " Statement already exists " )
2019-10-25 17:57:50 +02:00
if any ( action_name not in VALID_POLICY_ACTIONS for action_name in action_names ) :
2019-10-31 08:44:26 -07:00
raise SNSInvalidParameter ( " Policy statement action out of service scope! " )
2019-10-25 17:57:50 +02:00
2019-10-31 08:44:26 -07:00
principals = [
" arn:aws:iam:: {} :root " . format ( account_id ) for account_id in aws_account_ids
]
actions = [ " SNS: {} " . format ( action_name ) for action_name in action_names ]
2019-10-25 17:57:50 +02:00
statement = {
2019-10-31 08:44:26 -07:00
" Sid " : label ,
" Effect " : " Allow " ,
" Principal " : { " AWS " : principals [ 0 ] if len ( principals ) == 1 else principals } ,
" Action " : actions [ 0 ] if len ( actions ) == 1 else actions ,
" Resource " : topic_arn ,
2019-10-25 17:57:50 +02:00
}
2019-10-31 08:44:26 -07:00
self . topics [ topic_arn ] . _policy_json [ " Statement " ] . append ( statement )
2019-10-25 17:57:50 +02:00
def remove_permission ( self , topic_arn , label ) :
if topic_arn not in self . topics :
2019-10-31 08:44:26 -07:00
raise SNSNotFoundError ( " Topic does not exist " )
2019-10-25 17:57:50 +02:00
2019-10-31 08:44:26 -07:00
statements = self . topics [ topic_arn ] . _policy_json [ " Statement " ]
statements = [
statement for statement in statements if statement [ " Sid " ] != label
]
2019-10-25 17:57:50 +02:00
2019-10-31 08:44:26 -07:00
self . topics [ topic_arn ] . _policy_json [ " Statement " ] = statements
2019-10-25 17:57:50 +02:00
2019-10-11 17:58:48 +02:00
def list_tags_for_resource ( self , resource_arn ) :
2019-10-12 20:36:15 +02:00
if resource_arn not in self . topics :
raise ResourceNotFoundError
2019-10-11 17:58:48 +02:00
return self . topics [ resource_arn ] . _tags
2019-10-12 20:36:15 +02:00
def tag_resource ( self , resource_arn , tags ) :
if resource_arn not in self . topics :
raise ResourceNotFoundError
updated_tags = self . topics [ resource_arn ] . _tags . copy ( )
updated_tags . update ( tags )
if len ( updated_tags ) > 50 :
raise TagLimitExceededError
self . topics [ resource_arn ] . _tags = updated_tags
2019-10-12 21:10:51 +02:00
def untag_resource ( self , resource_arn , tag_keys ) :
if resource_arn not in self . topics :
raise ResourceNotFoundError
for key in tag_keys :
self . topics [ resource_arn ] . _tags . pop ( key , None )
2015-03-14 09:06:31 -04:00
2014-11-16 18:35:11 -05:00
sns_backends = { }
2019-10-31 08:44:26 -07:00
for region in Session ( ) . get_available_regions ( " sns " ) :
2018-04-18 11:24:31 -07:00
sns_backends [ region ] = SNSBackend ( region )
2019-12-26 17:12:22 +01:00
for region in Session ( ) . get_available_regions ( " sns " , partition_name = " aws-us-gov " ) :
sns_backends [ region ] = SNSBackend ( region )
for region in Session ( ) . get_available_regions ( " sns " , partition_name = " aws-cn " ) :
sns_backends [ region ] = SNSBackend ( region )
2014-05-11 22:56:44 -04:00
2017-02-27 20:53:57 -05:00
DEFAULT_EFFECTIVE_DELIVERY_POLICY = {
2019-11-30 15:51:43 +01:00
" defaultHealthyRetryPolicy " : {
" numNoDelayRetries " : 0 ,
" numMinDelayRetries " : 0 ,
" minDelayTarget " : 20 ,
" maxDelayTarget " : 20 ,
" numMaxDelayRetries " : 0 ,
" numRetries " : 3 ,
" backoffFunction " : " linear " ,
} ,
" sicklyRetryPolicy " : None ,
" throttlePolicy " : None ,
" guaranteed " : False ,
2017-02-27 20:53:57 -05:00
}
2019-10-25 17:57:50 +02:00
VALID_POLICY_ACTIONS = [
2019-10-31 08:44:26 -07:00
" GetTopicAttributes " ,
" SetTopicAttributes " ,
" AddPermission " ,
" RemovePermission " ,
" DeleteTopic " ,
" Subscribe " ,
" ListSubscriptionsByTopic " ,
" Publish " ,
" Receive " ,
2019-10-25 17:57:50 +02:00
]