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