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.

Files changed (158) hide show
  1. localstack/aws/api/apigateway/__init__.py +42 -0
  2. localstack/aws/api/cloudformation/__init__.py +161 -0
  3. localstack/aws/api/ec2/__init__.py +1178 -12
  4. localstack/aws/api/iam/__init__.py +228 -0
  5. localstack/aws/api/kms/__init__.py +1 -0
  6. localstack/aws/api/lambda_/__init__.py +1034 -66
  7. localstack/aws/api/logs/__init__.py +500 -0
  8. localstack/aws/api/opensearch/__init__.py +100 -0
  9. localstack/aws/api/redshift/__init__.py +69 -0
  10. localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
  11. localstack/aws/api/route53/__init__.py +45 -0
  12. localstack/aws/api/route53resolver/__init__.py +1 -0
  13. localstack/aws/api/s3/__init__.py +64 -0
  14. localstack/aws/api/s3control/__init__.py +19 -0
  15. localstack/aws/api/secretsmanager/__init__.py +37 -23
  16. localstack/aws/api/stepfunctions/__init__.py +52 -10
  17. localstack/aws/api/sts/__init__.py +52 -0
  18. localstack/aws/connect.py +35 -15
  19. localstack/aws/handlers/logging.py +8 -4
  20. localstack/aws/handlers/service.py +11 -2
  21. localstack/aws/protocol/serializer.py +1 -1
  22. localstack/config.py +8 -0
  23. localstack/constants.py +3 -0
  24. localstack/deprecations.py +0 -6
  25. localstack/dev/kubernetes/__main__.py +39 -14
  26. localstack/runtime/analytics.py +11 -0
  27. localstack/services/acm/provider.py +17 -1
  28. localstack/services/apigateway/legacy/provider.py +28 -15
  29. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  30. localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
  31. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +15 -1
  32. localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
  33. localstack/services/cloudformation/provider.py +26 -1
  34. localstack/services/cloudformation/provider_utils.py +20 -0
  35. localstack/services/cloudformation/resource_provider.py +5 -4
  36. localstack/services/cloudformation/scaffolding/__main__.py +94 -22
  37. localstack/services/cloudformation/v2/provider.py +41 -0
  38. localstack/services/cloudwatch/provider.py +10 -3
  39. localstack/services/cloudwatch/provider_v2.py +6 -3
  40. localstack/services/configservice/provider.py +5 -1
  41. localstack/services/dynamodb/provider.py +1 -0
  42. localstack/services/dynamodb/v2/provider.py +1 -0
  43. localstack/services/dynamodbstreams/provider.py +6 -0
  44. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  45. localstack/services/ec2/provider.py +6 -0
  46. localstack/services/es/provider.py +6 -0
  47. localstack/services/events/provider.py +4 -0
  48. localstack/services/events/v1/provider.py +9 -0
  49. localstack/services/firehose/provider.py +5 -0
  50. localstack/services/iam/provider.py +4 -0
  51. localstack/services/kinesis/packages.py +1 -1
  52. localstack/services/kms/models.py +16 -22
  53. localstack/services/kms/provider.py +4 -0
  54. localstack/services/lambda_/analytics.py +11 -2
  55. localstack/services/lambda_/api_utils.py +37 -20
  56. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  57. localstack/services/lambda_/invocation/assignment.py +4 -1
  58. localstack/services/lambda_/invocation/event_manager.py +15 -11
  59. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  60. localstack/services/lambda_/invocation/lambda_models.py +31 -2
  61. localstack/services/lambda_/invocation/lambda_service.py +62 -3
  62. localstack/services/lambda_/invocation/models.py +9 -1
  63. localstack/services/lambda_/invocation/version_manager.py +18 -3
  64. localstack/services/lambda_/provider.py +307 -106
  65. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  66. localstack/services/lambda_/runtimes.py +3 -1
  67. localstack/services/logs/provider.py +9 -0
  68. localstack/services/opensearch/packages.py +34 -20
  69. localstack/services/opensearch/provider.py +53 -3
  70. localstack/services/resource_groups/provider.py +5 -1
  71. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  72. localstack/services/route53/provider.py +7 -0
  73. localstack/services/route53resolver/provider.py +5 -0
  74. localstack/services/s3/constants.py +5 -0
  75. localstack/services/s3/exceptions.py +9 -0
  76. localstack/services/s3/models.py +9 -1
  77. localstack/services/s3/provider.py +51 -43
  78. localstack/services/s3/utils.py +81 -15
  79. localstack/services/s3control/provider.py +107 -2
  80. localstack/services/s3control/validation.py +50 -0
  81. localstack/services/scheduler/provider.py +4 -2
  82. localstack/services/secretsmanager/provider.py +4 -0
  83. localstack/services/ses/provider.py +4 -0
  84. localstack/services/sns/constants.py +16 -1
  85. localstack/services/sns/provider.py +5 -0
  86. localstack/services/sns/publisher.py +15 -6
  87. localstack/services/sns/v2/models.py +9 -0
  88. localstack/services/sns/v2/provider.py +750 -19
  89. localstack/services/sns/v2/utils.py +12 -0
  90. localstack/services/sqs/constants.py +6 -0
  91. localstack/services/sqs/provider.py +9 -1
  92. localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
  93. localstack/services/ssm/provider.py +6 -0
  94. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  95. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  96. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  101. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  102. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  103. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  107. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  108. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  109. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  110. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  111. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  112. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  113. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  114. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  115. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +256 -22
  116. localstack/services/stepfunctions/backend/execution.py +10 -11
  117. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  118. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  119. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  120. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  121. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  122. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  123. localstack/services/stepfunctions/provider.py +83 -25
  124. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  125. localstack/services/sts/provider.py +7 -0
  126. localstack/services/support/provider.py +5 -1
  127. localstack/services/swf/provider.py +5 -1
  128. localstack/services/transcribe/provider.py +7 -0
  129. localstack/testing/aws/lambda_utils.py +1 -1
  130. localstack/testing/aws/util.py +2 -1
  131. localstack/testing/config.py +1 -0
  132. localstack/testing/pytest/fixtures.py +28 -0
  133. localstack/testing/snapshots/transformer_utility.py +5 -0
  134. localstack/utils/analytics/publisher.py +37 -155
  135. localstack/utils/analytics/service_request_aggregator.py +6 -4
  136. localstack/utils/aws/arns.py +7 -0
  137. localstack/utils/aws/client_types.py +2 -4
  138. localstack/utils/batching.py +258 -0
  139. localstack/utils/bootstrap.py +2 -2
  140. localstack/utils/catalog/catalog.py +3 -2
  141. localstack/utils/collections.py +23 -11
  142. localstack/utils/container_utils/container_client.py +22 -13
  143. localstack/utils/container_utils/docker_cmd_client.py +6 -6
  144. localstack/version.py +2 -2
  145. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +7 -7
  146. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +155 -146
  147. localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
  148. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  149. localstack/utils/batch_policy.py +0 -124
  150. localstack_core-4.10.1.dev42.dist-info/plux.json +0 -1
  151. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  152. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
  153. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
  154. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
  155. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
  156. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
  157. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
  158. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
@@ -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 InvalidRequest, MalformedXML
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 < if_modified_since < datetime.datetime.now(
441
- tz=_gmt_zone_info
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(tag_set: TagSet, type_set: Literal["bucket", "object"] = "bucket"):
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
- else:
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=key,
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(tag["Value"]):
891
+ elif not TAG_REGEX.match(value):
868
892
  raise InvalidTag(
869
- "The TagValue you have provided is invalid", TagKey=key, TagValue=tag["Value"]
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 < cs_if_modified_since < datetime.datetime.now(tz=_gmt_zone_info):
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.replace(microsecond=0)
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 <= if_modified_since < datetime.datetime.now(tz=_gmt_zone_info)
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.s3control import S3ControlApi
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
- pass
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
- pass
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", "1") if topic_attributes else "1"
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
- if json_topic_delivery_policy := topic_attributes.get("delivery_policy"):
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
- topic_attributes.get("signature_version", "1") if topic_attributes else "1"
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.get_topic_subscriptions(topic_arn)
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.get_topic_subscriptions(topic_arn)
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)