localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__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.
Files changed (173) hide show
  1. localstack/aws/api/cloudformation/__init__.py +1 -0
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +4 -0
  5. localstack/aws/api/ec2/__init__.py +1113 -56
  6. localstack/aws/api/iam/__init__.py +7 -0
  7. localstack/aws/api/kinesis/__init__.py +19 -0
  8. localstack/aws/api/kms/__init__.py +6 -0
  9. localstack/aws/api/lambda_/__init__.py +13 -0
  10. localstack/aws/api/logs/__init__.py +15 -0
  11. localstack/aws/api/redshift/__init__.py +9 -3
  12. localstack/aws/api/route53/__init__.py +2 -0
  13. localstack/aws/api/s3/__init__.py +12 -0
  14. localstack/aws/api/s3control/__init__.py +32 -0
  15. localstack/aws/api/ssm/__init__.py +2 -0
  16. localstack/aws/client.py +7 -2
  17. localstack/aws/forwarder.py +52 -5
  18. localstack/aws/handlers/analytics.py +1 -1
  19. localstack/aws/handlers/logging.py +12 -2
  20. localstack/aws/handlers/metric_handler.py +41 -1
  21. localstack/aws/handlers/service.py +32 -9
  22. localstack/aws/protocol/parser.py +440 -21
  23. localstack/aws/protocol/serializer.py +684 -64
  24. localstack/aws/protocol/service_router.py +120 -20
  25. localstack/aws/skeleton.py +4 -2
  26. localstack/aws/spec-patches.json +58 -0
  27. localstack/aws/spec.py +33 -13
  28. localstack/cli/exceptions.py +1 -1
  29. localstack/cli/localstack.py +4 -4
  30. localstack/cli/lpm.py +3 -4
  31. localstack/cli/profiles.py +1 -2
  32. localstack/config.py +18 -12
  33. localstack/constants.py +4 -29
  34. localstack/dev/kubernetes/__main__.py +1 -1
  35. localstack/dev/run/paths.py +1 -1
  36. localstack/dns/plugins.py +5 -1
  37. localstack/dns/server.py +12 -3
  38. localstack/packages/api.py +9 -8
  39. localstack/packages/core.py +2 -2
  40. localstack/packages/plugins.py +0 -8
  41. localstack/runtime/init.py +1 -1
  42. localstack/services/apigateway/legacy/provider.py +53 -3
  43. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  44. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  45. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  46. localstack/services/apigateway/next_gen/provider.py +5 -0
  47. localstack/services/cloudformation/engine/entities.py +12 -1
  48. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  49. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  50. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  52. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  53. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  54. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  55. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  56. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  57. localstack/services/cloudformation/resource_provider.py +5 -1
  58. localstack/services/cloudformation/resources.py +24149 -0
  59. localstack/services/cloudformation/v2/entities.py +6 -3
  60. localstack/services/cloudformation/v2/provider.py +172 -27
  61. localstack/services/cloudformation/v2/types.py +8 -4
  62. localstack/services/cloudwatch/provider_v2.py +25 -28
  63. localstack/services/dynamodb/packages.py +2 -1
  64. localstack/services/dynamodb/provider.py +42 -0
  65. localstack/services/dynamodb/v2/provider.py +42 -0
  66. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  67. localstack/services/es/provider.py +2 -2
  68. localstack/services/events/event_rule_engine.py +31 -13
  69. localstack/services/events/models.py +4 -5
  70. localstack/services/events/target.py +17 -9
  71. localstack/services/iam/provider.py +11 -116
  72. localstack/services/iam/resources/policy_simulator.py +133 -0
  73. localstack/services/kinesis/models.py +15 -2
  74. localstack/services/kinesis/provider.py +77 -0
  75. localstack/services/kms/provider.py +14 -5
  76. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  77. localstack/services/lambda_/packages.py +1 -1
  78. localstack/services/logs/provider.py +1 -1
  79. localstack/services/moto.py +2 -1
  80. localstack/services/opensearch/cluster.py +15 -7
  81. localstack/services/opensearch/packages.py +26 -7
  82. localstack/services/opensearch/provider.py +6 -1
  83. localstack/services/opensearch/versions.py +56 -7
  84. localstack/services/s3/constants.py +5 -2
  85. localstack/services/s3/cors.py +4 -4
  86. localstack/services/s3/notifications.py +1 -1
  87. localstack/services/s3/presigned_url.py +27 -43
  88. localstack/services/s3/provider.py +67 -11
  89. localstack/services/s3/utils.py +42 -11
  90. localstack/services/ses/provider.py +16 -7
  91. localstack/services/sns/constants.py +7 -1
  92. localstack/services/sns/v2/models.py +167 -0
  93. localstack/services/sns/v2/provider.py +860 -2
  94. localstack/services/sns/v2/utils.py +130 -0
  95. localstack/services/sqs/developer_api.py +205 -0
  96. localstack/services/sqs/models.py +42 -3
  97. localstack/services/sqs/provider.py +8 -309
  98. localstack/services/sqs/query_api.py +1 -1
  99. localstack/services/sqs/utils.py +121 -2
  100. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  101. localstack/testing/aws/cloudformation_utils.py +1 -1
  102. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  103. localstack/testing/pytest/container.py +4 -5
  104. localstack/testing/pytest/fixtures.py +20 -19
  105. localstack/testing/pytest/in_memory_localstack.py +0 -4
  106. localstack/testing/pytest/marking.py +13 -4
  107. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  108. localstack/testing/pytest/util.py +1 -1
  109. localstack/testing/pytest/validation_tracking.py +1 -2
  110. localstack/testing/snapshots/transformer_utility.py +5 -0
  111. localstack/utils/analytics/events.py +2 -2
  112. localstack/utils/analytics/metadata.py +1 -2
  113. localstack/utils/analytics/metrics/counter.py +6 -8
  114. localstack/utils/analytics/publisher.py +1 -2
  115. localstack/utils/analytics/service_request_aggregator.py +2 -2
  116. localstack/utils/archives.py +11 -11
  117. localstack/utils/aws/arns.py +17 -9
  118. localstack/utils/aws/aws_responses.py +7 -7
  119. localstack/utils/aws/aws_stack.py +2 -3
  120. localstack/utils/aws/message_forwarding.py +1 -2
  121. localstack/utils/aws/request_context.py +4 -5
  122. localstack/utils/batch_policy.py +3 -3
  123. localstack/utils/bootstrap.py +7 -7
  124. localstack/utils/catalog/catalog.py +139 -0
  125. localstack/utils/catalog/catalog_loader.py +11 -0
  126. localstack/utils/catalog/common.py +58 -0
  127. localstack/utils/catalog/plugins.py +28 -0
  128. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  129. localstack/utils/collections.py +7 -8
  130. localstack/utils/config_listener.py +1 -1
  131. localstack/utils/container_networking.py +2 -3
  132. localstack/utils/container_utils/container_client.py +115 -131
  133. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  134. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  135. localstack/utils/diagnose.py +2 -3
  136. localstack/utils/docker_utils.py +3 -4
  137. localstack/utils/files.py +31 -7
  138. localstack/utils/functions.py +3 -2
  139. localstack/utils/http.py +4 -5
  140. localstack/utils/json.py +19 -5
  141. localstack/utils/kinesis/kinesis_connector.py +2 -1
  142. localstack/utils/net.py +6 -6
  143. localstack/utils/no_exit_argument_parser.py +2 -2
  144. localstack/utils/numbers.py +9 -2
  145. localstack/utils/objects.py +6 -5
  146. localstack/utils/patch.py +2 -1
  147. localstack/utils/run.py +10 -9
  148. localstack/utils/scheduler.py +11 -11
  149. localstack/utils/server/tcp_proxy.py +2 -2
  150. localstack/utils/serving.py +2 -3
  151. localstack/utils/strings.py +10 -11
  152. localstack/utils/sync.py +126 -1
  153. localstack/utils/tagging.py +1 -4
  154. localstack/utils/testutil.py +5 -4
  155. localstack/utils/threads.py +2 -2
  156. localstack/utils/time.py +11 -3
  157. localstack/utils/urls.py +1 -3
  158. localstack/version.py +2 -2
  159. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
  160. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
  161. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
  162. localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
  163. localstack/packages/terraform.py +0 -46
  164. localstack/services/cloudformation/deploy.html +0 -144
  165. localstack/services/cloudformation/deploy_ui.py +0 -47
  166. localstack/services/cloudformation/plugins.py +0 -12
  167. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  168. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
  169. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
  170. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
  171. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
  172. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
  173. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
@@ -40,14 +40,13 @@ from localstack.http.request import get_raw_path
40
40
  from localstack.services.s3.constants import (
41
41
  DEFAULT_PRE_SIGNED_ACCESS_KEY_ID,
42
42
  DEFAULT_PRE_SIGNED_SECRET_ACCESS_KEY,
43
+ S3_HOST_ID,
43
44
  SIGNATURE_V2_PARAMS,
44
45
  SIGNATURE_V4_PARAMS,
45
46
  )
46
47
  from localstack.services.s3.utils import (
47
- S3_VIRTUAL_HOST_FORWARDED_HEADER,
48
48
  capitalize_header_name_from_snake_case,
49
49
  extract_bucket_name_and_key_from_headers_and_path,
50
- forwarded_from_virtual_host_addressed_request,
51
50
  is_bucket_name_valid,
52
51
  is_presigned_url_request,
53
52
  uses_host_addressing,
@@ -85,8 +84,6 @@ IGNORED_SIGV4_HEADERS = [
85
84
  "x-amz-content-sha256",
86
85
  ]
87
86
 
88
- FAKE_HOST_ID = "9Gjjt1m+cjU4OPvX9O9/8RuvnG41MRb/18Oux2o5H5MY7ISNTlXN+Dz9IG62/ILVxhAGI0qyPfg="
89
-
90
87
  HOST_COMBINATION_REGEX = r"^(.*)(:[\d]{0,6})"
91
88
  PORT_REPLACEMENT = [":80", ":443", f":{config.GATEWAY_LISTEN[0].port}", ""]
92
89
 
@@ -156,7 +153,7 @@ def create_signature_does_not_match_sig_v2(
156
153
  "The request signature we calculated does not match the signature you provided. Check your key and signing method."
157
154
  )
158
155
  ex.AWSAccessKeyId = access_key_id
159
- ex.HostId = FAKE_HOST_ID
156
+ ex.HostId = S3_HOST_ID
160
157
  ex.SignatureProvided = request_signature
161
158
  ex.StringToSign = string_to_sign
162
159
  ex.StringToSignBytes = to_bytes(string_to_sign).hex(sep=" ", bytes_per_sep=2).upper()
@@ -299,7 +296,7 @@ def is_valid_sig_v2(query_args: set) -> bool:
299
296
  LOG.info("Presign signature calculation failed")
300
297
  raise AccessDenied(
301
298
  "Query-string authentication requires the Signature, Expires and AWSAccessKeyId parameters",
302
- HostId=FAKE_HOST_ID,
299
+ HostId=S3_HOST_ID,
303
300
  )
304
301
 
305
302
  return True
@@ -317,7 +314,7 @@ def is_valid_sig_v4(query_args: set) -> bool:
317
314
  LOG.info("Presign signature calculation failed")
318
315
  raise AuthorizationQueryParametersError(
319
316
  "Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.",
320
- HostId=FAKE_HOST_ID,
317
+ HostId=S3_HOST_ID,
321
318
  )
322
319
 
323
320
  return True
@@ -351,7 +348,7 @@ def validate_presigned_url_s3(context: RequestContext) -> None:
351
348
  )
352
349
  else:
353
350
  raise AccessDenied(
354
- "Request has expired", HostId=FAKE_HOST_ID, Expires=expires, ServerTime=time.time()
351
+ "Request has expired", HostId=S3_HOST_ID, Expires=expires, ServerTime=time.time()
355
352
  )
356
353
 
357
354
  auth_signer = HmacV1QueryAuthValidation(credentials=signing_credentials, expires=expires)
@@ -450,7 +447,7 @@ def validate_presigned_url_s3v4(context: RequestContext) -> None:
450
447
  else:
451
448
  raise AccessDenied(
452
449
  "There were headers present in the request which were not signed",
453
- HostId=FAKE_HOST_ID,
450
+ HostId=S3_HOST_ID,
454
451
  HeadersNotSigned=", ".join(sigv4_context.missing_signed_headers),
455
452
  )
456
453
 
@@ -482,7 +479,7 @@ def validate_presigned_url_s3v4(context: RequestContext) -> None:
482
479
  else:
483
480
  raise AccessDenied(
484
481
  "Request has expired",
485
- HostId=FAKE_HOST_ID,
482
+ HostId=S3_HOST_ID,
486
483
  Expires=expiration_time.timestamp(),
487
484
  ServerTime=time.time(),
488
485
  X_Amz_Expires=x_amz_expires,
@@ -568,34 +565,21 @@ class S3SigV4SignatureContext:
568
565
  self._query_parameters
569
566
  )
570
567
 
571
- if forwarded_from_virtual_host_addressed_request(self._headers):
572
- # FIXME: maybe move this so it happens earlier in the chain when using virtual host?
573
- if not is_bucket_name_valid(self._bucket):
574
- raise InvalidBucketName(BucketName=self._bucket)
575
- netloc = self._headers.get(S3_VIRTUAL_HOST_FORWARDED_HEADER)
576
- self.host = netloc
577
- self._original_host = netloc
578
- self.signed_headers["host"] = netloc
579
- # the request comes from the Virtual Host router, we need to remove the bucket from the path
568
+ netloc = urlparse.urlparse(self.request.url).netloc
569
+ self.host = netloc
570
+ self._original_host = netloc
571
+ if (host_addressed := uses_host_addressing(self._headers)) and not is_bucket_name_valid(
572
+ self._bucket
573
+ ):
574
+ raise InvalidBucketName(BucketName=self._bucket)
575
+
576
+ if not host_addressed and not self.request.path.startswith(f"/{self._bucket}"):
577
+ # if in path style, check that the path starts with the bucket
578
+ # our path has been sanitized, we should use the un-sanitized one
580
579
  splitted_path = self.request.path.split("/", maxsplit=2)
581
- self.path = f"/{splitted_path[-1]}"
582
-
580
+ self.path = f"/{self._bucket}/{splitted_path[-1]}"
583
581
  else:
584
- netloc = urlparse.urlparse(self.request.url).netloc
585
- self.host = netloc
586
- self._original_host = netloc
587
- if (host_addressed := uses_host_addressing(self._headers)) and not is_bucket_name_valid(
588
- self._bucket
589
- ):
590
- raise InvalidBucketName(BucketName=self._bucket)
591
-
592
- if not host_addressed and not self.request.path.startswith(f"/{self._bucket}"):
593
- # if in path style, check that the path starts with the bucket
594
- # our path has been sanitized, we should use the un-sanitized one
595
- splitted_path = self.request.path.split("/", maxsplit=2)
596
- self.path = f"/{self._bucket}/{splitted_path[-1]}"
597
- else:
598
- self.path = self.request.path
582
+ self.path = self.request.path
599
583
 
600
584
  # we need to URL encode the path, as the key needs to be urlencoded for the signature to match
601
585
  self.path = urlparse.quote(self.path)
@@ -714,7 +698,7 @@ class S3SigV4SignatureContext:
714
698
  if not (split_creds := credential.split("/")) or len(split_creds) != 5:
715
699
  raise AuthorizationQueryParametersError(
716
700
  'Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting "<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request".',
717
- HostId=FAKE_HOST_ID,
701
+ HostId=S3_HOST_ID,
718
702
  )
719
703
 
720
704
  return split_creds[2]
@@ -775,7 +759,7 @@ def validate_post_policy(
775
759
  "Bucket POST must contain a field named 'key'. If it is specified, please check the order of the fields.",
776
760
  ArgumentName="key",
777
761
  ArgumentValue="",
778
- HostId=FAKE_HOST_ID,
762
+ HostId=S3_HOST_ID,
779
763
  )
780
764
 
781
765
  form_dict = {k.lower(): v for k, v in request_form.items()}
@@ -791,7 +775,7 @@ def validate_post_policy(
791
775
 
792
776
  if not is_v2 and not is_v4:
793
777
  ex: AccessDenied = AccessDenied("Access Denied")
794
- ex.HostId = FAKE_HOST_ID
778
+ ex.HostId = S3_HOST_ID
795
779
  raise ex
796
780
 
797
781
  try:
@@ -810,7 +794,7 @@ def validate_post_policy(
810
794
  if expiration := policy_decoded.get("expiration"):
811
795
  if is_expired(_parse_policy_expiration_date(expiration)):
812
796
  ex: AccessDenied = AccessDenied("Invalid according to Policy: Policy expired.")
813
- ex.HostId = FAKE_HOST_ID
797
+ ex.HostId = S3_HOST_ID
814
798
  raise ex
815
799
 
816
800
  # TODO: validate the signature
@@ -832,7 +816,7 @@ def validate_post_policy(
832
816
  str_condition = str(condition).replace("'", '"')
833
817
  raise AccessDenied(
834
818
  f"Invalid according to Policy: Policy Condition failed: {str_condition}",
835
- HostId=FAKE_HOST_ID,
819
+ HostId=S3_HOST_ID,
836
820
  )
837
821
 
838
822
 
@@ -885,7 +869,7 @@ def _verify_condition(condition: list | dict, form: dict, additional_policy_meta
885
869
  "Your proposed upload exceeds the maximum allowed size",
886
870
  ProposedSize=size,
887
871
  MaxSizeAllowed=end,
888
- HostId=FAKE_HOST_ID,
872
+ HostId=S3_HOST_ID,
889
873
  )
890
874
  else:
891
875
  return True
@@ -934,7 +918,7 @@ def _is_match_with_signature_fields(
934
918
  f"Bucket POST must contain a field named '{argument_name}'. If it is specified, please check the order of the fields.",
935
919
  ArgumentName=argument_name,
936
920
  ArgumentValue="",
937
- HostId=FAKE_HOST_ID,
921
+ HostId=S3_HOST_ID,
938
922
  )
939
923
 
940
924
  return True
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import contextlib
2
3
  import copy
3
4
  import datetime
4
5
  import json
@@ -8,6 +9,7 @@ from collections import defaultdict
8
9
  from inspect import signature
9
10
  from io import BytesIO
10
11
  from operator import itemgetter
12
+ from threading import RLock
11
13
  from typing import IO
12
14
  from urllib import parse as urlparse
13
15
  from zoneinfo import ZoneInfo
@@ -23,6 +25,7 @@ from localstack.aws.api.s3 import (
23
25
  AccountId,
24
26
  AnalyticsConfiguration,
25
27
  AnalyticsId,
28
+ AuthorizationHeaderMalformed,
26
29
  BadDigest,
27
30
  Body,
28
31
  Bucket,
@@ -235,6 +238,7 @@ from localstack.services.s3.constants import (
235
238
  ARCHIVES_STORAGE_CLASSES,
236
239
  CHECKSUM_ALGORITHMS,
237
240
  DEFAULT_BUCKET_ENCRYPTION,
241
+ S3_HOST_ID,
238
242
  )
239
243
  from localstack.services.s3.cors import S3CorsHandler, s3_cors_request_handler
240
244
  from localstack.services.s3.exceptions import (
@@ -277,6 +281,7 @@ from localstack.services.s3.utils import (
277
281
  get_canned_acl,
278
282
  get_class_attrs_from_spec_class,
279
283
  get_failed_precondition_copy_source,
284
+ get_failed_upload_part_copy_source_preconditions,
280
285
  get_full_default_bucket_location,
281
286
  get_kms_key_arn,
282
287
  get_lifecycle_rule_from_object,
@@ -337,6 +342,8 @@ class S3Provider(S3Api, ServiceLifecycleHook):
337
342
  self._storage_backend = storage_backend or EphemeralS3ObjectStore(DEFAULT_S3_TMP_DIR)
338
343
  self._notification_dispatcher = NotificationDispatcher()
339
344
  self._cors_handler = S3CorsHandler(BucketCorsIndex())
345
+ # TODO: add lock for keys for PutObject, only way to support precondition writes for versioned buckets
346
+ self._preconditions_locks = defaultdict(lambda: defaultdict(RLock))
340
347
 
341
348
  # runtime cache of Lifecycle Expiration headers, as they need to be calculated everytime we fetch an object
342
349
  # in case the rules have changed
@@ -381,7 +388,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
381
388
  """
382
389
  if s3_bucket.notification_configuration:
383
390
  if not s3_notif_ctx:
384
- s3_notif_ctx = S3EventNotificationContext.from_request_context_native(
391
+ s3_notif_ctx = S3EventNotificationContext.from_request_context(
385
392
  context,
386
393
  s3_bucket=s3_bucket,
387
394
  s3_object=s3_object,
@@ -469,6 +476,16 @@ class S3Provider(S3Api, ServiceLifecycleHook):
469
476
  context: RequestContext,
470
477
  request: CreateBucketRequest,
471
478
  ) -> CreateBucketOutput:
479
+ if context.region == "aws-global":
480
+ # TODO: extend this logic to probably all the provider, and maybe all services. S3 is the most impacted
481
+ # right now so this will help users to properly set a region in their config
482
+ # See the `TestS3.test_create_bucket_aws_global` test
483
+ raise AuthorizationHeaderMalformed(
484
+ f"The authorization header is malformed; the region 'aws-global' is wrong; expecting '{AWS_REGION_US_EAST_1}'",
485
+ HostId=S3_HOST_ID,
486
+ Region=AWS_REGION_US_EAST_1,
487
+ )
488
+
472
489
  bucket_name = request["Bucket"]
473
490
 
474
491
  if not is_bucket_name_valid(bucket_name):
@@ -577,6 +594,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
577
594
  store.global_bucket_map.pop(bucket)
578
595
  self._cors_handler.invalidate_cache()
579
596
  self._expiration_cache.pop(bucket, None)
597
+ self._preconditions_locks.pop(bucket, None)
580
598
  # clean up the storage backend
581
599
  self._storage_backend.delete_bucket(bucket)
582
600
 
@@ -636,6 +654,16 @@ class S3Provider(S3Api, ServiceLifecycleHook):
636
654
  expected_bucket_owner: AccountId = None,
637
655
  **kwargs,
638
656
  ) -> HeadBucketOutput:
657
+ if context.region == "aws-global":
658
+ # TODO: extend this logic to probably all the provider, and maybe all services. S3 is the most impacted
659
+ # right now so this will help users to properly set a region in their config
660
+ # See the `TestS3.test_create_bucket_aws_global` test
661
+ raise AuthorizationHeaderMalformed(
662
+ f"The authorization header is malformed; the region 'aws-global' is wrong; expecting '{AWS_REGION_US_EAST_1}'",
663
+ HostId=S3_HOST_ID,
664
+ Region=AWS_REGION_US_EAST_1,
665
+ )
666
+
639
667
  store = self.get_store(context.account_id, context.region)
640
668
  if not (s3_bucket := store.buckets.get(bucket)):
641
669
  if not (account_id := store.global_bucket_map.get(bucket)):
@@ -727,6 +755,12 @@ class S3Provider(S3Api, ServiceLifecycleHook):
727
755
  system_metadata["ContentType"] = "binary/octet-stream"
728
756
 
729
757
  version_id = generate_version_id(s3_bucket.versioning_status)
758
+ if version_id != "null":
759
+ # if we are in a versioned bucket, we need to lock around the full key (all the versions)
760
+ # because object versions have locks per version
761
+ precondition_lock = self._preconditions_locks[bucket_name][key]
762
+ else:
763
+ precondition_lock = contextlib.nullcontext()
730
764
 
731
765
  etag_content_md5 = ""
732
766
  if content_md5 := request.get("ContentMD5"):
@@ -809,7 +843,10 @@ class S3Provider(S3Api, ServiceLifecycleHook):
809
843
  if encodings:
810
844
  s3_object.system_metadata["ContentEncoding"] = ",".join(encodings)
811
845
 
812
- with self._storage_backend.open(bucket_name, s3_object, mode="w") as s3_stored_object:
846
+ with (
847
+ precondition_lock,
848
+ self._storage_backend.open(bucket_name, s3_object, mode="w") as s3_stored_object,
849
+ ):
813
850
  # as we are inside the lock here, if multiple concurrent requests happen for the same object, it's the first
814
851
  # one to finish to succeed, and subsequent will raise exceptions. Once the first write finishes, we're
815
852
  # opening the lock and other requests can check this condition
@@ -1248,7 +1285,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1248
1285
  delete_marker_id = generate_version_id(s3_bucket.versioning_status)
1249
1286
  delete_marker = S3DeleteMarker(key=key, version_id=delete_marker_id)
1250
1287
  s3_bucket.objects.set(key, delete_marker)
1251
- s3_notif_ctx = S3EventNotificationContext.from_request_context_native(
1288
+ s3_notif_ctx = S3EventNotificationContext.from_request_context(
1252
1289
  context,
1253
1290
  s3_bucket=s3_bucket,
1254
1291
  s3_object=delete_marker,
@@ -1281,6 +1318,10 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1281
1318
  store.TAGS.tags.pop(get_unique_key_id(bucket, key, version_id), None)
1282
1319
  self._notify(context, s3_bucket=s3_bucket, s3_object=s3_object)
1283
1320
 
1321
+ if key not in s3_bucket.objects:
1322
+ # we clean up keys that do not have any object versions in them anymore
1323
+ self._preconditions_locks[bucket].pop(key, None)
1324
+
1284
1325
  return response
1285
1326
 
1286
1327
  def delete_objects(
@@ -1314,6 +1355,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1314
1355
  errors = []
1315
1356
 
1316
1357
  to_remove = []
1358
+ versioned_keys = set()
1317
1359
  for to_delete_object in objects:
1318
1360
  object_key = to_delete_object.get("Key")
1319
1361
  version_id = to_delete_object.get("VersionId")
@@ -1351,7 +1393,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1351
1393
  delete_marker_id = generate_version_id(s3_bucket.versioning_status)
1352
1394
  delete_marker = S3DeleteMarker(key=object_key, version_id=delete_marker_id)
1353
1395
  s3_bucket.objects.set(object_key, delete_marker)
1354
- s3_notif_ctx = S3EventNotificationContext.from_request_context_native(
1396
+ s3_notif_ctx = S3EventNotificationContext.from_request_context(
1355
1397
  context,
1356
1398
  s3_bucket=s3_bucket,
1357
1399
  s3_object=delete_marker,
@@ -1394,6 +1436,8 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1394
1436
  continue
1395
1437
 
1396
1438
  s3_bucket.objects.pop(object_key=object_key, version_id=version_id)
1439
+ versioned_keys.add(object_key)
1440
+
1397
1441
  if not quiet:
1398
1442
  deleted_object = DeletedObject(
1399
1443
  Key=object_key,
@@ -1411,6 +1455,11 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1411
1455
  self._notify(context, s3_bucket=s3_bucket, s3_object=found_object)
1412
1456
  store.TAGS.tags.pop(get_unique_key_id(bucket, object_key, version_id), None)
1413
1457
 
1458
+ for versioned_key in versioned_keys:
1459
+ # we clean up keys that do not have any object versions in them anymore
1460
+ if versioned_key not in s3_bucket.objects:
1461
+ self._preconditions_locks[bucket].pop(versioned_key, None)
1462
+
1414
1463
  # TODO: request charged
1415
1464
  self._storage_backend.remove(bucket, to_remove)
1416
1465
  response: DeleteObjectsOutput = {}
@@ -2179,7 +2228,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
2179
2228
  # TODO: add a way to transition from ongoing-request=true to false? for now it is instant
2180
2229
  s3_object.restore = f'ongoing-request="false", expiry-date="{restore_expiration_date}"'
2181
2230
 
2182
- s3_notif_ctx_initiated = S3EventNotificationContext.from_request_context_native(
2231
+ s3_notif_ctx_initiated = S3EventNotificationContext.from_request_context(
2183
2232
  context,
2184
2233
  s3_bucket=s3_bucket,
2185
2234
  s3_object=s3_object,
@@ -2483,10 +2532,6 @@ class S3Provider(S3Api, ServiceLifecycleHook):
2483
2532
  request: UploadPartCopyRequest,
2484
2533
  ) -> UploadPartCopyOutput:
2485
2534
  # TODO: handle following parameters:
2486
- # CopySourceIfMatch: Optional[CopySourceIfMatch]
2487
- # CopySourceIfModifiedSince: Optional[CopySourceIfModifiedSince]
2488
- # CopySourceIfNoneMatch: Optional[CopySourceIfNoneMatch]
2489
- # CopySourceIfUnmodifiedSince: Optional[CopySourceIfUnmodifiedSince]
2490
2535
  # SSECustomerAlgorithm: Optional[SSECustomerAlgorithm]
2491
2536
  # SSECustomerKey: Optional[SSECustomerKey]
2492
2537
  # SSECustomerKeyMD5: Optional[SSECustomerKeyMD5]
@@ -2549,6 +2594,14 @@ class S3Provider(S3Api, ServiceLifecycleHook):
2549
2594
  if source_range:
2550
2595
  range_data = parse_copy_source_range_header(source_range, src_s3_object.size)
2551
2596
 
2597
+ if precondition := get_failed_upload_part_copy_source_preconditions(
2598
+ request, src_s3_object.last_modified, src_s3_object.etag
2599
+ ):
2600
+ raise PreconditionFailed(
2601
+ "At least one of the pre-conditions you specified did not hold",
2602
+ Condition=precondition,
2603
+ )
2604
+
2552
2605
  s3_part = S3Part(part_number=part_number)
2553
2606
  if s3_multipart.checksum_algorithm:
2554
2607
  s3_part.checksum_algorithm = s3_multipart.checksum_algorithm
@@ -2622,6 +2675,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
2622
2675
  )
2623
2676
 
2624
2677
  elif if_none_match:
2678
+ # TODO: improve concurrency mechanism for `if_none_match` and `if_match`
2625
2679
  if if_none_match != "*":
2626
2680
  raise NotImplementedException(
2627
2681
  "A header you provided implies functionality that is not implemented",
@@ -3293,7 +3347,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
3293
3347
 
3294
3348
  s3_object = s3_bucket.get_object(key=key, version_id=version_id, http_method="DELETE")
3295
3349
 
3296
- store.TAGS.tags.pop(get_unique_key_id(bucket, key, version_id), None)
3350
+ store.TAGS.tags.pop(get_unique_key_id(bucket, key, s3_object.version_id), None)
3297
3351
  response = DeleteObjectTaggingOutput()
3298
3352
  if s3_object.version_id:
3299
3353
  response["VersionId"] = s3_object.version_id
@@ -3854,7 +3908,9 @@ class S3Provider(S3Api, ServiceLifecycleHook):
3854
3908
  if retention and retention["RetainUntilDate"] < datetime.datetime.now(datetime.UTC):
3855
3909
  # weirdly, this date is format as following: Tue Dec 31 16:00:00 PST 2019
3856
3910
  # it contains the timezone as PST, even if you target a bucket in Europe or Asia
3857
- pst_datetime = retention["RetainUntilDate"].astimezone(tz=ZoneInfo("US/Pacific"))
3911
+ pst_datetime = retention["RetainUntilDate"].astimezone(
3912
+ tz=ZoneInfo("America/Los_Angeles")
3913
+ )
3858
3914
  raise InvalidArgument(
3859
3915
  "The retain until date must be in the future!",
3860
3916
  ArgumentName="RetainUntilDate",
@@ -7,6 +7,7 @@ import logging
7
7
  import re
8
8
  import time
9
9
  import zlib
10
+ from collections.abc import Mapping
10
11
  from enum import StrEnum
11
12
  from secrets import token_bytes
12
13
  from typing import Any, Literal, NamedTuple, Protocol
@@ -50,6 +51,7 @@ from localstack.aws.api.s3 import (
50
51
  SSEKMSKeyId,
51
52
  TaggingHeader,
52
53
  TagSet,
54
+ UploadPartCopyRequest,
53
55
  UploadPartRequest,
54
56
  )
55
57
  from localstack.aws.api.s3 import Type as GranteeType
@@ -62,7 +64,6 @@ from localstack.services.s3.constants import (
62
64
  AUTHENTICATED_USERS_ACL_GRANTEE,
63
65
  CHECKSUM_ALGORITHMS,
64
66
  LOG_DELIVERY_ACL_GRANTEE,
65
- S3_VIRTUAL_HOST_FORWARDED_HEADER,
66
67
  SIGNATURE_V2_PARAMS,
67
68
  SIGNATURE_V4_PARAMS,
68
69
  SYSTEM_METADATA_SETTABLE_HEADERS,
@@ -403,6 +404,45 @@ def parse_copy_source_range_header(copy_source_range: str, object_size: int) ->
403
404
  )
404
405
 
405
406
 
407
+ def get_failed_upload_part_copy_source_preconditions(
408
+ request: UploadPartCopyRequest, last_modified: datetime.datetime, etag: ETag
409
+ ) -> str | None:
410
+ """
411
+ Utility which parses the conditions from a S3 UploadPartCopy request.
412
+ Note: The order in which these conditions are checked if used in conjunction matters
413
+
414
+ :param UploadPartCopyRequest request: The S3 UploadPartCopy request.
415
+ :param datetime last_modified: The time the source object was last modified.
416
+ :param ETag etag: The ETag of the source object.
417
+
418
+ :returns: The name of the failed precondition.
419
+ """
420
+ if_match = request.get("CopySourceIfMatch")
421
+ if_none_match = request.get("CopySourceIfNoneMatch")
422
+ if_unmodified_since = request.get("CopySourceIfUnmodifiedSince")
423
+ if_modified_since = request.get("CopySourceIfModifiedSince")
424
+
425
+ if if_match:
426
+ if if_match.strip('"') != etag.strip('"'):
427
+ return "x-amz-copy-source-If-Match"
428
+ if if_modified_since and if_modified_since > last_modified:
429
+ return "x-amz-copy-source-If-Modified-Since"
430
+ # CopySourceIfMatch is unaffected by CopySourceIfUnmodifiedSince so return early
431
+ if if_unmodified_since:
432
+ return None
433
+
434
+ if if_unmodified_since and if_unmodified_since < last_modified:
435
+ return "x-amz-copy-source-If-Unmodified-Since"
436
+
437
+ if if_none_match and if_none_match.strip('"') == etag.strip('"'):
438
+ return "x-amz-copy-source-If-None-Match"
439
+
440
+ if if_modified_since and last_modified < if_modified_since < datetime.datetime.now(
441
+ tz=_gmt_zone_info
442
+ ):
443
+ return "x-amz-copy-source-If-Modified-Since"
444
+
445
+
406
446
  def get_full_default_bucket_location(bucket_name: BucketName) -> str:
407
447
  host_definition = localstack_host()
408
448
  if host_definition.host != constants.LOCALHOST_HOSTNAME:
@@ -482,7 +522,7 @@ def is_valid_canonical_id(canonical_id: str) -> bool:
482
522
  return False
483
523
 
484
524
 
485
- def uses_host_addressing(headers: dict[str, str]) -> str | None:
525
+ def uses_host_addressing(headers: Mapping[str, str]) -> str | None:
486
526
  """
487
527
  Determines if the request is targeting S3 with virtual host addressing
488
528
  :param headers: the request headers
@@ -511,15 +551,6 @@ def get_system_metadata_from_request(request: dict) -> Metadata:
511
551
  return metadata
512
552
 
513
553
 
514
- def forwarded_from_virtual_host_addressed_request(headers: dict[str, str]) -> bool:
515
- """
516
- Determines if the request was forwarded from a v-host addressing style into a path one
517
- """
518
- # we can assume that the host header we are receiving here is actually the header we originally received
519
- # from the client (because the edge service is forwarding the request in memory)
520
- return S3_VIRTUAL_HOST_FORWARDED_HEADER in headers
521
-
522
-
523
554
  def extract_bucket_name_and_key_from_headers_and_path(
524
555
  headers: dict[str, str], path: str
525
556
  ) -> tuple[str | None, str | None]:
@@ -8,7 +8,6 @@ from datetime import UTC, date, datetime, time
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from botocore.exceptions import ClientError
11
- from moto.core.parsers import XFormedDict
12
11
  from moto.ses import ses_backends
13
12
  from moto.ses.models import SESBackend
14
13
 
@@ -183,6 +182,8 @@ class SesProvider(SesApi, ServiceLifecycleHook):
183
182
  #
184
183
 
185
184
  def on_after_init(self):
185
+ self._apply_patches()
186
+
186
187
  # Allow sent emails to be retrieved from the SES emails endpoint
187
188
  register_ses_api_resource()
188
189
 
@@ -198,6 +199,12 @@ class SesProvider(SesApi, ServiceLifecycleHook):
198
199
  return entity.replace("From:", "").strip()
199
200
  return None
200
201
 
202
+ def _apply_patches(self) -> None:
203
+ # Suppress Moto's validation of receipt rule actions. These validations use Moto's implementation of S3, Lambda
204
+ # and SQS, which fail because these services have been internalised in LocalStack.
205
+ # Besides, AWS does not run the same validations as evidenced by our AWS-validated tests.
206
+ SESBackend._validate_receipt_rule_actions = lambda *_: None
207
+
201
208
  #
202
209
  # Implementations for SES operations
203
210
  #
@@ -518,8 +525,10 @@ class SesProvider(SesApi, ServiceLifecycleHook):
518
525
  backend.create_receipt_rule_set(rule_set_name)
519
526
  original_rule_set = backend.describe_receipt_rule_set(original_rule_set_name)
520
527
 
528
+ after = None
521
529
  for rule in original_rule_set.rules:
522
- backend.create_receipt_rule(rule_set_name, rule)
530
+ backend.create_receipt_rule(rule_set_name, rule, after)
531
+ after = rule["Name"]
523
532
 
524
533
  return CloneReceiptRuleSetResponse()
525
534
 
@@ -548,7 +557,7 @@ class SesProvider(SesApi, ServiceLifecycleHook):
548
557
  )
549
558
 
550
559
  backend = get_ses_backend(context)
551
- if identity not in backend.addresses:
560
+ if identity not in backend.email_identities:
552
561
  raise MessageRejected(f"Identity {identity} is not verified or does not exist.")
553
562
 
554
563
  # Store the setting in the backend
@@ -678,7 +687,7 @@ class SNSEmitter:
678
687
  def notify_event_destinations(
679
688
  context: RequestContext,
680
689
  # FIXME: Moto stores the Event Destinations as a single value when it should be a list
681
- event_destinations: XFormedDict,
690
+ event_destinations: EventDestination | list[EventDestination],
682
691
  payload: EventDestinationPayload,
683
692
  email_type: EmailType,
684
693
  ):
@@ -688,14 +697,14 @@ def notify_event_destinations(
688
697
  event_destinations = [event_destinations]
689
698
 
690
699
  for event_destination in event_destinations:
691
- if not event_destination["enabled"]:
700
+ if not event_destination["Enabled"]:
692
701
  continue
693
702
 
694
- sns_destination_arn = event_destination.get("sns_destination", {}).get("topic_arn")
703
+ sns_destination_arn = event_destination.get("SNSDestination", {}).get("TopicARN")
695
704
  if not sns_destination_arn:
696
705
  continue
697
706
 
698
- matching_event_types = event_destination.get("matching_event_types") or []
707
+ matching_event_types = event_destination.get("MatchingEventTypes") or []
699
708
  if EventType.send in matching_event_types:
700
709
  emitter.emit_send_event(
701
710
  payload, sns_destination_arn, emit_source_arn=email_type != EmailType.TEMPLATED
@@ -1,5 +1,8 @@
1
1
  import re
2
2
  from string import ascii_letters, digits
3
+ from typing import get_args
4
+
5
+ from localstack.services.sns.v2.models import SnsApplicationPlatforms
3
6
 
4
7
  SNS_PROTOCOLS = [
5
8
  "http",
@@ -13,7 +16,7 @@ SNS_PROTOCOLS = [
13
16
  "firehose",
14
17
  ]
15
18
 
16
- VALID_SUBSCRIPTION_ATTR_NAME = [
19
+ VALID_SUBSCRIPTION_ATTR_NAME: list[str] = [
17
20
  "DeliveryPolicy",
18
21
  "FilterPolicy",
19
22
  "FilterPolicyScope",
@@ -39,3 +42,6 @@ SUBSCRIPTION_TOKENS_ENDPOINT = "/_aws/sns/subscription-tokens"
39
42
  SNS_CERT_ENDPOINT = "/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem"
40
43
 
41
44
  DUMMY_SUBSCRIPTION_PRINCIPAL = "arn:{partition}:iam::{account_id}:user/DummySNSPrincipal"
45
+ E164_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
46
+
47
+ VALID_APPLICATION_PLATFORMS = list(get_args(SnsApplicationPlatforms))