localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev42__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (208) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +560 -559
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1041 -969
  5. localstack/aws/api/cloudwatch/__init__.py +408 -368
  6. localstack/aws/api/config/__init__.py +788 -786
  7. localstack/aws/api/core.py +4 -0
  8. localstack/aws/api/dynamodb/__init__.py +753 -759
  9. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  10. localstack/aws/api/ec2/__init__.py +9713 -8573
  11. localstack/aws/api/es/__init__.py +453 -453
  12. localstack/aws/api/events/__init__.py +552 -552
  13. localstack/aws/api/firehose/__init__.py +541 -543
  14. localstack/aws/api/iam/__init__.py +646 -572
  15. localstack/aws/api/kinesis/__init__.py +251 -144
  16. localstack/aws/api/kms/__init__.py +343 -333
  17. localstack/aws/api/lambda_/__init__.py +585 -571
  18. localstack/aws/api/logs/__init__.py +682 -666
  19. localstack/aws/api/opensearch/__init__.py +814 -785
  20. localstack/aws/api/pipes/__init__.py +336 -336
  21. localstack/aws/api/redshift/__init__.py +1192 -1164
  22. localstack/aws/api/resource_groups/__init__.py +175 -175
  23. localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
  24. localstack/aws/api/route53/__init__.py +256 -254
  25. localstack/aws/api/route53resolver/__init__.py +396 -396
  26. localstack/aws/api/s3/__init__.py +1358 -1345
  27. localstack/aws/api/s3control/__init__.py +616 -584
  28. localstack/aws/api/scheduler/__init__.py +118 -118
  29. localstack/aws/api/secretsmanager/__init__.py +193 -193
  30. localstack/aws/api/ses/__init__.py +227 -227
  31. localstack/aws/api/sns/__init__.py +115 -115
  32. localstack/aws/api/sqs/__init__.py +100 -100
  33. localstack/aws/api/ssm/__init__.py +1978 -1970
  34. localstack/aws/api/stepfunctions/__init__.py +323 -323
  35. localstack/aws/api/sts/__init__.py +90 -66
  36. localstack/aws/api/support/__init__.py +112 -112
  37. localstack/aws/api/swf/__init__.py +378 -386
  38. localstack/aws/api/transcribe/__init__.py +425 -425
  39. localstack/aws/client.py +7 -2
  40. localstack/aws/forwarder.py +52 -5
  41. localstack/aws/handlers/analytics.py +1 -1
  42. localstack/aws/handlers/logging.py +12 -2
  43. localstack/aws/handlers/metric_handler.py +41 -1
  44. localstack/aws/handlers/service.py +43 -10
  45. localstack/aws/protocol/parser.py +440 -21
  46. localstack/aws/protocol/serializer.py +684 -64
  47. localstack/aws/protocol/service_router.py +120 -20
  48. localstack/aws/scaffold.py +15 -17
  49. localstack/aws/skeleton.py +4 -2
  50. localstack/aws/spec-patches.json +58 -0
  51. localstack/aws/spec.py +33 -13
  52. localstack/cli/exceptions.py +1 -1
  53. localstack/cli/localstack.py +10 -5
  54. localstack/cli/lpm.py +3 -4
  55. localstack/cli/profiles.py +1 -2
  56. localstack/config.py +18 -12
  57. localstack/constants.py +4 -29
  58. localstack/dev/kubernetes/__main__.py +39 -4
  59. localstack/dev/run/paths.py +1 -1
  60. localstack/dns/plugins.py +5 -1
  61. localstack/dns/server.py +12 -3
  62. localstack/packages/api.py +9 -8
  63. localstack/packages/core.py +2 -2
  64. localstack/packages/plugins.py +0 -8
  65. localstack/runtime/init.py +1 -1
  66. localstack/services/apigateway/helpers.py +5 -9
  67. localstack/services/apigateway/legacy/provider.py +85 -12
  68. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  69. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  70. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  71. localstack/services/apigateway/next_gen/provider.py +5 -0
  72. localstack/services/apigateway/patches.py +0 -9
  73. localstack/services/cloudformation/engine/entities.py +12 -1
  74. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  75. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  76. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  77. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  78. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  79. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  80. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  81. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  82. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  83. localstack/services/cloudformation/provider.py +2 -2
  84. localstack/services/cloudformation/resource_provider.py +5 -1
  85. localstack/services/cloudformation/resources.py +24149 -0
  86. localstack/services/cloudformation/v2/entities.py +6 -3
  87. localstack/services/cloudformation/v2/provider.py +178 -33
  88. localstack/services/cloudformation/v2/types.py +8 -4
  89. localstack/services/cloudwatch/provider_v2.py +25 -28
  90. localstack/services/dynamodb/packages.py +2 -1
  91. localstack/services/dynamodb/provider.py +42 -0
  92. localstack/services/dynamodb/v2/provider.py +42 -0
  93. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  94. localstack/services/es/provider.py +2 -2
  95. localstack/services/events/event_rule_engine.py +31 -13
  96. localstack/services/events/models.py +4 -5
  97. localstack/services/events/target.py +17 -9
  98. localstack/services/iam/provider.py +11 -116
  99. localstack/services/iam/resources/policy_simulator.py +133 -0
  100. localstack/services/kinesis/models.py +15 -2
  101. localstack/services/kinesis/packages.py +1 -1
  102. localstack/services/kinesis/provider.py +77 -0
  103. localstack/services/kms/models.py +34 -4
  104. localstack/services/kms/provider.py +107 -21
  105. localstack/services/lambda_/api_utils.py +3 -1
  106. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  107. localstack/services/lambda_/packages.py +1 -1
  108. localstack/services/lambda_/provider.py +1 -1
  109. localstack/services/lambda_/runtimes.py +8 -3
  110. localstack/services/logs/provider.py +36 -19
  111. localstack/services/moto.py +2 -1
  112. localstack/services/opensearch/cluster.py +15 -7
  113. localstack/services/opensearch/packages.py +26 -7
  114. localstack/services/opensearch/provider.py +6 -1
  115. localstack/services/opensearch/versions.py +56 -7
  116. localstack/services/s3/constants.py +5 -2
  117. localstack/services/s3/cors.py +4 -4
  118. localstack/services/s3/notifications.py +1 -1
  119. localstack/services/s3/presigned_url.py +27 -43
  120. localstack/services/s3/provider.py +68 -12
  121. localstack/services/s3/utils.py +42 -11
  122. localstack/services/ses/provider.py +16 -7
  123. localstack/services/sns/constants.py +7 -1
  124. localstack/services/sns/v2/models.py +190 -0
  125. localstack/services/sns/v2/provider.py +992 -2
  126. localstack/services/sns/v2/utils.py +138 -0
  127. localstack/services/sqs/developer_api.py +205 -0
  128. localstack/services/sqs/models.py +79 -13
  129. localstack/services/sqs/provider.py +8 -309
  130. localstack/services/sqs/query_api.py +1 -1
  131. localstack/services/sqs/utils.py +121 -2
  132. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  133. localstack/testing/aws/cloudformation_utils.py +1 -1
  134. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  135. localstack/testing/pytest/container.py +4 -5
  136. localstack/testing/pytest/fixtures.py +20 -19
  137. localstack/testing/pytest/in_memory_localstack.py +0 -4
  138. localstack/testing/pytest/marking.py +13 -4
  139. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  140. localstack/testing/pytest/util.py +1 -1
  141. localstack/testing/pytest/validation_tracking.py +1 -2
  142. localstack/testing/snapshots/transformer_utility.py +7 -0
  143. localstack/testing/testselection/matching.py +0 -1
  144. localstack/utils/analytics/events.py +2 -2
  145. localstack/utils/analytics/metadata.py +1 -2
  146. localstack/utils/analytics/metrics/counter.py +6 -8
  147. localstack/utils/analytics/publisher.py +1 -2
  148. localstack/utils/analytics/service_request_aggregator.py +2 -2
  149. localstack/utils/archives.py +11 -11
  150. localstack/utils/aws/arns.py +17 -9
  151. localstack/utils/aws/aws_responses.py +7 -7
  152. localstack/utils/aws/aws_stack.py +2 -3
  153. localstack/utils/aws/client_types.py +0 -8
  154. localstack/utils/aws/message_forwarding.py +1 -2
  155. localstack/utils/aws/request_context.py +4 -5
  156. localstack/utils/batch_policy.py +3 -3
  157. localstack/utils/bootstrap.py +7 -7
  158. localstack/utils/catalog/catalog.py +139 -0
  159. localstack/utils/catalog/catalog_loader.py +119 -0
  160. localstack/utils/catalog/common.py +58 -0
  161. localstack/utils/catalog/plugins.py +28 -0
  162. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  163. localstack/utils/collections.py +7 -8
  164. localstack/utils/config_listener.py +1 -1
  165. localstack/utils/container_networking.py +2 -3
  166. localstack/utils/container_utils/container_client.py +115 -131
  167. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  168. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  169. localstack/utils/crypto.py +109 -0
  170. localstack/utils/diagnose.py +2 -3
  171. localstack/utils/docker_utils.py +3 -4
  172. localstack/utils/files.py +31 -7
  173. localstack/utils/functions.py +3 -2
  174. localstack/utils/http.py +4 -5
  175. localstack/utils/json.py +19 -5
  176. localstack/utils/kinesis/kinesis_connector.py +2 -1
  177. localstack/utils/net.py +6 -6
  178. localstack/utils/no_exit_argument_parser.py +2 -2
  179. localstack/utils/numbers.py +9 -2
  180. localstack/utils/objects.py +6 -5
  181. localstack/utils/patch.py +2 -1
  182. localstack/utils/run.py +10 -9
  183. localstack/utils/scheduler.py +11 -11
  184. localstack/utils/server/tcp_proxy.py +2 -2
  185. localstack/utils/serving.py +2 -3
  186. localstack/utils/strings.py +10 -11
  187. localstack/utils/sync.py +126 -1
  188. localstack/utils/tagging.py +1 -4
  189. localstack/utils/testutil.py +5 -4
  190. localstack/utils/threads.py +2 -2
  191. localstack/utils/time.py +11 -3
  192. localstack/utils/urls.py +1 -3
  193. localstack/version.py +2 -2
  194. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
  195. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
  196. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
  197. localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
  198. localstack/packages/terraform.py +0 -46
  199. localstack/services/cloudformation/deploy.html +0 -144
  200. localstack/services/cloudformation/deploy_ui.py +0 -47
  201. localstack/services/cloudformation/plugins.py +0 -12
  202. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  203. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
  204. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
  205. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
  206. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
  207. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
  208. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,999 @@
1
+ import contextlib
2
+ import copy
3
+ import json
1
4
  import logging
5
+ import re
2
6
 
3
- from localstack.aws.api.sns import SnsApi
7
+ from botocore.utils import InvalidArnException
8
+
9
+ from localstack.aws.api import CommonServiceException, RequestContext
10
+ from localstack.aws.api.sns import (
11
+ AmazonResourceName,
12
+ ConfirmSubscriptionResponse,
13
+ CreateEndpointResponse,
14
+ CreatePlatformApplicationResponse,
15
+ CreateTopicResponse,
16
+ Endpoint,
17
+ GetEndpointAttributesResponse,
18
+ GetPlatformApplicationAttributesResponse,
19
+ GetSMSAttributesResponse,
20
+ GetSubscriptionAttributesResponse,
21
+ GetTopicAttributesResponse,
22
+ InvalidParameterException,
23
+ ListEndpointsByPlatformApplicationResponse,
24
+ ListPlatformApplicationsResponse,
25
+ ListString,
26
+ ListSubscriptionsByTopicResponse,
27
+ ListSubscriptionsResponse,
28
+ ListTagsForResourceResponse,
29
+ ListTopicsResponse,
30
+ MapStringToString,
31
+ NotFoundException,
32
+ PlatformApplication,
33
+ SetSMSAttributesResponse,
34
+ SnsApi,
35
+ String,
36
+ SubscribeResponse,
37
+ Subscription,
38
+ SubscriptionAttributesMap,
39
+ TagKeyList,
40
+ TagList,
41
+ TagResourceResponse,
42
+ TopicAttributesMap,
43
+ UntagResourceResponse,
44
+ attributeName,
45
+ attributeValue,
46
+ authenticateOnUnsubscribe,
47
+ endpoint,
48
+ nextToken,
49
+ protocol,
50
+ subscriptionARN,
51
+ topicARN,
52
+ topicName,
53
+ )
54
+ from localstack.services.sns import constants as sns_constants
55
+ from localstack.services.sns.certificate import SNS_SERVER_CERT
56
+ from localstack.services.sns.constants import (
57
+ DUMMY_SUBSCRIPTION_PRINCIPAL,
58
+ VALID_APPLICATION_PLATFORMS,
59
+ )
60
+ from localstack.services.sns.filter import FilterPolicyValidator
61
+ from localstack.services.sns.publisher import PublishDispatcher, SnsPublishContext
62
+ from localstack.services.sns.v2.models import (
63
+ SMS_ATTRIBUTE_NAMES,
64
+ SMS_DEFAULT_SENDER_REGEX,
65
+ SMS_TYPES,
66
+ EndpointAttributeNames,
67
+ PlatformApplicationDetails,
68
+ PlatformEndpoint,
69
+ SnsMessage,
70
+ SnsMessageType,
71
+ SnsStore,
72
+ SnsSubscription,
73
+ Topic,
74
+ sns_stores,
75
+ )
76
+ from localstack.services.sns.v2.utils import (
77
+ create_platform_endpoint_arn,
78
+ create_subscription_arn,
79
+ encode_subscription_token_with_region,
80
+ get_next_page_token_from_arn,
81
+ get_region_from_subscription_token,
82
+ is_valid_e164_number,
83
+ parse_and_validate_platform_application_arn,
84
+ parse_and_validate_topic_arn,
85
+ validate_subscription_attribute,
86
+ )
87
+ from localstack.utils.aws.arns import (
88
+ get_partition,
89
+ parse_arn,
90
+ sns_platform_application_arn,
91
+ sns_topic_arn,
92
+ )
93
+ from localstack.utils.collections import PaginatedList, select_from_typed_dict
4
94
 
5
95
  # set up logger
6
96
  LOG = logging.getLogger(__name__)
7
97
 
98
+ SNS_TOPIC_NAME_PATTERN_FIFO = r"^[a-zA-Z0-9_-]{1,256}\.fifo$"
99
+ SNS_TOPIC_NAME_PATTERN = r"^[a-zA-Z0-9_-]{1,256}$"
100
+
101
+
102
+ class SnsProvider(SnsApi):
103
+ def __init__(self) -> None:
104
+ super().__init__()
105
+ self._publisher = PublishDispatcher()
106
+ self._signature_cert_pem: str = SNS_SERVER_CERT
107
+
108
+ ## Topic Operations
109
+
110
+ def create_topic(
111
+ self,
112
+ context: RequestContext,
113
+ name: topicName,
114
+ attributes: TopicAttributesMap | None = None,
115
+ tags: TagList | None = None,
116
+ data_protection_policy: attributeValue | None = None,
117
+ **kwargs,
118
+ ) -> CreateTopicResponse:
119
+ store = self.get_store(context.account_id, context.region)
120
+ topic_arn = sns_topic_arn(
121
+ topic_name=name, region_name=context.region, account_id=context.account_id
122
+ )
123
+ topic: Topic = store.topics.get(topic_arn)
124
+ attributes = attributes or {}
125
+ if topic:
126
+ attrs = topic["attributes"]
127
+ for k, v in attributes.values():
128
+ if not attrs.get(k) or not attrs.get(k) == v:
129
+ # TODO:
130
+ raise InvalidParameterException("Fix this Exception message and type")
131
+ tag_resource_success = _check_matching_tags(topic_arn, tags, store)
132
+ if not tag_resource_success:
133
+ raise InvalidParameterException(
134
+ "Invalid parameter: Tags Reason: Topic already exists with different tags"
135
+ )
136
+ return CreateTopicResponse(TopicArn=topic_arn)
137
+
138
+ attributes = attributes or {}
139
+ if attributes.get("FifoTopic") and attributes["FifoTopic"].lower() == "true":
140
+ fifo_match = re.match(SNS_TOPIC_NAME_PATTERN_FIFO, name)
141
+ if not fifo_match:
142
+ # TODO: check this with a separate test
143
+ raise InvalidParameterException(
144
+ "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."
145
+ )
146
+ else:
147
+ # AWS does not seem to save explicit settings of fifo = false
148
+
149
+ attributes.pop("FifoTopic", None)
150
+ name_match = re.match(SNS_TOPIC_NAME_PATTERN, name)
151
+ if not name_match:
152
+ raise InvalidParameterException("Invalid parameter: Topic Name")
153
+
154
+ topic = _create_topic(name=name, attributes=attributes, context=context)
155
+ if tags:
156
+ self.tag_resource(context=context, resource_arn=topic_arn, tags=tags)
157
+
158
+ store.topics[topic_arn] = topic
159
+
160
+ return CreateTopicResponse(TopicArn=topic_arn)
161
+
162
+ def get_topic_attributes(
163
+ self, context: RequestContext, topic_arn: topicARN, **kwargs
164
+ ) -> GetTopicAttributesResponse:
165
+ topic: Topic = self._get_topic(arn=topic_arn, context=context)
166
+ if topic:
167
+ attributes = topic["attributes"]
168
+ return GetTopicAttributesResponse(Attributes=attributes)
169
+ else:
170
+ raise NotFoundException("Topic does not exist")
171
+
172
+ def delete_topic(self, context: RequestContext, topic_arn: topicARN, **kwargs) -> None:
173
+ store = self.get_store(context.account_id, context.region)
174
+
175
+ store.topics.pop(topic_arn, None)
176
+
177
+ def list_topics(
178
+ self, context: RequestContext, next_token: nextToken | None = None, **kwargs
179
+ ) -> ListTopicsResponse:
180
+ store = self.get_store(context.account_id, context.region)
181
+ topics = [{"TopicArn": t["arn"]} for t in list(store.topics.values())]
182
+ topics = PaginatedList(topics)
183
+ page, nxt = topics.get_page(
184
+ token_generator=lambda x: get_next_page_token_from_arn(x["TopicArn"]),
185
+ next_token=next_token,
186
+ page_size=100,
187
+ )
188
+ topics = {"Topics": page, "NextToken": nxt}
189
+ return ListTopicsResponse(**topics)
190
+
191
+ def set_topic_attributes(
192
+ self,
193
+ context: RequestContext,
194
+ topic_arn: topicARN,
195
+ attribute_name: attributeName,
196
+ attribute_value: attributeValue | None = None,
197
+ **kwargs,
198
+ ) -> None:
199
+ topic: Topic = self._get_topic(arn=topic_arn, context=context)
200
+ if attribute_name == "FifoTopic":
201
+ raise InvalidParameterException("Invalid parameter: AttributeName")
202
+ topic["attributes"][attribute_name] = attribute_value
203
+
204
+ ## Subscribe operations
205
+
206
+ def subscribe(
207
+ self,
208
+ context: RequestContext,
209
+ topic_arn: topicARN,
210
+ protocol: protocol,
211
+ endpoint: endpoint | None = None,
212
+ attributes: SubscriptionAttributesMap | None = None,
213
+ return_subscription_arn: bool | None = None,
214
+ **kwargs,
215
+ ) -> SubscribeResponse:
216
+ parsed_topic_arn = parse_and_validate_topic_arn(topic_arn)
217
+ if context.region != parsed_topic_arn["region"]:
218
+ raise InvalidParameterException("Invalid parameter: TopicArn")
219
+
220
+ store = self.get_store(account_id=parsed_topic_arn["account"], region=context.region)
221
+
222
+ if topic_arn not in store.topics:
223
+ raise NotFoundException("Topic does not exist")
224
+
225
+ topic_subscriptions = store.topics[topic_arn]["subscriptions"]
226
+ if not endpoint:
227
+ # TODO: check AWS behaviour (because endpoint is optional)
228
+ raise NotFoundException("Endpoint not specified in subscription")
229
+ if protocol not in sns_constants.SNS_PROTOCOLS:
230
+ raise InvalidParameterException(
231
+ f"Invalid parameter: Amazon SNS does not support this protocol string: {protocol}"
232
+ )
233
+ elif protocol in ["http", "https"] and not endpoint.startswith(f"{protocol}://"):
234
+ raise InvalidParameterException(
235
+ "Invalid parameter: Endpoint must match the specified protocol"
236
+ )
237
+ elif protocol == "sms" and not is_valid_e164_number(endpoint):
238
+ raise InvalidParameterException(f"Invalid SMS endpoint: {endpoint}")
239
+
240
+ elif protocol == "sqs":
241
+ try:
242
+ parse_arn(endpoint)
243
+ except InvalidArnException:
244
+ raise InvalidParameterException("Invalid parameter: SQS endpoint ARN")
245
+
246
+ elif protocol == "application":
247
+ # TODO: Validate exact behaviour
248
+ try:
249
+ parse_arn(endpoint)
250
+ except InvalidArnException:
251
+ raise InvalidParameterException("Invalid parameter: ApplicationEndpoint ARN")
252
+
253
+ if ".fifo" in endpoint and ".fifo" not in topic_arn:
254
+ # TODO: move to sqs protocol block if possible
255
+ raise InvalidParameterException(
256
+ "Invalid parameter: Invalid parameter: Endpoint Reason: FIFO SQS Queues can not be subscribed to standard SNS topics"
257
+ )
258
+
259
+ sub_attributes = copy.deepcopy(attributes) if attributes else None
260
+ if sub_attributes:
261
+ for attr_name, attr_value in sub_attributes.items():
262
+ validate_subscription_attribute(
263
+ attribute_name=attr_name,
264
+ attribute_value=attr_value,
265
+ topic_arn=topic_arn,
266
+ endpoint=endpoint,
267
+ is_subscribe_call=True,
268
+ )
269
+ if raw_msg_delivery := sub_attributes.get("RawMessageDelivery"):
270
+ sub_attributes["RawMessageDelivery"] = raw_msg_delivery.lower()
271
+
272
+ # An endpoint may only be subscribed to a topic once. Subsequent
273
+ # subscribe calls do nothing (subscribe is idempotent), except if its attributes are different.
274
+ for existing_topic_subscription in topic_subscriptions:
275
+ sub = store.subscriptions.get(existing_topic_subscription, {})
276
+ if sub.get("Endpoint") == endpoint:
277
+ if sub_attributes:
278
+ # validate the subscription attributes aren't different
279
+ for attr in sns_constants.VALID_SUBSCRIPTION_ATTR_NAME:
280
+ # if a new attribute is present and different from an existent one, raise
281
+ if (new_attr := sub_attributes.get(attr)) and sub.get(attr) != new_attr:
282
+ raise InvalidParameterException(
283
+ "Invalid parameter: Attributes Reason: Subscription already exists with different attributes"
284
+ )
285
+
286
+ return SubscribeResponse(SubscriptionArn=sub["SubscriptionArn"])
287
+ principal = DUMMY_SUBSCRIPTION_PRINCIPAL.format(
288
+ partition=get_partition(context.region), account_id=context.account_id
289
+ )
290
+ subscription_arn = create_subscription_arn(topic_arn)
291
+ subscription = SnsSubscription(
292
+ # http://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html
293
+ TopicArn=topic_arn,
294
+ Endpoint=endpoint,
295
+ Protocol=protocol,
296
+ SubscriptionArn=subscription_arn,
297
+ PendingConfirmation="true",
298
+ Owner=context.account_id,
299
+ RawMessageDelivery="false", # default value, will be overridden if set
300
+ FilterPolicyScope="MessageAttributes", # default value, will be overridden if set
301
+ SubscriptionPrincipal=principal, # dummy value, could be fetched with a call to STS?
302
+ )
303
+ if sub_attributes:
304
+ subscription.update(sub_attributes)
305
+ if "FilterPolicy" in sub_attributes:
306
+ filter_policy = (
307
+ json.loads(sub_attributes["FilterPolicy"])
308
+ if sub_attributes["FilterPolicy"]
309
+ else None
310
+ )
311
+ if filter_policy:
312
+ validator = FilterPolicyValidator(
313
+ scope=subscription.get("FilterPolicyScope", "MessageAttributes"),
314
+ is_subscribe_call=True,
315
+ )
316
+ validator.validate_filter_policy(filter_policy)
317
+
318
+ store.subscription_filter_policy[subscription_arn] = filter_policy
319
+
320
+ store.subscriptions[subscription_arn] = subscription
321
+
322
+ topic_subscriptions.append(subscription_arn)
323
+
324
+ # store the token and subscription arn
325
+ # TODO: the token is a 288 hex char string
326
+ subscription_token = encode_subscription_token_with_region(region=context.region)
327
+ store.subscription_tokens[subscription_token] = subscription_arn
328
+
329
+ response_subscription_arn = subscription_arn
330
+ # Send out confirmation message for HTTP(S), fix for https://github.com/localstack/localstack/issues/881
331
+ if protocol in ["http", "https"]:
332
+ message_ctx = SnsMessage(
333
+ type=SnsMessageType.SubscriptionConfirmation,
334
+ token=subscription_token,
335
+ message=f"You have chosen to subscribe to the topic {topic_arn}.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
336
+ )
337
+ publish_ctx = SnsPublishContext(
338
+ message=message_ctx,
339
+ store=store,
340
+ request_headers=context.request.headers,
341
+ # TODO: add topic attributes once they are ported from moto to LocalStack
342
+ # topic_attributes=vars(self._get_topic(topic_arn, context)),
343
+ )
344
+ self._publisher.publish_to_topic_subscriber(
345
+ ctx=publish_ctx,
346
+ topic_arn=topic_arn,
347
+ subscription_arn=subscription_arn,
348
+ )
349
+ if not return_subscription_arn:
350
+ response_subscription_arn = "pending confirmation"
351
+
352
+ elif protocol not in ["email", "email-json"]:
353
+ # Only HTTP(S) and email subscriptions are not auto validated
354
+ # Except if the endpoint and the topic are not in the same AWS account, then you'd need to manually confirm
355
+ # the subscription with the token
356
+ # TODO: revisit for multi-account
357
+ # TODO: test with AWS for email & email-json confirmation message
358
+ # we need to add the following check:
359
+ # if parsed_topic_arn["account"] == endpoint account (depending on the type, SQS, lambda, parse the arn)
360
+ subscription["PendingConfirmation"] = "false"
361
+ subscription["ConfirmationWasAuthenticated"] = "true"
362
+
363
+ return SubscribeResponse(SubscriptionArn=response_subscription_arn)
364
+
365
+ def unsubscribe(
366
+ self, context: RequestContext, subscription_arn: subscriptionARN, **kwargs
367
+ ) -> None:
368
+ if subscription_arn is None:
369
+ raise InvalidParameterException(
370
+ "Invalid parameter: SubscriptionArn Reason: no value for required parameter",
371
+ )
372
+ count = len(subscription_arn.split(":"))
373
+ try:
374
+ parsed_arn = parse_arn(subscription_arn)
375
+ except InvalidArnException:
376
+ # TODO: check for invalid SubscriptionGUID
377
+ raise InvalidParameterException(
378
+ f"Invalid parameter: SubscriptionArn Reason: An ARN must have at least 6 elements, not {count}"
379
+ )
380
+
381
+ account_id = parsed_arn["account"]
382
+ region_name = parsed_arn["region"]
383
+
384
+ store = self.get_store(account_id=account_id, region=region_name)
385
+ if count == 6 and subscription_arn not in store.subscriptions:
386
+ raise InvalidParameterException("Invalid parameter: SubscriptionId")
387
+
388
+ # TODO: here was a moto_backend.unsubscribe call, check correct functionality and remove this comment
389
+ # before switching to v2 for production
390
+
391
+ # pop the subscription at the end, to avoid race condition by iterating over the topic subscriptions
392
+ subscription = store.subscriptions.get(subscription_arn)
393
+
394
+ if not subscription:
395
+ # unsubscribe is idempotent, so unsubscribing from a non-existing topic does nothing
396
+ return
397
+
398
+ if subscription["Protocol"] in ["http", "https"]:
399
+ # TODO: actually validate this (re)subscribe behaviour somehow (localhost.run?)
400
+ # we might need to save the sub token in the store
401
+ # TODO: AWS only sends the UnsubscribeConfirmation if the call is unauthenticated or the requester is not
402
+ # the owner
403
+ subscription_token = encode_subscription_token_with_region(region=context.region)
404
+ message_ctx = SnsMessage(
405
+ type=SnsMessageType.UnsubscribeConfirmation,
406
+ token=subscription_token,
407
+ message=f"You have chosen to deactivate subscription {subscription_arn}.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.",
408
+ )
409
+ publish_ctx = SnsPublishContext(
410
+ message=message_ctx,
411
+ store=store,
412
+ request_headers=context.request.headers,
413
+ # TODO: add the topic attributes once we ported them from moto to LocalStack
414
+ # topic_attributes=vars(moto_topic),
415
+ )
416
+ self._publisher.publish_to_topic_subscriber(
417
+ publish_ctx,
418
+ topic_arn=subscription["TopicArn"],
419
+ subscription_arn=subscription_arn,
420
+ )
421
+
422
+ with contextlib.suppress(KeyError):
423
+ store.topics[subscription["TopicArn"]]["subscriptions"].remove(subscription_arn)
424
+ store.subscription_filter_policy.pop(subscription_arn, None)
425
+ store.subscriptions.pop(subscription_arn, None)
426
+
427
+ def get_subscription_attributes(
428
+ self, context: RequestContext, subscription_arn: subscriptionARN, **kwargs
429
+ ) -> GetSubscriptionAttributesResponse:
430
+ store = self.get_store(account_id=context.account_id, region=context.region)
431
+ sub = store.subscriptions.get(subscription_arn)
432
+ if not sub:
433
+ raise NotFoundException("Subscription does not exist")
434
+ removed_attrs = ["sqs_queue_url"]
435
+ if "FilterPolicyScope" in sub and not sub.get("FilterPolicy"):
436
+ removed_attrs.append("FilterPolicyScope")
437
+ removed_attrs.append("FilterPolicy")
438
+ elif "FilterPolicy" in sub and "FilterPolicyScope" not in sub:
439
+ sub["FilterPolicyScope"] = "MessageAttributes"
440
+
441
+ attributes = {k: v for k, v in sub.items() if k not in removed_attrs}
442
+ return GetSubscriptionAttributesResponse(Attributes=attributes)
443
+
444
+ def set_subscription_attributes(
445
+ self,
446
+ context: RequestContext,
447
+ subscription_arn: subscriptionARN,
448
+ attribute_name: attributeName,
449
+ attribute_value: attributeValue = None,
450
+ **kwargs,
451
+ ) -> None:
452
+ store = self.get_store(account_id=context.account_id, region=context.region)
453
+ sub = store.subscriptions.get(subscription_arn)
454
+ if not sub:
455
+ raise NotFoundException("Subscription does not exist")
456
+
457
+ validate_subscription_attribute(
458
+ attribute_name=attribute_name,
459
+ attribute_value=attribute_value,
460
+ topic_arn=sub["TopicArn"],
461
+ endpoint=sub["Endpoint"],
462
+ )
463
+ if attribute_name == "RawMessageDelivery":
464
+ attribute_value = attribute_value.lower()
465
+
466
+ elif attribute_name == "FilterPolicy":
467
+ filter_policy = json.loads(attribute_value) if attribute_value else None
468
+ if filter_policy:
469
+ validator = FilterPolicyValidator(
470
+ scope=sub.get("FilterPolicyScope", "MessageAttributes"),
471
+ is_subscribe_call=False,
472
+ )
473
+ validator.validate_filter_policy(filter_policy)
474
+
475
+ store.subscription_filter_policy[subscription_arn] = filter_policy
476
+
477
+ sub[attribute_name] = attribute_value
478
+
479
+ def confirm_subscription(
480
+ self,
481
+ context: RequestContext,
482
+ topic_arn: topicARN,
483
+ token: String,
484
+ authenticate_on_unsubscribe: authenticateOnUnsubscribe = None,
485
+ **kwargs,
486
+ ) -> ConfirmSubscriptionResponse:
487
+ # TODO: validate format on the token (seems to be 288 hex chars)
488
+ # this request can come from any http client, it might not be signed (we would need to implement
489
+ # `authenticate_on_unsubscribe` to force a signing client to do this request.
490
+ # so, the region and account_id might not be in the request. Use the ones from the topic_arn
491
+ try:
492
+ parsed_arn = parse_arn(topic_arn)
493
+ except InvalidArnException:
494
+ raise InvalidParameterException("Invalid parameter: Topic")
495
+
496
+ store = self.get_store(account_id=parsed_arn["account"], region=parsed_arn["region"])
497
+
498
+ # it seems SNS is able to know what the region of the topic should be, even though a wrong topic is accepted
499
+ if parsed_arn["region"] != get_region_from_subscription_token(token):
500
+ raise InvalidParameterException("Invalid parameter: Topic")
501
+
502
+ subscription_arn = store.subscription_tokens.get(token)
503
+ if not subscription_arn:
504
+ raise InvalidParameterException("Invalid parameter: Token")
505
+
506
+ subscription = store.subscriptions.get(subscription_arn)
507
+ if not subscription:
508
+ # subscription could have been deleted in the meantime
509
+ raise InvalidParameterException("Invalid parameter: Token")
510
+
511
+ # ConfirmSubscription is idempotent
512
+ if subscription.get("PendingConfirmation") == "false":
513
+ return ConfirmSubscriptionResponse(SubscriptionArn=subscription_arn)
514
+
515
+ subscription["PendingConfirmation"] = "false"
516
+ subscription["ConfirmationWasAuthenticated"] = "true"
517
+
518
+ return ConfirmSubscriptionResponse(SubscriptionArn=subscription_arn)
519
+
520
+ def list_subscriptions(
521
+ self, context: RequestContext, next_token: nextToken = None, **kwargs
522
+ ) -> ListSubscriptionsResponse:
523
+ store = self.get_store(context.account_id, context.region)
524
+ subscriptions = [
525
+ select_from_typed_dict(Subscription, sub) for sub in list(store.subscriptions.values())
526
+ ]
527
+ paginated_subscriptions = PaginatedList(subscriptions)
528
+ page, next_token = paginated_subscriptions.get_page(
529
+ token_generator=lambda x: get_next_page_token_from_arn(x["SubscriptionArn"]),
530
+ page_size=100,
531
+ next_token=next_token,
532
+ )
533
+
534
+ response = ListSubscriptionsResponse(Subscriptions=page)
535
+ if next_token:
536
+ response["NextToken"] = next_token
537
+ return response
538
+
539
+ def list_subscriptions_by_topic(
540
+ self, context: RequestContext, topic_arn: topicARN, next_token: nextToken = None, **kwargs
541
+ ) -> ListSubscriptionsByTopicResponse:
542
+ topic: Topic = self._get_topic(topic_arn, context)
543
+ parsed_topic_arn = parse_and_validate_topic_arn(topic_arn)
544
+ store = self.get_store(parsed_topic_arn["account"], parsed_topic_arn["region"])
545
+ sub_arns: list[str] = topic.get("subscriptions", [])
546
+ subscriptions = [store.subscriptions[k] for k in sub_arns if k in store.subscriptions]
547
+
548
+ paginated_subscriptions = PaginatedList(subscriptions)
549
+ page, next_token = paginated_subscriptions.get_page(
550
+ token_generator=lambda x: get_next_page_token_from_arn(x["SubscriptionArn"]),
551
+ page_size=100,
552
+ next_token=next_token,
553
+ )
554
+
555
+ response = ListSubscriptionsResponse(Subscriptions=page)
556
+ if next_token:
557
+ response["NextToken"] = next_token
558
+ return response
559
+
560
+ #
561
+ # PlatformApplications
562
+ #
563
+ def create_platform_application(
564
+ self,
565
+ context: RequestContext,
566
+ name: String,
567
+ platform: String,
568
+ attributes: MapStringToString,
569
+ **kwargs,
570
+ ) -> CreatePlatformApplicationResponse:
571
+ _validate_platform_application_name(name)
572
+ if platform not in VALID_APPLICATION_PLATFORMS:
573
+ raise InvalidParameterException(
574
+ f"Invalid parameter: Platform Reason: {platform} is not supported"
575
+ )
576
+
577
+ _validate_platform_application_attributes(attributes)
578
+
579
+ # attribute validation specific to create_platform_application
580
+ if "PlatformCredential" in attributes and "PlatformPrincipal" not in attributes:
581
+ raise InvalidParameterException(
582
+ "Invalid parameter: Attributes Reason: PlatformCredential attribute provided without PlatformPrincipal"
583
+ )
584
+
585
+ elif "PlatformPrincipal" in attributes and "PlatformCredential" not in attributes:
586
+ raise InvalidParameterException(
587
+ "Invalid parameter: Attributes Reason: PlatformPrincipal attribute provided without PlatformCredential"
588
+ )
589
+
590
+ store = self.get_store(context.account_id, context.region)
591
+ # We are not validating the access data here like AWS does (against ADM and the like)
592
+ attributes.pop("PlatformPrincipal")
593
+ attributes.pop("PlatformCredential")
594
+ _attributes = {"Enabled": "true"}
595
+ _attributes.update(attributes)
596
+ application_arn = sns_platform_application_arn(
597
+ platform_application_name=name,
598
+ platform=platform,
599
+ account_id=context.account_id,
600
+ region_name=context.region,
601
+ )
602
+ platform_application_details = PlatformApplicationDetails(
603
+ platform_application=PlatformApplication(
604
+ PlatformApplicationArn=application_arn,
605
+ Attributes=_attributes,
606
+ ),
607
+ platform_endpoints={},
608
+ )
609
+ store.platform_applications[application_arn] = platform_application_details
610
+
611
+ return platform_application_details.platform_application
612
+
613
+ def delete_platform_application(
614
+ self, context: RequestContext, platform_application_arn: String, **kwargs
615
+ ) -> None:
616
+ store = self.get_store(context.account_id, context.region)
617
+ store.platform_applications.pop(platform_application_arn, None)
618
+ # TODO: if the platform had endpoints, should we remove them from the store? There is no way to list
619
+ # endpoints without an application, so this is impossible to check the state of AWS here
620
+
621
+ def list_platform_applications(
622
+ self, context: RequestContext, next_token: String | None = None, **kwargs
623
+ ) -> ListPlatformApplicationsResponse:
624
+ store = self.get_store(context.account_id, context.region)
625
+ platform_applications = store.platform_applications.values()
626
+ paginated_applications = PaginatedList(platform_applications)
627
+ page, token = paginated_applications.get_page(
628
+ token_generator=lambda x: get_next_page_token_from_arn(x["PlatformApplicationArn"]),
629
+ page_size=100,
630
+ next_token=next_token,
631
+ )
632
+
633
+ response = ListPlatformApplicationsResponse(
634
+ PlatformApplications=[platform_app.platform_application for platform_app in page]
635
+ )
636
+ if token:
637
+ response["NextToken"] = token
638
+ return response
639
+
640
+ def get_platform_application_attributes(
641
+ self, context: RequestContext, platform_application_arn: String, **kwargs
642
+ ) -> GetPlatformApplicationAttributesResponse:
643
+ platform_application = self._get_platform_application(platform_application_arn, context)
644
+ attributes = platform_application["Attributes"]
645
+ return GetPlatformApplicationAttributesResponse(Attributes=attributes)
646
+
647
+ def set_platform_application_attributes(
648
+ self,
649
+ context: RequestContext,
650
+ platform_application_arn: String,
651
+ attributes: MapStringToString,
652
+ **kwargs,
653
+ ) -> None:
654
+ parse_and_validate_platform_application_arn(platform_application_arn)
655
+ _validate_platform_application_attributes(attributes)
656
+
657
+ platform_application = self._get_platform_application(platform_application_arn, context)
658
+ platform_application["Attributes"].update(attributes)
659
+
660
+ #
661
+ # Platform Endpoints
662
+ #
663
+
664
+ def create_platform_endpoint(
665
+ self,
666
+ context: RequestContext,
667
+ platform_application_arn: String,
668
+ token: String,
669
+ custom_user_data: String | None = None,
670
+ attributes: MapStringToString | None = None,
671
+ **kwargs,
672
+ ) -> CreateEndpointResponse:
673
+ store = self.get_store(context.account_id, context.region)
674
+ application = store.platform_applications.get(platform_application_arn)
675
+ if not application:
676
+ raise NotFoundException("PlatformApplication does not exist")
677
+ endpoint_arn = application.platform_endpoints.get(token, {})
678
+ attributes = attributes or {}
679
+ _validate_endpoint_attributes(attributes, allow_empty=True)
680
+ # CustomUserData can be specified both in attributes and as parameter. Attributes take precedence
681
+ attributes.setdefault(EndpointAttributeNames.CUSTOM_USER_DATA, custom_user_data)
682
+ _attributes = {"Enabled": "true", "Token": token, **attributes}
683
+ if endpoint_arn and (
684
+ platform_endpoint_details := store.platform_endpoints.get(endpoint_arn)
685
+ ):
686
+ # endpoint for that application with that particular token already exists
687
+ if not platform_endpoint_details.platform_endpoint["Attributes"] == _attributes:
688
+ raise InvalidParameterException(
689
+ f"Invalid parameter: Token Reason: Endpoint {endpoint_arn} already exists with the same Token, but different attributes."
690
+ )
691
+ else:
692
+ return CreateEndpointResponse(EndpointArn=endpoint_arn)
693
+
694
+ endpoint_arn = create_platform_endpoint_arn(platform_application_arn)
695
+ platform_endpoint = PlatformEndpoint(
696
+ platform_application_arn=endpoint_arn,
697
+ platform_endpoint=Endpoint(
698
+ Attributes=_attributes,
699
+ EndpointArn=endpoint_arn,
700
+ ),
701
+ )
702
+ store.platform_endpoints[endpoint_arn] = platform_endpoint
703
+ application.platform_endpoints[token] = endpoint_arn
704
+
705
+ return CreateEndpointResponse(EndpointArn=endpoint_arn)
706
+
707
+ def delete_endpoint(self, context: RequestContext, endpoint_arn: String, **kwargs) -> None:
708
+ store = self.get_store(context.account_id, context.region)
709
+ platform_endpoint_details = store.platform_endpoints.pop(endpoint_arn, None)
710
+ if platform_endpoint_details:
711
+ platform_application = store.platform_applications.get(
712
+ platform_endpoint_details.platform_application_arn
713
+ )
714
+ if platform_application:
715
+ platform_endpoint = platform_endpoint_details.platform_endpoint
716
+ platform_application.platform_endpoints.pop(
717
+ platform_endpoint["Attributes"]["Token"], None
718
+ )
719
+
720
+ def list_endpoints_by_platform_application(
721
+ self,
722
+ context: RequestContext,
723
+ platform_application_arn: String,
724
+ next_token: String | None = None,
725
+ **kwargs,
726
+ ) -> ListEndpointsByPlatformApplicationResponse:
727
+ store = self.get_store(context.account_id, context.region)
728
+ platform_application = store.platform_applications.get(platform_application_arn)
729
+ if not platform_application:
730
+ raise NotFoundException("PlatformApplication does not exist")
731
+ endpoint_arns = platform_application.platform_endpoints.values()
732
+ paginated_endpoint_arns = PaginatedList(endpoint_arns)
733
+ page, token = paginated_endpoint_arns.get_page(
734
+ token_generator=lambda x: get_next_page_token_from_arn(x),
735
+ page_size=100,
736
+ next_token=next_token,
737
+ )
738
+
739
+ response = ListEndpointsByPlatformApplicationResponse(
740
+ Endpoints=[
741
+ store.platform_endpoints[endpoint_arn].platform_endpoint
742
+ for endpoint_arn in page
743
+ if endpoint_arn in store.platform_endpoints
744
+ ]
745
+ )
746
+ if token:
747
+ response["NextToken"] = token
748
+ return response
749
+
750
+ def get_endpoint_attributes(
751
+ self, context: RequestContext, endpoint_arn: String, **kwargs
752
+ ) -> GetEndpointAttributesResponse:
753
+ store = self.get_store(context.account_id, context.region)
754
+ platform_endpoint_details = store.platform_endpoints.get(endpoint_arn)
755
+ if not platform_endpoint_details:
756
+ raise NotFoundException("Endpoint does not exist")
757
+ attributes = platform_endpoint_details.platform_endpoint["Attributes"]
758
+ return GetEndpointAttributesResponse(Attributes=attributes)
759
+
760
+ def set_endpoint_attributes(
761
+ self, context: RequestContext, endpoint_arn: String, attributes: MapStringToString, **kwargs
762
+ ) -> None:
763
+ store = self.get_store(context.account_id, context.region)
764
+ platform_endpoint_details = store.platform_endpoints.get(endpoint_arn)
765
+ if not platform_endpoint_details:
766
+ raise NotFoundException("Endpoint does not exist")
767
+ _validate_endpoint_attributes(attributes)
768
+ attributes = attributes or {}
769
+ platform_endpoint_details.platform_endpoint["Attributes"].update(attributes)
770
+
771
+ #
772
+ # Sms operations
773
+ #
774
+
775
+ def set_sms_attributes(
776
+ self, context: RequestContext, attributes: MapStringToString, **kwargs
777
+ ) -> SetSMSAttributesResponse:
778
+ store = self.get_store(context.account_id, context.region)
779
+ _validate_sms_attributes(attributes)
780
+ _set_sms_attribute_default(store)
781
+ store.sms_attributes.update(attributes or {})
782
+ return SetSMSAttributesResponse()
783
+
784
+ def get_sms_attributes(
785
+ self, context: RequestContext, attributes: ListString | None = None, **kwargs
786
+ ) -> GetSMSAttributesResponse:
787
+ store = self.get_store(context.account_id, context.region)
788
+ _set_sms_attribute_default(store)
789
+ store_attributes = store.sms_attributes
790
+ return_attributes = {}
791
+ for k, v in store_attributes.items():
792
+ if not attributes or k in attributes:
793
+ return_attributes[k] = store_attributes[k]
794
+
795
+ return GetSMSAttributesResponse(attributes=return_attributes)
796
+
797
+ def list_tags_for_resource(
798
+ self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
799
+ ) -> ListTagsForResourceResponse:
800
+ store = sns_stores[context.account_id][context.region]
801
+ tags = store.TAGS.list_tags_for_resource(resource_arn)
802
+ return ListTagsForResourceResponse(Tags=tags.get("Tags"))
803
+
804
+ def tag_resource(
805
+ self, context: RequestContext, resource_arn: AmazonResourceName, tags: TagList, **kwargs
806
+ ) -> TagResourceResponse:
807
+ unique_tag_keys = {tag["Key"] for tag in tags}
808
+ if len(unique_tag_keys) < len(tags):
809
+ raise InvalidParameterException("Invalid parameter: Duplicated keys are not allowed.")
810
+ store = sns_stores[context.account_id][context.region]
811
+ store.TAGS.tag_resource(resource_arn, tags)
812
+ return TagResourceResponse()
813
+
814
+ def untag_resource(
815
+ self,
816
+ context: RequestContext,
817
+ resource_arn: AmazonResourceName,
818
+ tag_keys: TagKeyList,
819
+ **kwargs,
820
+ ) -> UntagResourceResponse:
821
+ store = sns_stores[context.account_id][context.region]
822
+ store.TAGS.untag_resource(resource_arn, tag_keys)
823
+ return UntagResourceResponse()
824
+
825
+ @staticmethod
826
+ def get_store(account_id: str, region: str) -> SnsStore:
827
+ return sns_stores[account_id][region]
828
+
829
+ # TODO: reintroduce multi-region parameter (latest before final migration from v1)
830
+ @staticmethod
831
+ def _get_topic(arn: str, context: RequestContext) -> Topic:
832
+ """
833
+ :param arn: the Topic ARN
834
+ :param context: the RequestContext of the request
835
+ :return: the model Topic
836
+ """
837
+ arn_data = parse_and_validate_topic_arn(arn)
838
+ if context.region != arn_data["region"]:
839
+ raise InvalidParameterException("Invalid parameter: TopicArn")
840
+ try:
841
+ store = SnsProvider.get_store(context.account_id, context.region)
842
+ return store.topics[arn]
843
+ except KeyError:
844
+ raise NotFoundException("Topic does not exist")
845
+
846
+ @staticmethod
847
+ def _get_platform_application(
848
+ platform_application_arn: str, context: RequestContext
849
+ ) -> PlatformApplication:
850
+ parse_and_validate_platform_application_arn(platform_application_arn)
851
+ try:
852
+ store = SnsProvider.get_store(context.account_id, context.region)
853
+ return store.platform_applications[platform_application_arn].platform_application
854
+ except KeyError:
855
+ raise NotFoundException("PlatformApplication does not exist")
856
+
857
+
858
+ def _create_topic(name: str, attributes: dict, context: RequestContext) -> Topic:
859
+ topic_arn = sns_topic_arn(
860
+ topic_name=name, region_name=context.region, account_id=context.account_id
861
+ )
862
+ topic: Topic = {
863
+ "name": name,
864
+ "arn": topic_arn,
865
+ "attributes": {},
866
+ "subscriptions": [],
867
+ }
868
+ attrs = _default_attributes(topic, context)
869
+ attrs.update(attributes or {})
870
+ topic["attributes"] = attrs
871
+
872
+ return topic
873
+
874
+
875
+ def _default_attributes(topic: Topic, context: RequestContext) -> TopicAttributesMap:
876
+ default_attributes = {
877
+ "DisplayName": "",
878
+ "Owner": context.account_id,
879
+ "Policy": _create_default_topic_policy(topic, context),
880
+ "SubscriptionsConfirmed": "0",
881
+ "SubscriptionsDeleted": "0",
882
+ "SubscriptionsPending": "0",
883
+ "TopicArn": topic["arn"],
884
+ }
885
+ if topic["name"].endswith(".fifo"):
886
+ default_attributes.update(
887
+ {
888
+ "ContentBasedDeduplication": "false",
889
+ "FifoTopic": "false",
890
+ "SignatureVersion": "2",
891
+ }
892
+ )
893
+ return default_attributes
894
+
895
+
896
+ def _create_default_topic_policy(topic: Topic, context: RequestContext) -> str:
897
+ return json.dumps(
898
+ {
899
+ "Version": "2008-10-17",
900
+ "Id": "__default_policy_ID",
901
+ "Statement": [
902
+ {
903
+ "Effect": "Allow",
904
+ "Sid": "__default_statement_ID",
905
+ "Principal": {"AWS": "*"},
906
+ "Action": [
907
+ "SNS:GetTopicAttributes",
908
+ "SNS:SetTopicAttributes",
909
+ "SNS:AddPermission",
910
+ "SNS:RemovePermission",
911
+ "SNS:DeleteTopic",
912
+ "SNS:Subscribe",
913
+ "SNS:ListSubscriptionsByTopic",
914
+ "SNS:Publish",
915
+ ],
916
+ "Resource": topic["arn"],
917
+ "Condition": {"StringEquals": {"AWS:SourceOwner": context.account_id}},
918
+ }
919
+ ],
920
+ }
921
+ )
922
+
923
+
924
+ def _validate_platform_application_name(name: str) -> None:
925
+ reason = ""
926
+ if not name:
927
+ reason = "cannot be empty"
928
+ elif not re.match(r"^.{0,256}$", name):
929
+ reason = "must be at most 256 characters long"
930
+ elif not re.match(r"^[A-Za-z0-9._-]+$", name):
931
+ reason = "must contain only characters 'a'-'z', 'A'-'Z', '0'-'9', '_', '-', and '.'"
932
+
933
+ if reason:
934
+ raise InvalidParameterException(f"Invalid parameter: {name} Reason: {reason}")
935
+
936
+
937
+ def _validate_platform_application_attributes(attributes: dict) -> None:
938
+ _check_empty_attributes(attributes)
939
+
940
+
941
+ def _check_empty_attributes(attributes: dict) -> None:
942
+ if not attributes:
943
+ raise CommonServiceException(
944
+ code="ValidationError",
945
+ message="1 validation error detected: Value null at 'attributes' failed to satisfy constraint: Member must not be null",
946
+ sender_fault=True,
947
+ )
948
+
949
+
950
+ def _validate_endpoint_attributes(attributes: dict, allow_empty: bool = False) -> None:
951
+ if not allow_empty:
952
+ _check_empty_attributes(attributes)
953
+ for key in attributes:
954
+ if key not in EndpointAttributeNames:
955
+ raise InvalidParameterException(
956
+ f"Invalid parameter: Attributes Reason: Invalid attribute name: {key}"
957
+ )
958
+ if len(attributes.get(EndpointAttributeNames.CUSTOM_USER_DATA, "")) > 2048:
959
+ raise InvalidParameterException(
960
+ "Invalid parameter: Attributes Reason: Invalid value for attribute: CustomUserData: must be at most 2048 bytes long in UTF-8 encoding"
961
+ )
962
+
963
+
964
+ def _validate_sms_attributes(attributes: dict) -> None:
965
+ for k, v in attributes.items():
966
+ if k not in SMS_ATTRIBUTE_NAMES:
967
+ raise InvalidParameterException(f"{k} is not a valid attribute")
968
+ default_send_id = attributes.get("DefaultSendID")
969
+ if default_send_id and not re.match(SMS_DEFAULT_SENDER_REGEX, default_send_id):
970
+ raise InvalidParameterException("DefaultSendID is not a valid attribute")
971
+ sms_type = attributes.get("DefaultSMSType")
972
+ if sms_type and sms_type not in SMS_TYPES:
973
+ raise InvalidParameterException("DefaultSMSType is invalid")
974
+
975
+
976
+ def _set_sms_attribute_default(store: SnsStore) -> None:
977
+ # TODO: don't call this on every sms attribute crud api call
978
+ store.sms_attributes.setdefault("MonthlySpendLimit", "1")
979
+
8
980
 
9
- class SnsProvider(SnsApi): ...
981
+ def _check_matching_tags(topic_arn: str, tags: TagList | None, store: SnsStore) -> bool:
982
+ """
983
+ Checks if a topic to be created doesn't already exist with different tags
984
+ :param topic_arn: Arn of the topic
985
+ :param tags: Tags to be checked
986
+ :param store: Store object that holds the topics and tags
987
+ :return: False if there is a mismatch in tags, True otherwise
988
+ """
989
+ existing_tags = store.TAGS.list_tags_for_resource(topic_arn)["Tags"]
990
+ # if this is none there is nothing to check
991
+ if topic_arn in store.topics:
992
+ if tags is None:
993
+ tags = []
994
+ for tag in tags:
995
+ # this means topic already created with empty tags and when we try to create it
996
+ # again with other tag value then it should fail according to aws documentation.
997
+ if existing_tags is not None and tag not in existing_tags:
998
+ return False
999
+ return True