localstack-core 4.10.1.dev42__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.
Potentially problematic release.
This version of localstack-core might be problematic. Click here for more details.
- localstack/aws/api/apigateway/__init__.py +42 -0
- localstack/aws/api/cloudformation/__init__.py +161 -0
- localstack/aws/api/ec2/__init__.py +1178 -12
- localstack/aws/api/iam/__init__.py +228 -0
- localstack/aws/api/kms/__init__.py +1 -0
- localstack/aws/api/lambda_/__init__.py +1034 -66
- localstack/aws/api/logs/__init__.py +500 -0
- localstack/aws/api/opensearch/__init__.py +100 -0
- localstack/aws/api/redshift/__init__.py +69 -0
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
- localstack/aws/api/route53/__init__.py +45 -0
- localstack/aws/api/route53resolver/__init__.py +1 -0
- localstack/aws/api/s3/__init__.py +64 -0
- localstack/aws/api/s3control/__init__.py +19 -0
- localstack/aws/api/secretsmanager/__init__.py +37 -23
- localstack/aws/api/stepfunctions/__init__.py +52 -10
- localstack/aws/api/sts/__init__.py +52 -0
- localstack/aws/connect.py +35 -15
- localstack/aws/handlers/logging.py +8 -4
- localstack/aws/handlers/service.py +11 -2
- localstack/aws/protocol/serializer.py +1 -1
- localstack/config.py +8 -0
- localstack/constants.py +3 -0
- localstack/deprecations.py +0 -6
- localstack/dev/kubernetes/__main__.py +39 -14
- localstack/runtime/analytics.py +11 -0
- localstack/services/acm/provider.py +17 -1
- localstack/services/apigateway/legacy/provider.py +28 -15
- localstack/services/cloudformation/engine/template_preparer.py +6 -2
- localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +15 -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/cloudwatch/provider.py +10 -3
- localstack/services/cloudwatch/provider_v2.py +6 -3
- localstack/services/configservice/provider.py +5 -1
- localstack/services/dynamodb/provider.py +1 -0
- localstack/services/dynamodb/v2/provider.py +1 -0
- localstack/services/dynamodbstreams/provider.py +6 -0
- localstack/services/dynamodbstreams/v2/provider.py +6 -0
- localstack/services/ec2/provider.py +6 -0
- localstack/services/es/provider.py +6 -0
- localstack/services/events/provider.py +4 -0
- localstack/services/events/v1/provider.py +9 -0
- localstack/services/firehose/provider.py +5 -0
- localstack/services/iam/provider.py +4 -0
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kms/models.py +16 -22
- localstack/services/kms/provider.py +4 -0
- localstack/services/lambda_/analytics.py +11 -2
- localstack/services/lambda_/api_utils.py +37 -20
- localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
- localstack/services/lambda_/invocation/assignment.py +4 -1
- localstack/services/lambda_/invocation/event_manager.py +15 -11
- localstack/services/lambda_/invocation/execution_environment.py +21 -2
- localstack/services/lambda_/invocation/lambda_models.py +31 -2
- localstack/services/lambda_/invocation/lambda_service.py +62 -3
- localstack/services/lambda_/invocation/models.py +9 -1
- localstack/services/lambda_/invocation/version_manager.py +18 -3
- localstack/services/lambda_/provider.py +307 -106
- localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
- localstack/services/lambda_/runtimes.py +3 -1
- localstack/services/logs/provider.py +9 -0
- localstack/services/opensearch/packages.py +34 -20
- localstack/services/opensearch/provider.py +53 -3
- localstack/services/resource_groups/provider.py +5 -1
- localstack/services/resourcegroupstaggingapi/provider.py +6 -1
- 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 +51 -43
- localstack/services/s3/utils.py +81 -15
- localstack/services/s3control/provider.py +107 -2
- localstack/services/s3control/validation.py +50 -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 +16 -1
- localstack/services/sns/provider.py +5 -0
- localstack/services/sns/publisher.py +15 -6
- localstack/services/sns/v2/models.py +9 -0
- localstack/services/sns/v2/provider.py +750 -19
- localstack/services/sns/v2/utils.py +12 -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/component/common/path/result_path.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
- localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
- localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
- localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
- localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
- localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
- localstack/services/stepfunctions/asl/eval/environment.py +30 -22
- localstack/services/stepfunctions/asl/eval/states.py +1 -1
- localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
- localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
- localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
- localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
- localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
- localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +256 -22
- localstack/services/stepfunctions/backend/execution.py +10 -11
- localstack/services/stepfunctions/backend/execution_worker.py +5 -5
- localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
- localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
- localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
- localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
- localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
- localstack/services/stepfunctions/provider.py +83 -25
- localstack/services/stepfunctions/test_state/mock_config.py +47 -0
- 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/testing/pytest/fixtures.py +28 -0
- localstack/testing/snapshots/transformer_utility.py +5 -0
- localstack/utils/analytics/publisher.py +37 -155
- localstack/utils/analytics/service_request_aggregator.py +6 -4
- localstack/utils/aws/arns.py +7 -0
- localstack/utils/aws/client_types.py +2 -4
- localstack/utils/batching.py +258 -0
- localstack/utils/bootstrap.py +2 -2
- localstack/utils/catalog/catalog.py +3 -2
- localstack/utils/collections.py +23 -11
- 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.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +7 -7
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +155 -146
- localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
- localstack/services/stepfunctions/mocking/__init__.py +0 -0
- localstack/utils/batch_policy.py +0 -124
- localstack_core-4.10.1.dev42.dist-info/plux.json +0 -1
- /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
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
|
|
@@ -421,6 +429,7 @@ def get_failed_upload_part_copy_source_preconditions(
|
|
|
421
429
|
if_none_match = request.get("CopySourceIfNoneMatch")
|
|
422
430
|
if_unmodified_since = request.get("CopySourceIfUnmodifiedSince")
|
|
423
431
|
if_modified_since = request.get("CopySourceIfModifiedSince")
|
|
432
|
+
last_modified = second_resolution_datetime(last_modified)
|
|
424
433
|
|
|
425
434
|
if if_match:
|
|
426
435
|
if if_match.strip('"') != etag.strip('"'):
|
|
@@ -431,15 +440,15 @@ def get_failed_upload_part_copy_source_preconditions(
|
|
|
431
440
|
if if_unmodified_since:
|
|
432
441
|
return None
|
|
433
442
|
|
|
434
|
-
if if_unmodified_since and if_unmodified_since < last_modified:
|
|
443
|
+
if if_unmodified_since and second_resolution_datetime(if_unmodified_since) < last_modified:
|
|
435
444
|
return "x-amz-copy-source-If-Unmodified-Since"
|
|
436
445
|
|
|
437
446
|
if if_none_match and if_none_match.strip('"') == etag.strip('"'):
|
|
438
447
|
return "x-amz-copy-source-If-None-Match"
|
|
439
448
|
|
|
440
|
-
if if_modified_since and last_modified
|
|
441
|
-
|
|
442
|
-
):
|
|
449
|
+
if if_modified_since and last_modified <= second_resolution_datetime(
|
|
450
|
+
if_modified_since
|
|
451
|
+
) < datetime.datetime.now(tz=_gmt_zone_info):
|
|
443
452
|
return "x-amz-copy-source-If-Modified-Since"
|
|
444
453
|
|
|
445
454
|
|
|
@@ -701,6 +710,10 @@ def str_to_rfc_1123_datetime(value: str) -> datetime.datetime:
|
|
|
701
710
|
return datetime.datetime.strptime(value, RFC1123).replace(tzinfo=_gmt_zone_info)
|
|
702
711
|
|
|
703
712
|
|
|
713
|
+
def second_resolution_datetime(src: datetime.datetime) -> datetime.datetime:
|
|
714
|
+
return src.replace(microsecond=0)
|
|
715
|
+
|
|
716
|
+
|
|
704
717
|
def add_expiration_days_to_datetime(user_datatime: datetime.datetime, exp_days: int) -> str:
|
|
705
718
|
"""
|
|
706
719
|
This adds expiration days to a datetime, rounding to the next day at midnight UTC.
|
|
@@ -836,13 +849,20 @@ def parse_tagging_header(tagging_header: TaggingHeader) -> dict:
|
|
|
836
849
|
)
|
|
837
850
|
|
|
838
851
|
|
|
839
|
-
def validate_tag_set(
|
|
852
|
+
def validate_tag_set(
|
|
853
|
+
tag_set: TagSet, type_set: Literal["bucket", "object", "create-bucket"] = "bucket"
|
|
854
|
+
):
|
|
840
855
|
keys = set()
|
|
841
856
|
for tag in tag_set:
|
|
842
857
|
if set(tag) != {"Key", "Value"}:
|
|
843
858
|
raise MalformedXML()
|
|
844
859
|
|
|
845
860
|
key = tag["Key"]
|
|
861
|
+
value = tag["Value"]
|
|
862
|
+
|
|
863
|
+
if key is None or value is None:
|
|
864
|
+
raise MalformedXML()
|
|
865
|
+
|
|
846
866
|
if key in keys:
|
|
847
867
|
raise InvalidTag(
|
|
848
868
|
"Cannot provide multiple Tags with the same key",
|
|
@@ -852,11 +872,15 @@ def validate_tag_set(tag_set: TagSet, type_set: Literal["bucket", "object"] = "b
|
|
|
852
872
|
if key.startswith("aws:"):
|
|
853
873
|
if type_set == "bucket":
|
|
854
874
|
message = "System tags cannot be added/updated by requester"
|
|
855
|
-
|
|
875
|
+
elif type_set == "object":
|
|
856
876
|
message = "Your TagKey cannot be prefixed with aws:"
|
|
877
|
+
else:
|
|
878
|
+
message = 'User-defined tag keys can\'t start with "aws:". This prefix is reserved for system tags. Remove "aws:" from your tag keys and try again.'
|
|
857
879
|
raise InvalidTag(
|
|
858
880
|
message,
|
|
859
|
-
TagKey
|
|
881
|
+
# weirdly, AWS does not return the `TagKey` field here, but it does if the TagKey does not match the
|
|
882
|
+
# regex in the next step
|
|
883
|
+
TagKey=key if type_set != "create-bucket" else None,
|
|
860
884
|
)
|
|
861
885
|
|
|
862
886
|
if not TAG_REGEX.match(key):
|
|
@@ -864,14 +888,35 @@ def validate_tag_set(tag_set: TagSet, type_set: Literal["bucket", "object"] = "b
|
|
|
864
888
|
"The TagKey you have provided is invalid",
|
|
865
889
|
TagKey=key,
|
|
866
890
|
)
|
|
867
|
-
elif not TAG_REGEX.match(
|
|
891
|
+
elif not TAG_REGEX.match(value):
|
|
868
892
|
raise InvalidTag(
|
|
869
|
-
"The TagValue you have provided is invalid", TagKey=key, TagValue=
|
|
893
|
+
"The TagValue you have provided is invalid", TagKey=key, TagValue=value
|
|
870
894
|
)
|
|
871
895
|
|
|
872
896
|
keys.add(key)
|
|
873
897
|
|
|
874
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
|
+
|
|
875
920
|
def get_unique_key_id(
|
|
876
921
|
bucket: BucketName, object_key: ObjectKey, version_id: ObjectVersionId
|
|
877
922
|
) -> str:
|
|
@@ -908,6 +953,7 @@ def get_failed_precondition_copy_source(
|
|
|
908
953
|
:param etag: source object ETag
|
|
909
954
|
:return str: the failed precondition to raise
|
|
910
955
|
"""
|
|
956
|
+
last_modified = second_resolution_datetime(last_modified)
|
|
911
957
|
if (cs_if_match := request.get("CopySourceIfMatch")) and etag.strip('"') != cs_if_match.strip(
|
|
912
958
|
'"'
|
|
913
959
|
):
|
|
@@ -915,7 +961,7 @@ def get_failed_precondition_copy_source(
|
|
|
915
961
|
|
|
916
962
|
elif (
|
|
917
963
|
cs_if_unmodified_since := request.get("CopySourceIfUnmodifiedSince")
|
|
918
|
-
) and last_modified > cs_if_unmodified_since:
|
|
964
|
+
) and last_modified > second_resolution_datetime(cs_if_unmodified_since):
|
|
919
965
|
return "x-amz-copy-source-If-Unmodified-Since"
|
|
920
966
|
|
|
921
967
|
elif (cs_if_none_match := request.get("CopySourceIfNoneMatch")) and etag.strip(
|
|
@@ -925,7 +971,9 @@ def get_failed_precondition_copy_source(
|
|
|
925
971
|
|
|
926
972
|
elif (
|
|
927
973
|
cs_if_modified_since := request.get("CopySourceIfModifiedSince")
|
|
928
|
-
) and last_modified
|
|
974
|
+
) and last_modified <= second_resolution_datetime(cs_if_modified_since) < datetime.datetime.now(
|
|
975
|
+
tz=_gmt_zone_info
|
|
976
|
+
):
|
|
929
977
|
return "x-amz-copy-source-If-Modified-Since"
|
|
930
978
|
|
|
931
979
|
|
|
@@ -943,13 +991,13 @@ def validate_failed_precondition(
|
|
|
943
991
|
"""
|
|
944
992
|
precondition_failed = None
|
|
945
993
|
# last_modified needs to be rounded to a second so that strict equality can be enforced from a RFC1123 header
|
|
946
|
-
last_modified = last_modified
|
|
994
|
+
last_modified = second_resolution_datetime(last_modified)
|
|
947
995
|
if (if_match := request.get("IfMatch")) and etag != if_match.strip('"'):
|
|
948
996
|
precondition_failed = "If-Match"
|
|
949
997
|
|
|
950
998
|
elif (
|
|
951
999
|
if_unmodified_since := request.get("IfUnmodifiedSince")
|
|
952
|
-
) and last_modified > if_unmodified_since:
|
|
1000
|
+
) and last_modified > second_resolution_datetime(if_unmodified_since):
|
|
953
1001
|
precondition_failed = "If-Unmodified-Since"
|
|
954
1002
|
|
|
955
1003
|
if precondition_failed:
|
|
@@ -960,7 +1008,9 @@ def validate_failed_precondition(
|
|
|
960
1008
|
|
|
961
1009
|
if ((if_none_match := request.get("IfNoneMatch")) and etag == if_none_match.strip('"')) or (
|
|
962
1010
|
(if_modified_since := request.get("IfModifiedSince"))
|
|
963
|
-
and last_modified
|
|
1011
|
+
and last_modified
|
|
1012
|
+
<= second_resolution_datetime(if_modified_since)
|
|
1013
|
+
< datetime.datetime.now(tz=_gmt_zone_info)
|
|
964
1014
|
):
|
|
965
1015
|
raise CommonServiceException(
|
|
966
1016
|
message="Not Modified",
|
|
@@ -1084,3 +1134,19 @@ def is_version_older_than_other(version_id: str, other: str):
|
|
|
1084
1134
|
See `generate_safe_version_id`
|
|
1085
1135
|
"""
|
|
1086
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
|
+
)
|
|
@@ -1,5 +1,110 @@
|
|
|
1
|
-
from localstack.aws.api
|
|
1
|
+
from localstack.aws.api import CommonServiceException, RequestContext
|
|
2
|
+
from localstack.aws.api.s3control import (
|
|
3
|
+
AccountId,
|
|
4
|
+
ListTagsForResourceResult,
|
|
5
|
+
S3ControlApi,
|
|
6
|
+
S3ResourceArn,
|
|
7
|
+
TagKeyList,
|
|
8
|
+
TagList,
|
|
9
|
+
TagResourceResult,
|
|
10
|
+
UntagResourceResult,
|
|
11
|
+
)
|
|
12
|
+
from localstack.aws.forwarder import NotImplementedAvoidFallbackError
|
|
13
|
+
from localstack.services.s3.models import s3_stores
|
|
14
|
+
from localstack.services.s3control.validation import validate_tags
|
|
15
|
+
from localstack.state import StateVisitor
|
|
16
|
+
from localstack.utils.tagging import TaggingService
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NoSuchResource(CommonServiceException):
|
|
20
|
+
def __init__(self, message=None):
|
|
21
|
+
super().__init__("NoSuchResource", status_code=404, message=message)
|
|
2
22
|
|
|
3
23
|
|
|
4
24
|
class S3ControlProvider(S3ControlApi):
|
|
5
|
-
|
|
25
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
26
|
+
from moto.s3control.models import s3control_backends
|
|
27
|
+
|
|
28
|
+
visitor.visit(s3control_backends)
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
S3Control is a management interface for S3, and can access some of its internals with no public API
|
|
32
|
+
This requires us to access the s3 stores directly
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _get_tagging_service_for_bucket(
|
|
37
|
+
resource_arn: S3ResourceArn,
|
|
38
|
+
partition: str,
|
|
39
|
+
region: str,
|
|
40
|
+
account_id: str,
|
|
41
|
+
) -> TaggingService:
|
|
42
|
+
s3_prefix = f"arn:{partition}:s3:::"
|
|
43
|
+
if not resource_arn.startswith(s3_prefix):
|
|
44
|
+
# Moto does not support Tagging operations for S3 Control, so we should not forward those operations back
|
|
45
|
+
# to it
|
|
46
|
+
raise NotImplementedAvoidFallbackError(
|
|
47
|
+
"LocalStack only support Bucket tagging operations for S3Control"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
store = s3_stores[account_id][region]
|
|
51
|
+
bucket_name = resource_arn.removeprefix(s3_prefix)
|
|
52
|
+
if bucket_name not in store.global_bucket_map:
|
|
53
|
+
raise NoSuchResource("The specified resource doesn't exist.")
|
|
54
|
+
|
|
55
|
+
return store.TAGS
|
|
56
|
+
|
|
57
|
+
def tag_resource(
|
|
58
|
+
self,
|
|
59
|
+
context: RequestContext,
|
|
60
|
+
account_id: AccountId,
|
|
61
|
+
resource_arn: S3ResourceArn,
|
|
62
|
+
tags: TagList,
|
|
63
|
+
**kwargs,
|
|
64
|
+
) -> TagResourceResult:
|
|
65
|
+
# currently S3Control only supports tagging buckets
|
|
66
|
+
tagging_service = self._get_tagging_service_for_bucket(
|
|
67
|
+
resource_arn=resource_arn,
|
|
68
|
+
partition=context.partition,
|
|
69
|
+
region=context.region,
|
|
70
|
+
account_id=account_id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
validate_tags(tags=tags)
|
|
74
|
+
tagging_service.tag_resource(resource_arn, tags)
|
|
75
|
+
|
|
76
|
+
return TagResourceResult()
|
|
77
|
+
|
|
78
|
+
def untag_resource(
|
|
79
|
+
self,
|
|
80
|
+
context: RequestContext,
|
|
81
|
+
account_id: AccountId,
|
|
82
|
+
resource_arn: S3ResourceArn,
|
|
83
|
+
tag_keys: TagKeyList,
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> UntagResourceResult:
|
|
86
|
+
# currently S3Control only supports tagging buckets
|
|
87
|
+
tagging_service = self._get_tagging_service_for_bucket(
|
|
88
|
+
resource_arn=resource_arn,
|
|
89
|
+
partition=context.partition,
|
|
90
|
+
region=context.region,
|
|
91
|
+
account_id=account_id,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
tagging_service.untag_resource(resource_arn, tag_keys)
|
|
95
|
+
|
|
96
|
+
return TagResourceResult()
|
|
97
|
+
|
|
98
|
+
def list_tags_for_resource(
|
|
99
|
+
self, context: RequestContext, account_id: AccountId, resource_arn: S3ResourceArn, **kwargs
|
|
100
|
+
) -> ListTagsForResourceResult:
|
|
101
|
+
# currently S3Control only supports tagging buckets
|
|
102
|
+
tagging_service = self._get_tagging_service_for_bucket(
|
|
103
|
+
resource_arn=resource_arn,
|
|
104
|
+
partition=context.partition,
|
|
105
|
+
region=context.region,
|
|
106
|
+
account_id=account_id,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
tags = tagging_service.list_tags_for_resource(resource_arn)
|
|
110
|
+
return ListTagsForResourceResult(Tags=tags["Tags"])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from localstack.aws.api.s3 import InvalidTag
|
|
2
|
+
from localstack.aws.api.s3control import Tag, TagList
|
|
3
|
+
from localstack.services.s3.exceptions import MalformedXML
|
|
4
|
+
from localstack.services.s3.utils import TAG_REGEX
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_tags(tags: TagList):
|
|
8
|
+
"""
|
|
9
|
+
Validate the tags provided. This is the same function as S3, but with different error messages
|
|
10
|
+
:param tags: a TagList object
|
|
11
|
+
:raises MalformedXML if the object does not conform to the schema
|
|
12
|
+
:raises InvalidTag if the tag key or value are outside the set of validations defined by S3 and S3Control
|
|
13
|
+
:return: None
|
|
14
|
+
"""
|
|
15
|
+
keys = set()
|
|
16
|
+
for tag in tags:
|
|
17
|
+
tag: Tag
|
|
18
|
+
if set(tag) != {"Key", "Value"}:
|
|
19
|
+
raise MalformedXML()
|
|
20
|
+
|
|
21
|
+
key = tag["Key"]
|
|
22
|
+
value = tag["Value"]
|
|
23
|
+
|
|
24
|
+
if key is None or value is None:
|
|
25
|
+
raise MalformedXML()
|
|
26
|
+
|
|
27
|
+
if key in keys:
|
|
28
|
+
raise InvalidTag(
|
|
29
|
+
"There are duplicate tag keys in your request. Remove the duplicate tag keys and try again.",
|
|
30
|
+
TagKey=key,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if key.startswith("aws:"):
|
|
34
|
+
raise InvalidTag(
|
|
35
|
+
'User-defined tag keys can\'t start with "aws:". This prefix is reserved for system tags. Remove "aws:" from your tag keys and try again.',
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not TAG_REGEX.match(key):
|
|
39
|
+
raise InvalidTag(
|
|
40
|
+
"This request contains a tag key or value that isn't valid. Valid characters include the following: [a-zA-Z+-=._:/]. Tag keys can contain up to 128 characters. Tag values can contain up to 256 characters.",
|
|
41
|
+
TagKey=key,
|
|
42
|
+
)
|
|
43
|
+
elif not TAG_REGEX.match(value):
|
|
44
|
+
raise InvalidTag(
|
|
45
|
+
"This request contains a tag key or value that isn't valid. Valid characters include the following: [a-zA-Z+-=._:/]. Tag keys can contain up to 128 characters. Tag values can contain up to 256 characters.",
|
|
46
|
+
TagKey=key,
|
|
47
|
+
TagValue=value,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
keys.add(key)
|
|
@@ -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,9 +25,23 @@ 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 + "." + "-" + "_")
|
|
44
|
+
E164_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
|
|
31
45
|
|
|
32
46
|
|
|
33
47
|
GCM_URL = "https://fcm.googleapis.com/fcm/send"
|
|
@@ -42,6 +56,7 @@ SUBSCRIPTION_TOKENS_ENDPOINT = "/_aws/sns/subscription-tokens"
|
|
|
42
56
|
SNS_CERT_ENDPOINT = "/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem"
|
|
43
57
|
|
|
44
58
|
DUMMY_SUBSCRIPTION_PRINCIPAL = "arn:{partition}:iam::{account_id}:user/DummySNSPrincipal"
|
|
45
|
-
E164_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
|
|
46
59
|
|
|
47
60
|
VALID_APPLICATION_PLATFORMS = list(get_args(SnsApplicationPlatforms))
|
|
61
|
+
|
|
62
|
+
MAXIMUM_MESSAGE_LENGTH = 262144
|
|
@@ -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
|
|
|
@@ -30,6 +30,7 @@ from localstack.services.sns.models import (
|
|
|
30
30
|
SnsStore,
|
|
31
31
|
SnsSubscription,
|
|
32
32
|
)
|
|
33
|
+
from localstack.services.sns.v2.utils import get_topic_subscriptions
|
|
33
34
|
from localstack.utils.aws.arns import (
|
|
34
35
|
PARTITION_NAMES,
|
|
35
36
|
extract_account_id_from_arn,
|
|
@@ -254,9 +255,11 @@ class LambdaTopicPublisher(TopicPublisher):
|
|
|
254
255
|
"UnsubscribeUrl": unsubscribe_url,
|
|
255
256
|
"MessageAttributes": message_attributes,
|
|
256
257
|
}
|
|
257
|
-
|
|
258
|
+
# TODO: remove v1 "signature_version" access once v1 is retired
|
|
258
259
|
signature_version = (
|
|
259
|
-
topic_attributes.get("signature_version", "
|
|
260
|
+
topic_attributes.get("signature_version", topic_attributes.get("SignatureVersion", "1"))
|
|
261
|
+
if topic_attributes
|
|
262
|
+
else "1"
|
|
260
263
|
)
|
|
261
264
|
canonical_string = compute_canonical_string(event_payload, message_context.type)
|
|
262
265
|
signature = get_message_signature(canonical_string, signature_version=signature_version)
|
|
@@ -558,7 +561,10 @@ class HttpTopicPublisher(TopicPublisher):
|
|
|
558
561
|
):
|
|
559
562
|
return sub_content_type
|
|
560
563
|
|
|
561
|
-
|
|
564
|
+
# TODO: remove lower case access once legacy v1 provider is removed
|
|
565
|
+
if json_topic_delivery_policy := topic_attributes.get(
|
|
566
|
+
"delivery_policy", topic_attributes.get("DeliveryPolicy")
|
|
567
|
+
):
|
|
562
568
|
topic_delivery_policy = json.loads(json_topic_delivery_policy)
|
|
563
569
|
if not (
|
|
564
570
|
topic_content_type := topic_delivery_policy.get(subscriber["Protocol"].lower())
|
|
@@ -1009,7 +1015,10 @@ def create_sns_message_body(
|
|
|
1009
1015
|
# FIFO topics do not add the signature in the message
|
|
1010
1016
|
if not subscriber.get("TopicArn", "").endswith(".fifo"):
|
|
1011
1017
|
signature_version = (
|
|
1012
|
-
|
|
1018
|
+
# we allow for both casings, depending on v1 or v2 provider
|
|
1019
|
+
topic_attributes.get("signature_version", topic_attributes.get("SignatureVersion", "1"))
|
|
1020
|
+
if topic_attributes
|
|
1021
|
+
else "1"
|
|
1013
1022
|
)
|
|
1014
1023
|
canonical_string = compute_canonical_string(data, message_type)
|
|
1015
1024
|
signature = get_message_signature(canonical_string, signature_version=signature_version)
|
|
@@ -1234,7 +1243,7 @@ class PublishDispatcher:
|
|
|
1234
1243
|
)
|
|
1235
1244
|
|
|
1236
1245
|
def publish_to_topic(self, ctx: SnsPublishContext, topic_arn: str) -> None:
|
|
1237
|
-
subscriptions = ctx.store
|
|
1246
|
+
subscriptions = get_topic_subscriptions(ctx.store, topic_arn)
|
|
1238
1247
|
for subscriber in subscriptions:
|
|
1239
1248
|
if self._should_publish(ctx.store.subscription_filter_policy, ctx.message, subscriber):
|
|
1240
1249
|
notifier = self.topic_notifiers[subscriber["Protocol"]]
|
|
@@ -1249,7 +1258,7 @@ class PublishDispatcher:
|
|
|
1249
1258
|
self._submit_notification(notifier, ctx, subscriber)
|
|
1250
1259
|
|
|
1251
1260
|
def publish_batch_to_topic(self, ctx: SnsBatchPublishContext, topic_arn: str) -> None:
|
|
1252
|
-
subscriptions = ctx.store
|
|
1261
|
+
subscriptions = get_topic_subscriptions(ctx.store, topic_arn)
|
|
1253
1262
|
for subscriber in subscriptions:
|
|
1254
1263
|
protocol = subscriber["Protocol"]
|
|
1255
1264
|
notifier = self.batch_topic_notifiers.get(protocol)
|
|
@@ -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,
|
|
@@ -181,10 +182,18 @@ class SnsStore(BaseStore):
|
|
|
181
182
|
# maps endpoint arns to platform endpoints
|
|
182
183
|
platform_endpoints: dict[str, PlatformEndpoint] = LocalAttribute(default=dict)
|
|
183
184
|
|
|
185
|
+
# cache of topic ARN to platform endpoint messages (used primarily for testing)
|
|
186
|
+
platform_endpoint_messages: dict[str, list[dict]] = LocalAttribute(default=dict)
|
|
187
|
+
|
|
184
188
|
# topic/subscription independent default values for sending sms messages
|
|
185
189
|
sms_attributes: dict[str, str] = LocalAttribute(default=dict)
|
|
186
190
|
|
|
191
|
+
# list of sent SMS messages
|
|
192
|
+
sms_messages: list[dict] = LocalAttribute(default=list)
|
|
193
|
+
|
|
187
194
|
TAGS: TaggingService = CrossRegionAttribute(default=TaggingService)
|
|
188
195
|
|
|
196
|
+
PHONE_NUMBERS_OPTED_OUT: list[PhoneNumber] = CrossRegionAttribute(default=list)
|
|
197
|
+
|
|
189
198
|
|
|
190
199
|
sns_stores = AccountRegionBundle("sns", SnsStore)
|