2014-08-27 11:17:06 -04:00
from __future__ import unicode_literals
2014-11-16 18:35:11 -05:00
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
2014-08-26 13:25:50 -04:00
import six
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
2014-11-29 22:43:30 -05:00
from moto . compat import OrderedDict
2017-03-11 23:41:12 -05:00
from moto . core import BaseBackend , BaseModel
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
from moto . awslambda import lambda_backends
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
DEFAULT_ACCOUNT_ID = 123456789012
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
2014-05-11 22:56:44 -04:00
2017-03-11 23:41:12 -05:00
class Topic ( BaseModel ) :
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 = " "
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 ) :
2014-08-26 13:25:50 -04:00
message_id = six . text_type ( 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 )
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
2019-10-31 08:44:26 -07:00
topic = sns_backend . create_topic ( properties . get ( " TopicName " ) )
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
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 " :
enveloped_message = json . dumps (
self . get_post_data (
message ,
message_id ,
subject ,
message_attributes = message_attributes ,
) ,
sort_keys = True ,
indent = 2 ,
separators = ( " , " , " : " ) ,
)
2018-05-29 16:06:25 +02:00
else :
enveloped_message = message
2017-08-22 04:29:34 +09:00
sqs_backends [ region ] . send_message ( queue_name , enveloped_message )
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
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
2018-03-21 15:49:11 +00:00
if isinstance ( rule , six . string_types ) :
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
if isinstance ( rule , ( six . integer_types , float ) ) :
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 :
# Even the offical documentation states a 5 digits of accuracy after the decimal point for numerics, in reality it is 6
# 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 )
for field , rules in six . iteritems ( self . _filter_policy )
)
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-10-31 08:44:26 -07:00
" UnsubscribeURL " : " https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55 " ,
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 ) :
return " arn:aws:sns: {region} :123456789012:app/ {platform} / {name} " . format (
2019-10-31 08:44:26 -07:00
region = self . region , platform = self . platform , name = self . name
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
if " Enabled " not in self . attributes :
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 ) :
return " arn:aws:sns: {region} :123456789012:endpoint/ {platform} / {name} / {id} " . format (
region = self . region ,
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
2016-06-02 11:02:43 +02:00
message_id = six . text_type ( uuid . uuid4 ( ) )
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 ( )
self . subscriptions = 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 = { }
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
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 ) :
2019-10-31 08:44:26 -07:00
fails_constraints = not re . match ( r " ^[a-zA-Z0-9_-] { 1,256}$ " , name )
2017-11-13 18:27:11 +00:00
if fails_constraints :
2019-10-31 08:44:26 -07:00
raise InvalidParameterValue (
" 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-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
def delete_topic ( self , arn ) :
2017-07-31 13:37:29 +02:00
topic = self . get_topic ( arn )
subscriptions = self . _get_topic_subscriptions ( topic )
for sub in subscriptions :
self . unsubscribe ( sub . arn )
2014-05-11 22:56:44 -04:00
self . topics . pop ( arn )
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
2017-09-26 00:21:07 +01:00
def get_topic_from_phone_number ( self , number ) :
for subscription in self . subscriptions . values ( ) :
2019-10-31 08:44:26 -07:00
if subscription . protocol == " sms " and subscription . endpoint == number :
2017-09-26 00:21:07 +01:00
return subscription . topic . arn
2019-10-31 08:44:26 -07:00
raise SNSNotFoundError ( " Could not find valid subscription " )
2017-09-26 00:21:07 +01:00
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 " ,
" Endpoint " : endpoint ,
" TopicArn " : topic_arn ,
" Protocol " : protocol ,
" SubscriptionArn " : subscription . arn ,
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 ) :
self . subscriptions . pop ( subscription_arn )
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
2018-03-21 15:49:11 +00:00
def publish ( self , arn , message , 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
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
) :
if any (
token == endpoint . token for endpoint in self . platform_endpoints . values ( )
) :
2017-03-16 22:28:30 -04:00
raise DuplicateSnsEndpointError ( " Duplicate endpoint token: %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 :
2019-10-31 08:44:26 -07:00
raise SNSNotFoundError ( " Endpoint with arn {0} not found " . format ( arn ) )
2015-03-14 09:06:31 -04:00
def set_endpoint_attributes ( self , arn , attributes ) :
endpoint = self . get_endpoint ( arn )
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 ) :
_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 ]
return subscription . attributes
def set_subscription_attributes ( self , arn , name , value ) :
2019-10-31 08:44:26 -07:00
if name not in [ " RawMessageDelivery " , " DeliveryPolicy " , " FilterPolicy " ] :
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
for rules in six . itervalues ( value ) :
combinations * = len ( rules )
# Even the offical documentation states the total combination of values must not exceed 100, in reality it is 150
# 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
for field , rules in six . iteritems ( value ) :
for rule in rules :
if rule is None :
continue
if isinstance ( rule , six . string_types ) :
continue
if isinstance ( rule , bool ) :
continue
if isinstance ( rule , ( six . integer_types , float ) ) :
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 )
2014-05-11 22:56:44 -04:00
2017-02-27 20:53:57 -05:00
DEFAULT_EFFECTIVE_DELIVERY_POLICY = {
2019-10-31 08:44:26 -07:00
" http " : {
" disableSubscriptionOverrides " : False ,
" defaultHealthyRetryPolicy " : {
" numNoDelayRetries " : 0 ,
" numMinDelayRetries " : 0 ,
" minDelayTarget " : 20 ,
" maxDelayTarget " : 20 ,
" numMaxDelayRetries " : 0 ,
" numRetries " : 3 ,
" backoffFunction " : " linear " ,
} ,
2014-05-11 22:56:44 -04:00
}
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
]