localstack-core 4.11.2.dev14__py3-none-any.whl → 4.12.1.dev18__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/ec2/__init__.py +13 -0
- localstack/aws/api/iam/__init__.py +1 -0
- localstack/aws/api/lambda_/__init__.py +616 -0
- localstack/aws/api/logs/__init__.py +188 -0
- localstack/aws/api/opensearch/__init__.py +11 -0
- localstack/aws/api/route53/__init__.py +3 -0
- localstack/aws/api/s3/__init__.py +2 -0
- localstack/aws/api/s3control/__init__.py +19 -0
- localstack/aws/api/secretsmanager/__init__.py +9 -0
- localstack/aws/connect.py +35 -15
- localstack/config.py +8 -0
- localstack/constants.py +3 -0
- localstack/dev/kubernetes/__main__.py +39 -14
- localstack/runtime/analytics.py +11 -0
- localstack/services/acm/provider.py +13 -1
- localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +3 -1
- localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
- localstack/services/cloudformation/provider.py +26 -1
- localstack/services/cloudformation/provider_utils.py +20 -0
- localstack/services/cloudformation/resource_provider.py +5 -4
- localstack/services/cloudformation/scaffolding/__main__.py +94 -22
- localstack/services/cloudformation/v2/provider.py +41 -0
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kms/models.py +6 -2
- localstack/services/lambda_/analytics.py +11 -2
- localstack/services/lambda_/invocation/event_manager.py +15 -11
- localstack/services/lambda_/invocation/lambda_models.py +4 -0
- localstack/services/lambda_/invocation/lambda_service.py +11 -0
- localstack/services/lambda_/provider.py +70 -13
- localstack/services/opensearch/packages.py +34 -20
- localstack/services/route53/provider.py +7 -0
- localstack/services/route53resolver/provider.py +5 -0
- localstack/services/s3/constants.py +5 -0
- localstack/services/s3/exceptions.py +9 -0
- localstack/services/s3/models.py +9 -1
- localstack/services/s3/provider.py +25 -30
- localstack/services/s3/utils.py +46 -1
- localstack/services/s3control/provider.py +6 -0
- localstack/services/scheduler/provider.py +4 -2
- localstack/services/secretsmanager/provider.py +4 -0
- localstack/services/ses/provider.py +4 -0
- localstack/services/sns/constants.py +13 -0
- localstack/services/sns/provider.py +5 -0
- localstack/services/sns/v2/models.py +3 -0
- localstack/services/sns/v2/provider.py +100 -0
- localstack/services/sqs/constants.py +6 -0
- localstack/services/sqs/provider.py +9 -1
- localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
- localstack/services/ssm/provider.py +6 -0
- localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +193 -107
- localstack/services/stepfunctions/backend/execution.py +4 -5
- localstack/services/stepfunctions/provider.py +21 -14
- localstack/services/sts/provider.py +7 -0
- localstack/services/support/provider.py +5 -1
- localstack/services/swf/provider.py +5 -1
- localstack/services/transcribe/provider.py +7 -0
- localstack/testing/aws/lambda_utils.py +1 -1
- localstack/testing/aws/util.py +2 -1
- localstack/testing/config.py +1 -0
- localstack/utils/aws/client_types.py +2 -4
- localstack/utils/bootstrap.py +2 -2
- localstack/utils/catalog/catalog.py +3 -2
- localstack/utils/container_utils/container_client.py +22 -13
- localstack/utils/container_utils/docker_cmd_client.py +6 -6
- localstack/version.py +2 -2
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +6 -6
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +76 -75
- localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
- localstack_core-4.11.2.dev14.dist-info/plux.json +0 -1
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
|
@@ -33,6 +33,7 @@ from localstack.aws.api.s3 import (
|
|
|
33
33
|
BucketAlreadyOwnedByYou,
|
|
34
34
|
BucketCannedACL,
|
|
35
35
|
BucketLifecycleConfiguration,
|
|
36
|
+
BucketLocationConstraint,
|
|
36
37
|
BucketLoggingStatus,
|
|
37
38
|
BucketName,
|
|
38
39
|
BucketNotEmpty,
|
|
@@ -117,7 +118,6 @@ from localstack.aws.api.s3 import (
|
|
|
117
118
|
InvalidArgument,
|
|
118
119
|
InvalidBucketName,
|
|
119
120
|
InvalidDigest,
|
|
120
|
-
InvalidLocationConstraint,
|
|
121
121
|
InvalidObjectState,
|
|
122
122
|
InvalidPartNumber,
|
|
123
123
|
InvalidPartOrder,
|
|
@@ -229,7 +229,7 @@ from localstack.aws.handlers import (
|
|
|
229
229
|
preprocess_request,
|
|
230
230
|
serve_custom_service_request_handlers,
|
|
231
231
|
)
|
|
232
|
-
from localstack.constants import AWS_REGION_US_EAST_1
|
|
232
|
+
from localstack.constants import AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1
|
|
233
233
|
from localstack.services.edge import ROUTER
|
|
234
234
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
235
235
|
from localstack.services.s3.codec import AwsChunkedDecoder
|
|
@@ -278,6 +278,7 @@ from localstack.services.s3.utils import (
|
|
|
278
278
|
etag_to_base_64_content_md5,
|
|
279
279
|
extract_bucket_key_version_id_from_copy_source,
|
|
280
280
|
generate_safe_version_id,
|
|
281
|
+
get_bucket_location_xml,
|
|
281
282
|
get_canned_acl,
|
|
282
283
|
get_class_attrs_from_spec_class,
|
|
283
284
|
get_failed_precondition_copy_source,
|
|
@@ -304,6 +305,7 @@ from localstack.services.s3.utils import (
|
|
|
304
305
|
validate_dict_fields,
|
|
305
306
|
validate_failed_precondition,
|
|
306
307
|
validate_kms_key_id,
|
|
308
|
+
validate_location_constraint,
|
|
307
309
|
validate_tag_set,
|
|
308
310
|
)
|
|
309
311
|
from localstack.services.s3.validation import (
|
|
@@ -493,29 +495,19 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
493
495
|
raise InvalidBucketName("The specified bucket is not valid.", BucketName=bucket_name)
|
|
494
496
|
|
|
495
497
|
create_bucket_configuration = request.get("CreateBucketConfiguration") or {}
|
|
496
|
-
|
|
498
|
+
|
|
497
499
|
bucket_tags = create_bucket_configuration.get("Tags")
|
|
498
500
|
if bucket_tags:
|
|
499
501
|
validate_tag_set(bucket_tags, type_set="create-bucket")
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
507
|
-
elif context.region != bucket_region:
|
|
508
|
-
raise CommonServiceException(
|
|
509
|
-
code="IllegalLocationConstraintException",
|
|
510
|
-
message=f"The {bucket_region} location constraint is incompatible for the region specific endpoint this request was sent to.",
|
|
511
|
-
)
|
|
512
|
-
else:
|
|
502
|
+
|
|
503
|
+
location_constraint = create_bucket_configuration.get("LocationConstraint", "")
|
|
504
|
+
validate_location_constraint(context.region, location_constraint)
|
|
505
|
+
|
|
506
|
+
bucket_region = location_constraint
|
|
507
|
+
if not location_constraint:
|
|
513
508
|
bucket_region = AWS_REGION_US_EAST_1
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
code="IllegalLocationConstraintException",
|
|
517
|
-
message="The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.",
|
|
518
|
-
)
|
|
509
|
+
if location_constraint == BucketLocationConstraint.EU:
|
|
510
|
+
bucket_region = AWS_REGION_EU_WEST_1
|
|
519
511
|
|
|
520
512
|
store = self.get_store(context.account_id, bucket_region)
|
|
521
513
|
|
|
@@ -554,6 +546,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
554
546
|
acl=acl,
|
|
555
547
|
object_ownership=request.get("ObjectOwnership"),
|
|
556
548
|
object_lock_enabled_for_bucket=request.get("ObjectLockEnabledForBucket"),
|
|
549
|
+
location_constraint=location_constraint,
|
|
557
550
|
)
|
|
558
551
|
|
|
559
552
|
store.buckets[bucket_name] = s3_bucket
|
|
@@ -709,16 +702,18 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
709
702
|
"""
|
|
710
703
|
store, s3_bucket = self._get_cross_account_bucket(context, bucket)
|
|
711
704
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
705
|
+
# TODO: Remove usage of getattr once persistence mechanism is updated.
|
|
706
|
+
# If the stored constraint is None the bucket was made before the storage of location_constraint.
|
|
707
|
+
# The EU location constraint wasn't supported before this point so we can safely default to the region.
|
|
708
|
+
location_constraint = getattr(s3_bucket, "location_constraint", None)
|
|
709
|
+
if location_constraint is None:
|
|
710
|
+
location_constraint = (
|
|
711
|
+
s3_bucket.bucket_region if s3_bucket.bucket_region != "us-east-1" else ""
|
|
712
|
+
)
|
|
719
713
|
|
|
720
|
-
|
|
721
|
-
|
|
714
|
+
return GetBucketLocationOutput(
|
|
715
|
+
LocationConstraint=get_bucket_location_xml(location_constraint)
|
|
716
|
+
)
|
|
722
717
|
|
|
723
718
|
@handler("PutObject", expand=False)
|
|
724
719
|
def put_object(
|
localstack/services/s3/utils.py
CHANGED
|
@@ -34,6 +34,7 @@ from localstack.aws.api.s3 import (
|
|
|
34
34
|
Grantee,
|
|
35
35
|
HeadObjectRequest,
|
|
36
36
|
InvalidArgument,
|
|
37
|
+
InvalidLocationConstraint,
|
|
37
38
|
InvalidRange,
|
|
38
39
|
InvalidTag,
|
|
39
40
|
LifecycleExpiration,
|
|
@@ -57,18 +58,25 @@ from localstack.aws.api.s3 import (
|
|
|
57
58
|
from localstack.aws.api.s3 import Type as GranteeType
|
|
58
59
|
from localstack.aws.chain import HandlerChain
|
|
59
60
|
from localstack.aws.connect import connect_to
|
|
61
|
+
from localstack.constants import AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1
|
|
60
62
|
from localstack.http import Response
|
|
61
63
|
from localstack.services.s3 import checksums
|
|
62
64
|
from localstack.services.s3.constants import (
|
|
63
65
|
ALL_USERS_ACL_GRANTEE,
|
|
64
66
|
AUTHENTICATED_USERS_ACL_GRANTEE,
|
|
67
|
+
BUCKET_LOCATION_CONSTRAINTS,
|
|
65
68
|
CHECKSUM_ALGORITHMS,
|
|
69
|
+
EU_WEST_1_LOCATION_CONSTRAINTS,
|
|
66
70
|
LOG_DELIVERY_ACL_GRANTEE,
|
|
67
71
|
SIGNATURE_V2_PARAMS,
|
|
68
72
|
SIGNATURE_V4_PARAMS,
|
|
69
73
|
SYSTEM_METADATA_SETTABLE_HEADERS,
|
|
70
74
|
)
|
|
71
|
-
from localstack.services.s3.exceptions import
|
|
75
|
+
from localstack.services.s3.exceptions import (
|
|
76
|
+
IllegalLocationConstraintException,
|
|
77
|
+
InvalidRequest,
|
|
78
|
+
MalformedXML,
|
|
79
|
+
)
|
|
72
80
|
from localstack.utils.aws import arns
|
|
73
81
|
from localstack.utils.aws.arns import parse_arn
|
|
74
82
|
from localstack.utils.objects import singleton_factory
|
|
@@ -888,6 +896,27 @@ def validate_tag_set(
|
|
|
888
896
|
keys.add(key)
|
|
889
897
|
|
|
890
898
|
|
|
899
|
+
def validate_location_constraint(context_region: str, location_constraint: str) -> None:
|
|
900
|
+
if location_constraint:
|
|
901
|
+
if context_region == AWS_REGION_US_EAST_1:
|
|
902
|
+
if (
|
|
903
|
+
not config.ALLOW_NONSTANDARD_REGIONS
|
|
904
|
+
and location_constraint not in BUCKET_LOCATION_CONSTRAINTS
|
|
905
|
+
):
|
|
906
|
+
raise InvalidLocationConstraint(
|
|
907
|
+
"The specified location-constraint is not valid",
|
|
908
|
+
LocationConstraint=location_constraint,
|
|
909
|
+
)
|
|
910
|
+
elif context_region == AWS_REGION_EU_WEST_1:
|
|
911
|
+
if location_constraint not in EU_WEST_1_LOCATION_CONSTRAINTS:
|
|
912
|
+
raise IllegalLocationConstraintException(location_constraint)
|
|
913
|
+
elif context_region != location_constraint:
|
|
914
|
+
raise IllegalLocationConstraintException(location_constraint)
|
|
915
|
+
else:
|
|
916
|
+
if context_region != AWS_REGION_US_EAST_1:
|
|
917
|
+
raise IllegalLocationConstraintException("unspecified")
|
|
918
|
+
|
|
919
|
+
|
|
891
920
|
def get_unique_key_id(
|
|
892
921
|
bucket: BucketName, object_key: ObjectKey, version_id: ObjectVersionId
|
|
893
922
|
) -> str:
|
|
@@ -1105,3 +1134,19 @@ def is_version_older_than_other(version_id: str, other: str):
|
|
|
1105
1134
|
See `generate_safe_version_id`
|
|
1106
1135
|
"""
|
|
1107
1136
|
return base64.b64decode(version_id, altchars=b"._") < base64.b64decode(other, altchars=b"._")
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def get_bucket_location_xml(location_constraint: str) -> str:
|
|
1140
|
+
"""
|
|
1141
|
+
Returns the formatted XML for the GetBucketLocation operation.
|
|
1142
|
+
|
|
1143
|
+
:param location_constraint: The location constraint to return in the XML. It can be an empty string when
|
|
1144
|
+
it's not specified in the bucket configuration.
|
|
1145
|
+
:return: The XML response.
|
|
1146
|
+
"""
|
|
1147
|
+
|
|
1148
|
+
return (
|
|
1149
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
1150
|
+
'<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/"'
|
|
1151
|
+
+ ("/>" if not location_constraint else f">{location_constraint}</LocationConstraint>")
|
|
1152
|
+
)
|
|
@@ -12,6 +12,7 @@ from localstack.aws.api.s3control import (
|
|
|
12
12
|
from localstack.aws.forwarder import NotImplementedAvoidFallbackError
|
|
13
13
|
from localstack.services.s3.models import s3_stores
|
|
14
14
|
from localstack.services.s3control.validation import validate_tags
|
|
15
|
+
from localstack.state import StateVisitor
|
|
15
16
|
from localstack.utils.tagging import TaggingService
|
|
16
17
|
|
|
17
18
|
|
|
@@ -21,6 +22,11 @@ class NoSuchResource(CommonServiceException):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class S3ControlProvider(S3ControlApi):
|
|
25
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
26
|
+
from moto.s3control.models import s3control_backends
|
|
27
|
+
|
|
28
|
+
visitor.visit(s3control_backends)
|
|
29
|
+
|
|
24
30
|
"""
|
|
25
31
|
S3Control is a management interface for S3, and can access some of its internals with no public API
|
|
26
32
|
This requires us to access the s3 stores directly
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
|
-
from moto.scheduler.models import EventBridgeSchedulerBackend
|
|
4
|
+
from moto.scheduler.models import EventBridgeSchedulerBackend, scheduler_backends
|
|
5
5
|
|
|
6
6
|
from localstack.aws.api.scheduler import SchedulerApi, ValidationException
|
|
7
7
|
from localstack.services.events.rule import RULE_SCHEDULE_CRON_REGEX, RULE_SCHEDULE_RATE_REGEX
|
|
8
8
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
9
|
+
from localstack.state import StateVisitor
|
|
9
10
|
from localstack.utils.patch import patch
|
|
10
11
|
|
|
11
12
|
LOG = logging.getLogger(__name__)
|
|
@@ -17,7 +18,8 @@ RULE_SCHEDULE_AT_REGEX = re.compile(AT_REGEX)
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class SchedulerProvider(SchedulerApi, ServiceLifecycleHook):
|
|
20
|
-
|
|
21
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
22
|
+
visitor.visit(scheduler_backends)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def _validate_schedule_expression(schedule_expression: str) -> None:
|
|
@@ -65,6 +65,7 @@ from localstack.aws.api.secretsmanager import (
|
|
|
65
65
|
)
|
|
66
66
|
from localstack.aws.connect import connect_to
|
|
67
67
|
from localstack.services.moto import call_moto
|
|
68
|
+
from localstack.state import StateVisitor
|
|
68
69
|
from localstack.utils.aws import arns
|
|
69
70
|
from localstack.utils.patch import patch
|
|
70
71
|
from localstack.utils.time import today_no_time
|
|
@@ -105,6 +106,9 @@ class SecretsmanagerProvider(SecretsmanagerApi):
|
|
|
105
106
|
super().__init__()
|
|
106
107
|
apply_patches()
|
|
107
108
|
|
|
109
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
110
|
+
visitor.visit(secretsmanager_backends)
|
|
111
|
+
|
|
108
112
|
@staticmethod
|
|
109
113
|
def get_moto_backend_for_resource(
|
|
110
114
|
name_or_arn: str, context: RequestContext
|
|
@@ -62,6 +62,7 @@ from localstack.http import Resource, Response
|
|
|
62
62
|
from localstack.services.moto import call_moto
|
|
63
63
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
64
64
|
from localstack.services.ses.models import EmailType, SentEmail, SentEmailBody
|
|
65
|
+
from localstack.state import StateVisitor
|
|
65
66
|
from localstack.utils.aws import arns
|
|
66
67
|
from localstack.utils.files import mkdir
|
|
67
68
|
from localstack.utils.strings import long_uid, to_str
|
|
@@ -177,6 +178,9 @@ def register_ses_api_resource():
|
|
|
177
178
|
|
|
178
179
|
|
|
179
180
|
class SesProvider(SesApi, ServiceLifecycleHook):
|
|
181
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
182
|
+
visitor.visit(ses_backends)
|
|
183
|
+
|
|
180
184
|
#
|
|
181
185
|
# Lifecycle Hooks
|
|
182
186
|
#
|
|
@@ -25,6 +25,19 @@ VALID_SUBSCRIPTION_ATTR_NAME: list[str] = [
|
|
|
25
25
|
"SubscriptionRoleArn",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
+
|
|
29
|
+
VALID_POLICY_ACTIONS = [
|
|
30
|
+
"GetTopicAttributes",
|
|
31
|
+
"SetTopicAttributes",
|
|
32
|
+
"AddPermission",
|
|
33
|
+
"RemovePermission",
|
|
34
|
+
"DeleteTopic",
|
|
35
|
+
"Subscribe",
|
|
36
|
+
"ListSubscriptionsByTopic",
|
|
37
|
+
"Publish",
|
|
38
|
+
"Receive",
|
|
39
|
+
]
|
|
40
|
+
|
|
28
41
|
MSG_ATTR_NAME_REGEX = re.compile(r"^(?!\.)(?!.*\.$)(?!.*\.\.)[a-zA-Z0-9_\-.]+$")
|
|
29
42
|
ATTR_TYPE_REGEX = re.compile(r"^(String|Number|Binary)\..+$")
|
|
30
43
|
VALID_MSG_ATTR_NAME_CHARS = set(ascii_letters + digits + "." + "-" + "_")
|
|
@@ -86,6 +86,7 @@ from localstack.utils.aws.arns import (
|
|
|
86
86
|
from localstack.utils.collections import PaginatedList, select_from_typed_dict
|
|
87
87
|
from localstack.utils.strings import short_uid, to_bytes, to_str
|
|
88
88
|
|
|
89
|
+
from ...state import StateVisitor
|
|
89
90
|
from .analytics import internal_api_calls
|
|
90
91
|
|
|
91
92
|
# set up logger
|
|
@@ -118,6 +119,10 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
|
|
|
118
119
|
self._publisher = PublishDispatcher()
|
|
119
120
|
self._signature_cert_pem: str = SNS_SERVER_CERT
|
|
120
121
|
|
|
122
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
123
|
+
visitor.visit(sns_backends)
|
|
124
|
+
visitor.visit(sns_stores)
|
|
125
|
+
|
|
121
126
|
def on_before_stop(self):
|
|
122
127
|
self._publisher.shutdown()
|
|
123
128
|
|
|
@@ -7,6 +7,7 @@ from typing import Literal, TypedDict
|
|
|
7
7
|
from localstack.aws.api.sns import (
|
|
8
8
|
Endpoint,
|
|
9
9
|
MessageAttributeMap,
|
|
10
|
+
PhoneNumber,
|
|
10
11
|
PlatformApplication,
|
|
11
12
|
PublishBatchRequestEntry,
|
|
12
13
|
TopicAttributesMap,
|
|
@@ -192,5 +193,7 @@ class SnsStore(BaseStore):
|
|
|
192
193
|
|
|
193
194
|
TAGS: TaggingService = CrossRegionAttribute(default=TaggingService)
|
|
194
195
|
|
|
196
|
+
PHONE_NUMBERS_OPTED_OUT: list[PhoneNumber] = CrossRegionAttribute(default=list)
|
|
197
|
+
|
|
195
198
|
|
|
196
199
|
sns_stores = AccountRegionBundle("sns", SnsStore)
|
|
@@ -10,12 +10,15 @@ from rolo import Request, Router, route
|
|
|
10
10
|
|
|
11
11
|
from localstack.aws.api import CommonServiceException, RequestContext
|
|
12
12
|
from localstack.aws.api.sns import (
|
|
13
|
+
ActionsList,
|
|
13
14
|
AmazonResourceName,
|
|
14
15
|
BatchEntryIdsNotDistinctException,
|
|
16
|
+
CheckIfPhoneNumberIsOptedOutResponse,
|
|
15
17
|
ConfirmSubscriptionResponse,
|
|
16
18
|
CreateEndpointResponse,
|
|
17
19
|
CreatePlatformApplicationResponse,
|
|
18
20
|
CreateTopicResponse,
|
|
21
|
+
DelegatesList,
|
|
19
22
|
Endpoint,
|
|
20
23
|
EndpointDisabledException,
|
|
21
24
|
GetEndpointAttributesResponse,
|
|
@@ -26,6 +29,7 @@ from localstack.aws.api.sns import (
|
|
|
26
29
|
InvalidParameterException,
|
|
27
30
|
InvalidParameterValueException,
|
|
28
31
|
ListEndpointsByPlatformApplicationResponse,
|
|
32
|
+
ListPhoneNumbersOptedOutResponse,
|
|
29
33
|
ListPlatformApplicationsResponse,
|
|
30
34
|
ListString,
|
|
31
35
|
ListSubscriptionsByTopicResponse,
|
|
@@ -35,6 +39,7 @@ from localstack.aws.api.sns import (
|
|
|
35
39
|
MapStringToString,
|
|
36
40
|
MessageAttributeMap,
|
|
37
41
|
NotFoundException,
|
|
42
|
+
OptInPhoneNumberResponse,
|
|
38
43
|
PhoneNumber,
|
|
39
44
|
PlatformApplication,
|
|
40
45
|
PublishBatchRequestEntryList,
|
|
@@ -57,10 +62,12 @@ from localstack.aws.api.sns import (
|
|
|
57
62
|
attributeValue,
|
|
58
63
|
authenticateOnUnsubscribe,
|
|
59
64
|
endpoint,
|
|
65
|
+
label,
|
|
60
66
|
message,
|
|
61
67
|
messageStructure,
|
|
62
68
|
nextToken,
|
|
63
69
|
protocol,
|
|
70
|
+
string,
|
|
64
71
|
subject,
|
|
65
72
|
subscriptionARN,
|
|
66
73
|
topicARN,
|
|
@@ -84,6 +91,7 @@ from localstack.services.sns.constants import (
|
|
|
84
91
|
SUBSCRIPTION_TOKENS_ENDPOINT,
|
|
85
92
|
VALID_APPLICATION_PLATFORMS,
|
|
86
93
|
VALID_MSG_ATTR_NAME_CHARS,
|
|
94
|
+
VALID_POLICY_ACTIONS,
|
|
87
95
|
VALID_SUBSCRIPTION_ATTR_NAME,
|
|
88
96
|
)
|
|
89
97
|
from localstack.services.sns.filter import FilterPolicyValidator
|
|
@@ -118,6 +126,7 @@ from localstack.services.sns.v2.utils import (
|
|
|
118
126
|
parse_and_validate_topic_arn,
|
|
119
127
|
validate_subscription_attribute,
|
|
120
128
|
)
|
|
129
|
+
from localstack.state import StateVisitor
|
|
121
130
|
from localstack.utils.aws.arns import (
|
|
122
131
|
extract_account_id_from_arn,
|
|
123
132
|
extract_region_from_arn,
|
|
@@ -142,6 +151,9 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
|
|
|
142
151
|
self._publisher = PublishDispatcher()
|
|
143
152
|
self._signature_cert_pem: str = SNS_SERVER_CERT
|
|
144
153
|
|
|
154
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
155
|
+
visitor.visit(sns_stores)
|
|
156
|
+
|
|
145
157
|
def on_before_stop(self):
|
|
146
158
|
self._publisher.shutdown()
|
|
147
159
|
|
|
@@ -1073,6 +1085,94 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
|
|
|
1073
1085
|
|
|
1074
1086
|
return GetSMSAttributesResponse(attributes=return_attributes)
|
|
1075
1087
|
|
|
1088
|
+
#
|
|
1089
|
+
# Phone number operations
|
|
1090
|
+
#
|
|
1091
|
+
|
|
1092
|
+
def check_if_phone_number_is_opted_out(
|
|
1093
|
+
self, context: RequestContext, phone_number: PhoneNumber, **kwargs
|
|
1094
|
+
) -> CheckIfPhoneNumberIsOptedOutResponse:
|
|
1095
|
+
store = sns_stores[context.account_id][context.region]
|
|
1096
|
+
return CheckIfPhoneNumberIsOptedOutResponse(
|
|
1097
|
+
isOptedOut=phone_number in store.PHONE_NUMBERS_OPTED_OUT
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
def list_phone_numbers_opted_out(
|
|
1101
|
+
self, context: RequestContext, next_token: string | None = None, **kwargs
|
|
1102
|
+
) -> ListPhoneNumbersOptedOutResponse:
|
|
1103
|
+
store = self.get_store(context.account_id, context.region)
|
|
1104
|
+
numbers_opted_out = PaginatedList(store.PHONE_NUMBERS_OPTED_OUT)
|
|
1105
|
+
page, nxt = numbers_opted_out.get_page(
|
|
1106
|
+
token_generator=lambda x: x,
|
|
1107
|
+
next_token=next_token,
|
|
1108
|
+
page_size=100,
|
|
1109
|
+
)
|
|
1110
|
+
phone_numbers = {"phoneNumbers": page, "nextToken": nxt}
|
|
1111
|
+
return ListPhoneNumbersOptedOutResponse(**phone_numbers)
|
|
1112
|
+
|
|
1113
|
+
def opt_in_phone_number(
|
|
1114
|
+
self, context: RequestContext, phone_number: PhoneNumber, **kwargs
|
|
1115
|
+
) -> OptInPhoneNumberResponse:
|
|
1116
|
+
store = self.get_store(context.account_id, context.region)
|
|
1117
|
+
if phone_number in store.PHONE_NUMBERS_OPTED_OUT:
|
|
1118
|
+
store.PHONE_NUMBERS_OPTED_OUT.remove(phone_number)
|
|
1119
|
+
return OptInPhoneNumberResponse()
|
|
1120
|
+
|
|
1121
|
+
#
|
|
1122
|
+
# Permission operations
|
|
1123
|
+
#
|
|
1124
|
+
|
|
1125
|
+
def add_permission(
|
|
1126
|
+
self,
|
|
1127
|
+
context: RequestContext,
|
|
1128
|
+
topic_arn: topicARN,
|
|
1129
|
+
label: label,
|
|
1130
|
+
aws_account_id: DelegatesList,
|
|
1131
|
+
action_name: ActionsList,
|
|
1132
|
+
**kwargs,
|
|
1133
|
+
) -> None:
|
|
1134
|
+
topic: Topic = self._get_topic(topic_arn, context)
|
|
1135
|
+
policy = json.loads(topic["attributes"]["Policy"])
|
|
1136
|
+
statement = next(
|
|
1137
|
+
(statement for statement in policy["Statement"] if statement["Sid"] == label),
|
|
1138
|
+
None,
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
if statement:
|
|
1142
|
+
raise InvalidParameterException("Invalid parameter: Statement already exists")
|
|
1143
|
+
|
|
1144
|
+
if any(action not in VALID_POLICY_ACTIONS for action in action_name):
|
|
1145
|
+
raise InvalidParameterException(
|
|
1146
|
+
"Invalid parameter: Policy statement action out of service scope!"
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
principals = [
|
|
1150
|
+
f"arn:{get_partition(context.region)}:iam::{account_id}:root"
|
|
1151
|
+
for account_id in aws_account_id
|
|
1152
|
+
]
|
|
1153
|
+
actions = [f"SNS:{action}" for action in action_name]
|
|
1154
|
+
|
|
1155
|
+
statement = {
|
|
1156
|
+
"Sid": label,
|
|
1157
|
+
"Effect": "Allow",
|
|
1158
|
+
"Principal": {"AWS": principals[0] if len(principals) == 1 else principals},
|
|
1159
|
+
"Action": actions[0] if len(actions) == 1 else actions,
|
|
1160
|
+
"Resource": topic_arn,
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
policy["Statement"].append(statement)
|
|
1164
|
+
topic["attributes"]["Policy"] = json.dumps(policy)
|
|
1165
|
+
|
|
1166
|
+
def remove_permission(
|
|
1167
|
+
self, context: RequestContext, topic_arn: topicARN, label: label, **kwargs
|
|
1168
|
+
) -> None:
|
|
1169
|
+
topic = self._get_topic(topic_arn, context)
|
|
1170
|
+
policy = json.loads(topic["attributes"]["Policy"])
|
|
1171
|
+
statements = policy["Statement"]
|
|
1172
|
+
statements = [statement for statement in statements if statement["Sid"] != label]
|
|
1173
|
+
policy["Statement"] = statements
|
|
1174
|
+
topic["attributes"]["Policy"] = json.dumps(policy)
|
|
1175
|
+
|
|
1076
1176
|
def list_tags_for_resource(
|
|
1077
1177
|
self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
|
|
1078
1178
|
) -> ListTagsForResourceResponse:
|
|
@@ -31,6 +31,12 @@ INTERNAL_QUEUE_ATTRIBUTES = [
|
|
|
31
31
|
QueueAttributeName.QueueArn,
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
+
#
|
|
35
|
+
# If these attributes are set to their default values, they are effectively
|
|
36
|
+
# deleted from the queue attributes and not returned in future calls to get_queue_attributes()
|
|
37
|
+
#
|
|
38
|
+
DELETE_IF_DEFAULT = {"KmsMasterKeyId": "", "KmsDataKeyReusePeriodSeconds": "300"}
|
|
39
|
+
|
|
34
40
|
INVALID_STANDARD_QUEUE_ATTRIBUTES = [
|
|
35
41
|
QueueAttributeName.FifoQueue,
|
|
36
42
|
QueueAttributeName.ContentBasedDeduplication,
|
|
@@ -101,6 +101,7 @@ from localstack.services.sqs.utils import (
|
|
|
101
101
|
parse_queue_url,
|
|
102
102
|
)
|
|
103
103
|
from localstack.services.stores import AccountRegionBundle
|
|
104
|
+
from localstack.state import StateVisitor
|
|
104
105
|
from localstack.utils.aws.arns import parse_arn
|
|
105
106
|
from localstack.utils.bootstrap import is_api_enabled
|
|
106
107
|
from localstack.utils.cloudwatch.cloudwatch_util import (
|
|
@@ -659,6 +660,9 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
659
660
|
self._router_rules = []
|
|
660
661
|
self._init_cloudwatch_metrics_reporting()
|
|
661
662
|
|
|
663
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
664
|
+
visitor.visit(sqs_stores)
|
|
665
|
+
|
|
662
666
|
@staticmethod
|
|
663
667
|
def get_store(account_id: str, region: str) -> SqsStore:
|
|
664
668
|
return sqs_stores[account_id][region]
|
|
@@ -1269,7 +1273,11 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
1269
1273
|
for k, v in attributes.items():
|
|
1270
1274
|
if k in sqs_constants.INTERNAL_QUEUE_ATTRIBUTES:
|
|
1271
1275
|
raise InvalidAttributeName(f"Unknown Attribute {k}.")
|
|
1272
|
-
|
|
1276
|
+
if k in sqs_constants.DELETE_IF_DEFAULT and v == sqs_constants.DELETE_IF_DEFAULT[k]:
|
|
1277
|
+
if k in queue.attributes:
|
|
1278
|
+
del queue.attributes[k]
|
|
1279
|
+
else:
|
|
1280
|
+
queue.attributes[k] = v
|
|
1273
1281
|
|
|
1274
1282
|
# Special cases
|
|
1275
1283
|
if queue.attributes.get(QueueAttributeName.Policy) == "":
|