localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev42__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 (208) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +560 -559
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1041 -969
  5. localstack/aws/api/cloudwatch/__init__.py +408 -368
  6. localstack/aws/api/config/__init__.py +788 -786
  7. localstack/aws/api/core.py +4 -0
  8. localstack/aws/api/dynamodb/__init__.py +753 -759
  9. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  10. localstack/aws/api/ec2/__init__.py +9713 -8573
  11. localstack/aws/api/es/__init__.py +453 -453
  12. localstack/aws/api/events/__init__.py +552 -552
  13. localstack/aws/api/firehose/__init__.py +541 -543
  14. localstack/aws/api/iam/__init__.py +646 -572
  15. localstack/aws/api/kinesis/__init__.py +251 -144
  16. localstack/aws/api/kms/__init__.py +343 -333
  17. localstack/aws/api/lambda_/__init__.py +585 -571
  18. localstack/aws/api/logs/__init__.py +682 -666
  19. localstack/aws/api/opensearch/__init__.py +814 -785
  20. localstack/aws/api/pipes/__init__.py +336 -336
  21. localstack/aws/api/redshift/__init__.py +1192 -1164
  22. localstack/aws/api/resource_groups/__init__.py +175 -175
  23. localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
  24. localstack/aws/api/route53/__init__.py +256 -254
  25. localstack/aws/api/route53resolver/__init__.py +396 -396
  26. localstack/aws/api/s3/__init__.py +1358 -1345
  27. localstack/aws/api/s3control/__init__.py +616 -584
  28. localstack/aws/api/scheduler/__init__.py +118 -118
  29. localstack/aws/api/secretsmanager/__init__.py +193 -193
  30. localstack/aws/api/ses/__init__.py +227 -227
  31. localstack/aws/api/sns/__init__.py +115 -115
  32. localstack/aws/api/sqs/__init__.py +100 -100
  33. localstack/aws/api/ssm/__init__.py +1978 -1970
  34. localstack/aws/api/stepfunctions/__init__.py +323 -323
  35. localstack/aws/api/sts/__init__.py +90 -66
  36. localstack/aws/api/support/__init__.py +112 -112
  37. localstack/aws/api/swf/__init__.py +378 -386
  38. localstack/aws/api/transcribe/__init__.py +425 -425
  39. localstack/aws/client.py +7 -2
  40. localstack/aws/forwarder.py +52 -5
  41. localstack/aws/handlers/analytics.py +1 -1
  42. localstack/aws/handlers/logging.py +12 -2
  43. localstack/aws/handlers/metric_handler.py +41 -1
  44. localstack/aws/handlers/service.py +43 -10
  45. localstack/aws/protocol/parser.py +440 -21
  46. localstack/aws/protocol/serializer.py +684 -64
  47. localstack/aws/protocol/service_router.py +120 -20
  48. localstack/aws/scaffold.py +15 -17
  49. localstack/aws/skeleton.py +4 -2
  50. localstack/aws/spec-patches.json +58 -0
  51. localstack/aws/spec.py +33 -13
  52. localstack/cli/exceptions.py +1 -1
  53. localstack/cli/localstack.py +10 -5
  54. localstack/cli/lpm.py +3 -4
  55. localstack/cli/profiles.py +1 -2
  56. localstack/config.py +18 -12
  57. localstack/constants.py +4 -29
  58. localstack/dev/kubernetes/__main__.py +39 -4
  59. localstack/dev/run/paths.py +1 -1
  60. localstack/dns/plugins.py +5 -1
  61. localstack/dns/server.py +12 -3
  62. localstack/packages/api.py +9 -8
  63. localstack/packages/core.py +2 -2
  64. localstack/packages/plugins.py +0 -8
  65. localstack/runtime/init.py +1 -1
  66. localstack/services/apigateway/helpers.py +5 -9
  67. localstack/services/apigateway/legacy/provider.py +85 -12
  68. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  69. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  70. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  71. localstack/services/apigateway/next_gen/provider.py +5 -0
  72. localstack/services/apigateway/patches.py +0 -9
  73. localstack/services/cloudformation/engine/entities.py +12 -1
  74. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  75. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  76. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  77. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  78. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  79. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  80. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  81. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  82. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  83. localstack/services/cloudformation/provider.py +2 -2
  84. localstack/services/cloudformation/resource_provider.py +5 -1
  85. localstack/services/cloudformation/resources.py +24149 -0
  86. localstack/services/cloudformation/v2/entities.py +6 -3
  87. localstack/services/cloudformation/v2/provider.py +178 -33
  88. localstack/services/cloudformation/v2/types.py +8 -4
  89. localstack/services/cloudwatch/provider_v2.py +25 -28
  90. localstack/services/dynamodb/packages.py +2 -1
  91. localstack/services/dynamodb/provider.py +42 -0
  92. localstack/services/dynamodb/v2/provider.py +42 -0
  93. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  94. localstack/services/es/provider.py +2 -2
  95. localstack/services/events/event_rule_engine.py +31 -13
  96. localstack/services/events/models.py +4 -5
  97. localstack/services/events/target.py +17 -9
  98. localstack/services/iam/provider.py +11 -116
  99. localstack/services/iam/resources/policy_simulator.py +133 -0
  100. localstack/services/kinesis/models.py +15 -2
  101. localstack/services/kinesis/packages.py +1 -1
  102. localstack/services/kinesis/provider.py +77 -0
  103. localstack/services/kms/models.py +34 -4
  104. localstack/services/kms/provider.py +107 -21
  105. localstack/services/lambda_/api_utils.py +3 -1
  106. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  107. localstack/services/lambda_/packages.py +1 -1
  108. localstack/services/lambda_/provider.py +1 -1
  109. localstack/services/lambda_/runtimes.py +8 -3
  110. localstack/services/logs/provider.py +36 -19
  111. localstack/services/moto.py +2 -1
  112. localstack/services/opensearch/cluster.py +15 -7
  113. localstack/services/opensearch/packages.py +26 -7
  114. localstack/services/opensearch/provider.py +6 -1
  115. localstack/services/opensearch/versions.py +56 -7
  116. localstack/services/s3/constants.py +5 -2
  117. localstack/services/s3/cors.py +4 -4
  118. localstack/services/s3/notifications.py +1 -1
  119. localstack/services/s3/presigned_url.py +27 -43
  120. localstack/services/s3/provider.py +68 -12
  121. localstack/services/s3/utils.py +42 -11
  122. localstack/services/ses/provider.py +16 -7
  123. localstack/services/sns/constants.py +7 -1
  124. localstack/services/sns/v2/models.py +190 -0
  125. localstack/services/sns/v2/provider.py +992 -2
  126. localstack/services/sns/v2/utils.py +138 -0
  127. localstack/services/sqs/developer_api.py +205 -0
  128. localstack/services/sqs/models.py +79 -13
  129. localstack/services/sqs/provider.py +8 -309
  130. localstack/services/sqs/query_api.py +1 -1
  131. localstack/services/sqs/utils.py +121 -2
  132. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  133. localstack/testing/aws/cloudformation_utils.py +1 -1
  134. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  135. localstack/testing/pytest/container.py +4 -5
  136. localstack/testing/pytest/fixtures.py +20 -19
  137. localstack/testing/pytest/in_memory_localstack.py +0 -4
  138. localstack/testing/pytest/marking.py +13 -4
  139. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  140. localstack/testing/pytest/util.py +1 -1
  141. localstack/testing/pytest/validation_tracking.py +1 -2
  142. localstack/testing/snapshots/transformer_utility.py +7 -0
  143. localstack/testing/testselection/matching.py +0 -1
  144. localstack/utils/analytics/events.py +2 -2
  145. localstack/utils/analytics/metadata.py +1 -2
  146. localstack/utils/analytics/metrics/counter.py +6 -8
  147. localstack/utils/analytics/publisher.py +1 -2
  148. localstack/utils/analytics/service_request_aggregator.py +2 -2
  149. localstack/utils/archives.py +11 -11
  150. localstack/utils/aws/arns.py +17 -9
  151. localstack/utils/aws/aws_responses.py +7 -7
  152. localstack/utils/aws/aws_stack.py +2 -3
  153. localstack/utils/aws/client_types.py +0 -8
  154. localstack/utils/aws/message_forwarding.py +1 -2
  155. localstack/utils/aws/request_context.py +4 -5
  156. localstack/utils/batch_policy.py +3 -3
  157. localstack/utils/bootstrap.py +7 -7
  158. localstack/utils/catalog/catalog.py +139 -0
  159. localstack/utils/catalog/catalog_loader.py +119 -0
  160. localstack/utils/catalog/common.py +58 -0
  161. localstack/utils/catalog/plugins.py +28 -0
  162. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  163. localstack/utils/collections.py +7 -8
  164. localstack/utils/config_listener.py +1 -1
  165. localstack/utils/container_networking.py +2 -3
  166. localstack/utils/container_utils/container_client.py +115 -131
  167. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  168. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  169. localstack/utils/crypto.py +109 -0
  170. localstack/utils/diagnose.py +2 -3
  171. localstack/utils/docker_utils.py +3 -4
  172. localstack/utils/files.py +31 -7
  173. localstack/utils/functions.py +3 -2
  174. localstack/utils/http.py +4 -5
  175. localstack/utils/json.py +19 -5
  176. localstack/utils/kinesis/kinesis_connector.py +2 -1
  177. localstack/utils/net.py +6 -6
  178. localstack/utils/no_exit_argument_parser.py +2 -2
  179. localstack/utils/numbers.py +9 -2
  180. localstack/utils/objects.py +6 -5
  181. localstack/utils/patch.py +2 -1
  182. localstack/utils/run.py +10 -9
  183. localstack/utils/scheduler.py +11 -11
  184. localstack/utils/server/tcp_proxy.py +2 -2
  185. localstack/utils/serving.py +2 -3
  186. localstack/utils/strings.py +10 -11
  187. localstack/utils/sync.py +126 -1
  188. localstack/utils/tagging.py +1 -4
  189. localstack/utils/testutil.py +5 -4
  190. localstack/utils/threads.py +2 -2
  191. localstack/utils/time.py +11 -3
  192. localstack/utils/urls.py +1 -3
  193. localstack/version.py +2 -2
  194. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
  195. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
  196. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
  197. localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
  198. localstack/packages/terraform.py +0 -46
  199. localstack/services/cloudformation/deploy.html +0 -144
  200. localstack/services/cloudformation/deploy_ui.py +0 -47
  201. localstack/services/cloudformation/plugins.py +0 -12
  202. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  203. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
  204. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
  205. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
  206. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
  207. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
  208. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/top_level.txt +0 -0
@@ -47,6 +47,8 @@ from localstack.aws.api.dynamodb import (
47
47
  DeleteRequest,
48
48
  DeleteTableOutput,
49
49
  DescribeContinuousBackupsOutput,
50
+ DescribeContributorInsightsInput,
51
+ DescribeContributorInsightsOutput,
50
52
  DescribeGlobalTableOutput,
51
53
  DescribeKinesisStreamingDestinationOutput,
52
54
  DescribeTableOutput,
@@ -746,6 +748,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
746
748
  if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
747
749
  table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
748
750
 
751
+ if "WarmThroughput" in table_description:
752
+ table_description["WarmThroughput"]["Status"] = "UPDATING"
753
+
749
754
  tags = table_definitions.pop("Tags", [])
750
755
  if tags:
751
756
  get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
@@ -763,6 +768,13 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
763
768
  ) -> DeleteTableOutput:
764
769
  global_table_region = self.get_global_table_region(context, table_name)
765
770
 
771
+ self.ensure_table_exists(
772
+ context.account_id,
773
+ global_table_region,
774
+ table_name,
775
+ error_message=f"Requested resource not found: Table: {table_name} not found",
776
+ )
777
+
766
778
  # Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
767
779
  # This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
768
780
 
@@ -823,6 +835,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
823
835
  table_description["TableClassSummary"] = {
824
836
  "TableClass": table_definitions["TableClass"]
825
837
  }
838
+ if warm_throughput := table_definitions.get("WarmThroughput"):
839
+ table_description["WarmThroughput"] = warm_throughput.copy()
840
+ table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
826
841
 
827
842
  if "GlobalSecondaryIndexes" in table_description:
828
843
  for gsi in table_description["GlobalSecondaryIndexes"]:
@@ -835,6 +850,17 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
835
850
  # Terraform depends on this parity for update operations
836
851
  gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
837
852
 
853
+ # Set defaults for warm throughput
854
+ if "WarmThroughput" not in table_description:
855
+ billing_mode = table_definitions.get("BillingMode") if table_definitions else None
856
+ table_description["WarmThroughput"] = {
857
+ "ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
858
+ "WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
859
+ }
860
+ table_description["WarmThroughput"]["Status"] = (
861
+ table_description.get("TableStatus") or "ACTIVE"
862
+ )
863
+
838
864
  return DescribeTableOutput(
839
865
  Table=select_from_typed_dict(TableDescription, table_description)
840
866
  )
@@ -955,6 +981,22 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
955
981
 
956
982
  return response
957
983
 
984
+ #
985
+ # Contributor Insights
986
+ #
987
+
988
+ @handler("DescribeContributorInsights", expand=False)
989
+ def describe_contributor_insights(
990
+ self,
991
+ context: RequestContext,
992
+ describe_contributor_insights_input: DescribeContributorInsightsInput,
993
+ ) -> DescribeContributorInsightsOutput:
994
+ return DescribeContributorInsightsOutput(
995
+ TableName=describe_contributor_insights_input["TableName"],
996
+ IndexName=describe_contributor_insights_input.get("IndexName"),
997
+ ContributorInsightsStatus="DISABLED",
998
+ )
999
+
958
1000
  #
959
1001
  # Item ops
960
1002
  #
@@ -40,6 +40,8 @@ from localstack.aws.api.dynamodb import (
40
40
  DeleteRequest,
41
41
  DeleteTableOutput,
42
42
  DescribeContinuousBackupsOutput,
43
+ DescribeContributorInsightsInput,
44
+ DescribeContributorInsightsOutput,
43
45
  DescribeGlobalTableOutput,
44
46
  DescribeKinesisStreamingDestinationOutput,
45
47
  DescribeTableOutput,
@@ -558,6 +560,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
558
560
  if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
559
561
  table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
560
562
 
563
+ if "WarmThroughput" in table_description:
564
+ table_description["WarmThroughput"]["Status"] = "UPDATING"
565
+
561
566
  tags = table_definitions.pop("Tags", [])
562
567
  if tags:
563
568
  get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
@@ -575,6 +580,13 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
575
580
  ) -> DeleteTableOutput:
576
581
  global_table_region = self.get_global_table_region(context, table_name)
577
582
 
583
+ self.ensure_table_exists(
584
+ context.account_id,
585
+ global_table_region,
586
+ table_name,
587
+ error_message=f"Requested resource not found: Table: {table_name} not found",
588
+ )
589
+
578
590
  # Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
579
591
  # This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
580
592
 
@@ -634,6 +646,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
634
646
  table_description["TableClassSummary"] = {
635
647
  "TableClass": table_definitions["TableClass"]
636
648
  }
649
+ if warm_throughput := table_definitions.get("WarmThroughput"):
650
+ table_description["WarmThroughput"] = warm_throughput.copy()
651
+ table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
637
652
 
638
653
  if "GlobalSecondaryIndexes" in table_description:
639
654
  for gsi in table_description["GlobalSecondaryIndexes"]:
@@ -646,6 +661,17 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
646
661
  # Terraform depends on this parity for update operations
647
662
  gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
648
663
 
664
+ # Set defaults for warm throughput
665
+ if "WarmThroughput" not in table_description:
666
+ billing_mode = table_definitions.get("BillingMode") if table_definitions else None
667
+ table_description["WarmThroughput"] = {
668
+ "ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
669
+ "WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
670
+ }
671
+ table_description["WarmThroughput"]["Status"] = (
672
+ table_description.get("TableStatus") or "ACTIVE"
673
+ )
674
+
649
675
  return DescribeTableOutput(
650
676
  Table=select_from_typed_dict(TableDescription, table_description)
651
677
  )
@@ -757,6 +783,22 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
757
783
 
758
784
  return response
759
785
 
786
+ #
787
+ # Contributor Insights
788
+ #
789
+
790
+ @handler("DescribeContributorInsights", expand=False)
791
+ def describe_contributor_insights(
792
+ self,
793
+ context: RequestContext,
794
+ describe_contributor_insights_input: DescribeContributorInsightsInput,
795
+ ) -> DescribeContributorInsightsOutput:
796
+ return DescribeContributorInsightsOutput(
797
+ TableName=describe_contributor_insights_input["TableName"],
798
+ IndexName=describe_contributor_insights_input.get("IndexName"),
799
+ ContributorInsightsStatus="DISABLED",
800
+ )
801
+
760
802
  #
761
803
  # Item ops
762
804
  #
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from typing import TypedDict
7
7
 
8
8
  import localstack.services.cloudformation.provider_utils as util
9
- from localstack.constants import AWS_REGION_US_EAST_1, DEFAULT_AWS_ACCOUNT_ID
10
9
  from localstack.services.cloudformation.resource_provider import (
11
10
  OperationStatus,
12
11
  ProgressEvent,
@@ -90,6 +89,10 @@ class ECRRepositoryProvider(ResourceProvider[ECRRepositoryProperties]):
90
89
 
91
90
  """
92
91
  model = request.desired_state
92
+ model["RepositoryName"] = (
93
+ model.get("RepositoryName")
94
+ or util.generate_default_name(request.stack_name, request.logical_resource_id).lower()
95
+ )
93
96
 
94
97
  default_repos_per_stack[request.stack_name] = model["RepositoryName"]
95
98
  LOG.warning(
@@ -98,7 +101,7 @@ class ECRRepositoryProvider(ResourceProvider[ECRRepositoryProperties]):
98
101
  model.update(
99
102
  {
100
103
  "Arn": arns.ecr_repository_arn(
101
- model["RepositoryName"], DEFAULT_AWS_ACCOUNT_ID, AWS_REGION_US_EAST_1
104
+ model["RepositoryName"], request.account_id, request.region_name
102
105
  ),
103
106
  "RepositoryUri": "http://localhost:4566",
104
107
  "ImageTagMutability": "MUTABLE",
@@ -3,7 +3,6 @@ from typing import cast
3
3
 
4
4
  from botocore.exceptions import ClientError
5
5
 
6
- from localstack import constants
7
6
  from localstack.aws.api import RequestContext, handler
8
7
  from localstack.aws.api.es import (
9
8
  ARN,
@@ -68,6 +67,7 @@ from localstack.aws.api.opensearch import (
68
67
  VersionString,
69
68
  )
70
69
  from localstack.aws.connect import connect_to
70
+ from localstack.services.opensearch.packages import ELASTICSEARCH_DEFAULT_VERSION
71
71
 
72
72
 
73
73
  def _version_to_opensearch(
@@ -236,7 +236,7 @@ class EsProvider(EsApi):
236
236
  engine_version = (
237
237
  _version_to_opensearch(elasticsearch_version)
238
238
  if elasticsearch_version
239
- else constants.ELASTICSEARCH_DEFAULT_VERSION
239
+ else ELASTICSEARCH_DEFAULT_VERSION
240
240
  )
241
241
  kwargs = {
242
242
  "DomainName": domain_name,
@@ -59,14 +59,21 @@ class EventRuleEngine:
59
59
  for flat_pattern in flat_pattern_conditions
60
60
  )
61
61
 
62
- def _evaluate_condition(self, value, condition, field_exists: bool):
62
+ def _evaluate_condition(self, value: t.Any, condition: t.Any, field_exists: bool) -> bool:
63
63
  if not isinstance(condition, dict):
64
64
  return field_exists and value == condition
65
+
65
66
  elif (must_exist := condition.get("exists")) is not None:
66
67
  # if must_exists is True then field_exists must be True
67
68
  # if must_exists is False then fields_exists must be False
68
69
  return must_exist == field_exists
70
+
69
71
  elif (anything_but := condition.get("anything-but")) is not None:
72
+ if not field_exists:
73
+ # anything-but can handle None `value`, but it needs to differentiate between user-set `null` and
74
+ # missing value
75
+ return False
76
+
70
77
  if isinstance(anything_but, dict):
71
78
  if (not_condition := anything_but.get("prefix")) is not None:
72
79
  predicate = self._evaluate_prefix
@@ -95,6 +102,7 @@ class EventRuleEngine:
95
102
  elif value is None:
96
103
  # the remaining conditions require the value to not be None
97
104
  return False
105
+
98
106
  elif (prefix := condition.get("prefix")) is not None:
99
107
  if isinstance(prefix, dict):
100
108
  if (prefix_equal_ignore_case := prefix.get("equals-ignore-case")) is not None:
@@ -104,7 +112,7 @@ class EventRuleEngine:
104
112
 
105
113
  elif (suffix := condition.get("suffix")) is not None:
106
114
  if isinstance(suffix, dict):
107
- if suffix_equal_ignore_case := suffix.get("equals-ignore-case"):
115
+ if (suffix_equal_ignore_case := suffix.get("equals-ignore-case")) is not None:
108
116
  return self._evaluate_suffix(suffix_equal_ignore_case.lower(), value.lower())
109
117
  else:
110
118
  return self._evaluate_suffix(suffix, value)
@@ -126,19 +134,19 @@ class EventRuleEngine:
126
134
  return False
127
135
 
128
136
  @staticmethod
129
- def _evaluate_prefix(condition: str | list, value: str) -> bool:
130
- return value.startswith(condition)
137
+ def _evaluate_prefix(condition: str | list, value: t.Any) -> bool:
138
+ return isinstance(value, str) and value.startswith(condition)
131
139
 
132
140
  @staticmethod
133
- def _evaluate_suffix(condition: str | list, value: str) -> bool:
134
- return value.endswith(condition)
141
+ def _evaluate_suffix(condition: str | list, value: t.Any) -> bool:
142
+ return isinstance(value, str) and value.endswith(condition)
135
143
 
136
144
  @staticmethod
137
- def _evaluate_equal_ignore_case(condition: str, value: str) -> bool:
138
- return condition.lower() == value.lower()
145
+ def _evaluate_equal_ignore_case(condition: str, value: t.Any) -> bool:
146
+ return isinstance(value, str) and condition.lower() == value.lower()
139
147
 
140
148
  @staticmethod
141
- def _evaluate_cidr(condition: str, value: str) -> bool:
149
+ def _evaluate_cidr(condition: str, value: t.Any) -> bool:
142
150
  try:
143
151
  ip = ipaddress.ip_address(value)
144
152
  return ip in ipaddress.ip_network(condition)
@@ -146,8 +154,10 @@ class EventRuleEngine:
146
154
  return False
147
155
 
148
156
  @staticmethod
149
- def _evaluate_wildcard(condition: str, value: str) -> bool:
150
- return bool(re.match(re.escape(condition).replace("\\*", ".+") + "$", value))
157
+ def _evaluate_wildcard(condition: str, value: t.Any) -> bool:
158
+ return isinstance(value, str) and bool(
159
+ re.match(re.escape(condition).replace("\\*", ".+") + "$", value)
160
+ )
151
161
 
152
162
  @staticmethod
153
163
  def _evaluate_numeric_condition(conditions: list, value: t.Any) -> bool:
@@ -457,10 +467,18 @@ class EventPatternCompiler:
457
467
  return
458
468
 
459
469
  elif operator == "anything-but":
460
- # anything-but can actually contain any kind of simple rule (str, number, and list)
470
+ # anything-but can actually contain any kind of simple rule (str, number, and list) except Null
471
+ if value is None:
472
+ raise InvalidEventPatternException(
473
+ f"{self.error_prefix}Value of anything-but must be an array or single string/number value."
474
+ )
461
475
  if isinstance(value, list):
462
476
  for v in value:
463
- self._validate_rule(v)
477
+ if v is None:
478
+ raise InvalidEventPatternException(
479
+ f"{self.error_prefix}Inside anything but list, start|null|boolean is not supported."
480
+ )
481
+ self._validate_rule(v, from_="anything-but")
464
482
 
465
483
  return
466
484
 
@@ -4,7 +4,7 @@ from datetime import UTC, datetime
4
4
  from enum import Enum
5
5
  from typing import Literal, TypeAlias, TypedDict
6
6
 
7
- from localstack.aws.api.core import ServiceException
7
+ from localstack.aws.api import CommonServiceException
8
8
  from localstack.aws.api.events import (
9
9
  ApiDestinationDescription,
10
10
  ApiDestinationHttpMethod,
@@ -66,10 +66,9 @@ from localstack.utils.tagging import TaggingService
66
66
  TargetDict = dict[TargetId, Target]
67
67
 
68
68
 
69
- class ValidationException(ServiceException):
70
- code: str = "ValidationException"
71
- sender_fault: bool = True
72
- status_code: int = 400
69
+ class ValidationException(CommonServiceException):
70
+ def __init__(self, message: str):
71
+ super().__init__("ValidationException", message, 400, True)
73
72
 
74
73
 
75
74
  class InvalidEventPatternException(Exception):
@@ -90,11 +90,20 @@ def get_template_replacements(
90
90
  return template_replacements
91
91
 
92
92
 
93
- def replace_template_placeholders(
94
- template: str, replacements: dict[str, Any], is_json_template: bool
95
- ) -> TransformedEvent:
96
- """Replace placeholders defined by <key> in the template with the values from the replacements dict.
97
- Can handle single template string or template dict."""
93
+ def replace_template_placeholders(template: str, replacements: dict[str, Any]) -> TransformedEvent:
94
+ """
95
+ Replaces placeholders in an EventBridge-style InputTemplate string.
96
+
97
+ :param template: The template string containing placeholders like ``<$.foo.bar>``.
98
+ :type template: str
99
+ :param replacements: A dictionary providing values to fill in.
100
+ :type replacements: dict
101
+ :returns: The transformed string with placeholders replaced by values from ``replacements``.
102
+ :rtype: str
103
+ """
104
+
105
+ ...
106
+ is_json_template = template.strip().startswith("{")
98
107
 
99
108
  def replace_placeholder(match):
100
109
  key = match.group(1)
@@ -110,6 +119,8 @@ def replace_template_placeholders(
110
119
  if is_json_template:
111
120
  return json.dumps(value)
112
121
  return f"[{','.join(value)}]"
122
+ if isinstance(value, bool):
123
+ return json.dumps(value)
113
124
  if is_nested_in_string(template, match):
114
125
  return value
115
126
  if is_json_template:
@@ -222,10 +233,7 @@ class TargetSender(ABC):
222
233
  predefined_template_replacements = self._get_predefined_template_replacements(event)
223
234
  template_replacements.update(predefined_template_replacements)
224
235
 
225
- is_json_template = input_template.strip().startswith("{")
226
- populated_template = replace_template_placeholders(
227
- input_template, template_replacements, is_json_template
228
- )
236
+ populated_template = replace_template_placeholders(input_template, template_replacements)
229
237
 
230
238
  return populated_template
231
239
 
@@ -20,10 +20,7 @@ from moto.iam.utils import generate_access_key_id_from_account_id
20
20
 
21
21
  from localstack.aws.api import CommonServiceException, RequestContext, handler
22
22
  from localstack.aws.api.iam import (
23
- ActionNameListType,
24
- ActionNameType,
25
23
  AttachedPermissionsBoundary,
26
- ContextEntryListType,
27
24
  CreateRoleRequest,
28
25
  CreateRoleResponse,
29
26
  CreateServiceLinkedRoleResponse,
@@ -33,7 +30,6 @@ from localstack.aws.api.iam import (
33
30
  DeleteServiceLinkedRoleResponse,
34
31
  DeletionTaskIdType,
35
32
  DeletionTaskStatusType,
36
- EvaluationResult,
37
33
  GetServiceLinkedRoleDeletionStatusResponse,
38
34
  GetUserResponse,
39
35
  IamApi,
@@ -43,16 +39,12 @@ from localstack.aws.api.iam import (
43
39
  ListServiceSpecificCredentialsResponse,
44
40
  MalformedPolicyDocumentException,
45
41
  NoSuchEntityException,
46
- PolicyEvaluationDecisionType,
47
42
  ResetServiceSpecificCredentialResponse,
48
- ResourceHandlingOptionType,
49
- ResourceNameListType,
50
- ResourceNameType,
51
43
  Role,
52
44
  ServiceSpecificCredential,
53
45
  ServiceSpecificCredentialMetadata,
54
46
  SimulatePolicyResponse,
55
- SimulationPolicyListType,
47
+ SimulatePrincipalPolicyRequest,
56
48
  User,
57
49
  allUsers,
58
50
  arnType,
@@ -65,7 +57,6 @@ from localstack.aws.api.iam import (
65
57
  maxItemsType,
66
58
  pathPrefixType,
67
59
  pathType,
68
- policyDocumentType,
69
60
  roleDescriptionType,
70
61
  roleNameType,
71
62
  serviceName,
@@ -78,6 +69,10 @@ from localstack.aws.api.iam import (
78
69
  from localstack.aws.connect import connect_to
79
70
  from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY
80
71
  from localstack.services.iam.iam_patches import apply_iam_patches
72
+ from localstack.services.iam.resources.policy_simulator import (
73
+ BasicIAMPolicySimulator,
74
+ IAMPolicySimulator,
75
+ )
81
76
  from localstack.services.iam.resources.service_linked_roles import SERVICE_LINKED_ROLES
82
77
  from localstack.services.moto import call_moto
83
78
  from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
@@ -108,58 +103,12 @@ def get_iam_backend(context: RequestContext) -> IAMBackend:
108
103
  return iam_backends[context.account_id][context.partition]
109
104
 
110
105
 
111
- def get_policies_from_principal(backend: IAMBackend, principal_arn: str) -> list[dict]:
112
- policies = []
113
- if ":role" in principal_arn:
114
- role_name = principal_arn.split("/")[-1]
115
-
116
- policies.append(backend.get_role(role_name=role_name).assume_role_policy_document)
117
-
118
- policy_names = backend.list_role_policies(role_name=role_name)
119
- policies.extend(
120
- [
121
- backend.get_role_policy(role_name=role_name, policy_name=policy_name)[1]
122
- for policy_name in policy_names
123
- ]
124
- )
125
-
126
- attached_policies, _ = backend.list_attached_role_policies(role_name=role_name)
127
- policies.extend([policy.document for policy in attached_policies])
128
-
129
- if ":group" in principal_arn:
130
- print(principal_arn)
131
- group_name = principal_arn.split("/")[-1]
132
- policy_names = backend.list_group_policies(group_name=group_name)
133
- policies.extend(
134
- [
135
- backend.get_group_policy(group_name=group_name, policy_name=policy_name)[1]
136
- for policy_name in policy_names
137
- ]
138
- )
139
-
140
- attached_policies, _ = backend.list_attached_group_policies(group_name=group_name)
141
- policies.extend([policy.document for policy in attached_policies])
142
-
143
- if ":user" in principal_arn:
144
- print(principal_arn)
145
- user_name = principal_arn.split("/")[-1]
146
- policy_names = backend.list_user_policies(user_name=user_name)
147
- policies.extend(
148
- [
149
- backend.get_user_policy(user_name=user_name, policy_name=policy_name)[1]
150
- for policy_name in policy_names
151
- ]
152
- )
153
-
154
- attached_policies, _ = backend.list_attached_user_policies(user_name=user_name)
155
- policies.extend([policy.document for policy in attached_policies])
156
-
157
- return policies
158
-
159
-
160
106
  class IamProvider(IamApi):
107
+ policy_simulator: IAMPolicySimulator
108
+
161
109
  def __init__(self):
162
110
  apply_iam_patches()
111
+ self.policy_simulator = BasicIAMPolicySimulator()
163
112
 
164
113
  @handler("CreateRole", expand=False)
165
114
  def create_role(
@@ -181,68 +130,14 @@ class IamProvider(IamApi):
181
130
 
182
131
  return result
183
132
 
184
- @staticmethod
185
- def build_evaluation_result(
186
- action_name: ActionNameType, resource_name: ResourceNameType, policy_statements: list[dict]
187
- ) -> EvaluationResult:
188
- eval_res = EvaluationResult()
189
- eval_res["EvalActionName"] = action_name
190
- eval_res["EvalResourceName"] = resource_name
191
- eval_res["EvalDecision"] = PolicyEvaluationDecisionType.explicitDeny
192
- for statement in policy_statements:
193
- # TODO Implement evaluation logic here
194
- if (
195
- action_name in statement["Action"]
196
- and resource_name in statement["Resource"]
197
- and statement["Effect"] == "Allow"
198
- ):
199
- eval_res["EvalDecision"] = PolicyEvaluationDecisionType.allowed
200
- eval_res["MatchedStatements"] = [] # TODO: add support for statement compilation.
201
- return eval_res
202
-
133
+ @handler("SimulatePrincipalPolicy", expand=False)
203
134
  def simulate_principal_policy(
204
135
  self,
205
136
  context: RequestContext,
206
- policy_source_arn: arnType,
207
- action_names: ActionNameListType,
208
- policy_input_list: SimulationPolicyListType = None,
209
- permissions_boundary_policy_input_list: SimulationPolicyListType = None,
210
- resource_arns: ResourceNameListType = None,
211
- resource_policy: policyDocumentType = None,
212
- resource_owner: ResourceNameType = None,
213
- caller_arn: ResourceNameType = None,
214
- context_entries: ContextEntryListType = None,
215
- resource_handling_option: ResourceHandlingOptionType = None,
216
- max_items: maxItemsType = None,
217
- marker: markerType = None,
137
+ request: SimulatePrincipalPolicyRequest,
218
138
  **kwargs,
219
139
  ) -> SimulatePolicyResponse:
220
- backend = get_iam_backend(context)
221
-
222
- policies = get_policies_from_principal(backend, policy_source_arn)
223
-
224
- def _get_statements_from_policy_list(policies: list[str]):
225
- statements = []
226
- for policy_str in policies:
227
- policy_dict = json.loads(policy_str)
228
- if isinstance(policy_dict["Statement"], list):
229
- statements.extend(policy_dict["Statement"])
230
- else:
231
- statements.append(policy_dict["Statement"])
232
- return statements
233
-
234
- policy_statements = _get_statements_from_policy_list(policies)
235
-
236
- evaluations = [
237
- self.build_evaluation_result(action_name, resource_arn, policy_statements)
238
- for action_name in action_names
239
- for resource_arn in resource_arns
240
- ]
241
-
242
- response = SimulatePolicyResponse()
243
- response["IsTruncated"] = False
244
- response["EvaluationResults"] = evaluations
245
- return response
140
+ return self.policy_simulator.simulate_principal_policy(context, request)
246
141
 
247
142
  def delete_policy(self, context: RequestContext, policy_arn: arnType, **kwargs) -> None:
248
143
  backend = get_iam_backend(context)