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 boto . sns
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
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
2014-11-29 23:34:40 -05:00
from moto . core . utils import iso_8601_datetime_with_milliseconds
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 (
2017-11-13 18:27:11 +00:00
SNSNotFoundError , DuplicateSnsEndpointError , SnsEndpointDisabled , SNSInvalidParameter ,
InvalidParameterValue
2017-03-16 22:28:30 -04:00
)
2014-05-11 22:56:44 -04:00
from . utils import make_arn_for_topic , make_arn_for_subscription
DEFAULT_ACCOUNT_ID = 123456789012
2014-11-29 22:37:48 -05:00
DEFAULT_PAGE_SIZE = 100
2014-05-11 22:56:44 -04:00
2017-03-11 23:41:12 -05:00
class Topic ( BaseModel ) :
2017-02-23 21:37:43 -05:00
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 = " "
2017-02-27 10:20:53 -05:00
self . policy = json . dumps ( DEFAULT_TOPIC_POLICY )
2014-05-11 22:56:44 -04:00
self . delivery_policy = " "
2017-02-27 20:53:57 -05:00
self . effective_delivery_policy = json . dumps ( DEFAULT_EFFECTIVE_DELIVERY_POLICY )
2017-02-23 21:37:43 -05: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
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 :
2018-03-21 15:49:11 +00: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
2014-10-21 12:45:03 -04:00
if attribute_name == ' TopicName ' :
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
@classmethod
def create_from_cloudformation_json ( cls , resource_name , cloudformation_json , region_name ) :
sns_backend = sns_backends [ region_name ]
properties = cloudformation_json [ ' Properties ' ]
topic = sns_backend . create_topic (
properties . get ( " TopicName " )
)
for subscription in properties . get ( " Subscription " , [ ] ) :
2017-02-23 21:37:43 -05:00
sns_backend . subscribe ( topic . arn , subscription [
' Endpoint ' ] , subscription [ ' Protocol ' ] )
2015-01-17 19:48:08 -05:00
return topic
2014-05-11 22:56:44 -04:00
2017-03-11 23:41:12 -05:00
class Subscription ( BaseModel ) :
2017-02-23 21:37:43 -05:00
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
2018-03-21 15:49:11 +00:00
def publish ( self , message , message_id , subject = None ,
message_attributes = None ) :
if not self . _matches_filter_policy ( message_attributes ) :
return
2014-05-11 22:56:44 -04:00
if self . protocol == ' sqs ' :
queue_name = self . endpoint . split ( " : " ) [ - 1 ]
2014-11-16 18:42:53 -05:00
region = self . endpoint . split ( " : " ) [ 3 ]
2017-12-10 13:59:04 -08:00
enveloped_message = json . dumps ( self . get_post_data ( message , message_id , subject ) , sort_keys = True , indent = 2 , separators = ( ' , ' , ' : ' ) )
2017-08-22 04:29:34 +09:00
sqs_backends [ region ] . send_message ( queue_name , enveloped_message )
2014-05-11 22:56:44 -04: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 )
2017-07-30 20:44:06 +08:00
requests . post ( self . endpoint , json = post_data )
2017-09-27 16:04:58 -07:00
elif self . protocol == ' lambda ' :
# 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 :
assert arr [ 5 ] == ' function '
function_name = arr [ - 1 ]
elif len ( arr ) == 8 :
assert arr [ 5 ] == ' function '
qualifier = arr [ - 1 ]
function_name = arr [ - 2 ]
else :
assert False
lambda_backends [ region ] . send_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 ) :
if field not in message_attributes :
return False
for rule in rules :
if isinstance ( rule , six . string_types ) :
# only string value matching is supported
if message_attributes [ field ] == rule :
return True
return False
return all ( _field_match ( field , rules , message_attributes )
for field , rules in six . iteritems ( self . _filter_policy ) )
2017-12-10 13:59:04 -08:00
def get_post_data ( self , message , message_id , subject ) :
2014-05-11 22:56:44 -04:00
return {
" 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 ,
2016-09-07 14:40:52 -04: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 " ,
" 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 "
}
2017-03-11 23:41:12 -05:00
class PlatformApplication ( BaseModel ) :
2017-02-23 21:37:43 -05:00
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 (
region = self . region ,
platform = self . platform ,
name = self . name ,
)
2017-03-11 23:41:12 -05:00
class PlatformEndpoint ( BaseModel ) :
2017-02-23 21:37:43 -05:00
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.
2017-02-23 21:37:43 -05:00
if ' Token ' not in self . attributes :
2016-11-11 17:01:47 -05:00
self . attributes [ ' Token ' ] = self . token
2017-02-23 21:37:43 -05:00
if ' Enabled ' not in self . attributes :
2017-10-17 18:42:29 +01:00
self . attributes [ ' Enabled ' ] = ' True '
2015-03-14 09:06:31 -04:00
2017-03-16 22:28:30 -04:00
@property
def enabled ( self ) :
return json . loads ( self . attributes . get ( ' Enabled ' , ' true ' ) . lower ( ) )
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 ) :
2017-02-23 21:37:43 -05:00
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 = { }
2017-09-20 20:56:37 +01:00
self . opt_out_numbers = [ ' +447420500600 ' , ' +447420505401 ' , ' +447632960543 ' , ' +447632960028 ' , ' +447700900149 ' , ' +447700900550 ' , ' +447700900545 ' , ' +447700900907 ' ]
2017-09-20 21:13:26 +01:00
self . permissions = { }
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 )
2014-05-11 22:56:44 -04:00
def create_topic ( self , name ) :
2017-11-13 18:27:11 +00:00
fails_constraints = not re . match ( r ' ^[a-zA-Z0-9](?:[A-Za-z0-9_-] { 0,253}[a-zA-Z0-9])?$ ' , name )
if fails_constraints :
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 )
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 ) :
if next_token is None :
next_token = 0
next_token = int ( next_token )
2017-02-23 21:37:43 -05: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 ( ) :
if subscription . protocol == ' sms ' and subscription . endpoint == number :
return subscription . topic . arn
raise SNSNotFoundError ( ' Could not find valid subscription ' )
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 ) :
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 )
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 ( ) :
if subscription . topic . arn == topic_arn and subscription . endpoint == endpoint and subscription . protocol == protocol :
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 (
2017-07-31 13:37:29 +02: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
2017-10-20 13:19:55 +01:00
raise ValueError ( ' Subject must be less than 100 characters ' )
2015-03-14 09:06:31 -04:00
try :
topic = self . get_topic ( arn )
2018-03-21 15:49:11 +00: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 :
2017-02-23 21:37:43 -05: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 )
def create_platform_endpoint ( self , region , application , custom_user_data , token , attributes ) :
2017-03-16 22:28:30 -04:00
if any ( token == endpoint . token for endpoint in self . platform_endpoints . values ( ) ) :
raise DuplicateSnsEndpointError ( " Duplicate endpoint token: %s " % token )
2017-02-23 21:37:43 -05:00
platform_endpoint = PlatformEndpoint (
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 [
endpoint for endpoint
in self . platform_endpoints . values ( )
if endpoint . application . arn == application_arn
]
def get_endpoint ( self , arn ) :
try :
return self . platform_endpoints [ arn ]
except KeyError :
2017-02-23 21:37:43 -05: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 :
2017-02-23 21:37:43 -05: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 ) :
2018-03-21 15:49:11 +00:00
if name not in [ ' RawMessageDelivery ' , ' DeliveryPolicy ' , ' FilterPolicy ' ] :
2017-09-08 03:19:34 +09:00
raise SNSInvalidParameter ( ' AttributeName ' )
# 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
2018-03-21 15:49:11 +00:00
if name == ' FilterPolicy ' :
subscription . _filter_policy = json . loads ( value )
2015-03-14 09:06:31 -04:00
2014-11-16 18:35:11 -05:00
sns_backends = { }
for region in boto . sns . regions ( ) :
2015-07-31 16:01:07 -04:00
sns_backends [ region . name ] = SNSBackend ( region . name )
2014-05-11 22:56:44 -04:00
2017-02-27 10:20:53 -05:00
DEFAULT_TOPIC_POLICY = {
2014-05-11 22:56:44 -04:00
" Version " : " 2008-10-17 " ,
" Id " : " us-east-1/698519295917/test__default_policy_ID " ,
" Statement " : [ {
" Effect " : " Allow " ,
" Sid " : " us-east-1/698519295917/test__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 " : " arn:aws:sns:us-east-1:698519295917:test " ,
" Condition " : {
" StringLike " : {
" AWS:SourceArn " : " arn:aws:*:*:698519295917:* "
}
}
} ]
2017-02-27 10:20:53 -05:00
}
2014-05-11 22:56:44 -04:00
2017-02-27 20:53:57 -05:00
DEFAULT_EFFECTIVE_DELIVERY_POLICY = {
2014-05-11 22:56:44 -04:00
' http ' : {
' disableSubscriptionOverrides ' : False ,
' defaultHealthyRetryPolicy ' : {
' numNoDelayRetries ' : 0 ,
' numMinDelayRetries ' : 0 ,
' minDelayTarget ' : 20 ,
' maxDelayTarget ' : 20 ,
' numMaxDelayRetries ' : 0 ,
' numRetries ' : 3 ,
' backoffFunction ' : ' linear '
}
}
2017-02-27 20:53:57 -05:00
}