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.
- localstack/aws/api/acm/__init__.py +122 -122
- localstack/aws/api/apigateway/__init__.py +560 -559
- localstack/aws/api/cloudcontrol/__init__.py +63 -63
- localstack/aws/api/cloudformation/__init__.py +1041 -969
- localstack/aws/api/cloudwatch/__init__.py +408 -368
- localstack/aws/api/config/__init__.py +788 -786
- localstack/aws/api/core.py +4 -0
- localstack/aws/api/dynamodb/__init__.py +753 -759
- localstack/aws/api/dynamodbstreams/__init__.py +74 -74
- localstack/aws/api/ec2/__init__.py +9713 -8573
- localstack/aws/api/es/__init__.py +453 -453
- localstack/aws/api/events/__init__.py +552 -552
- localstack/aws/api/firehose/__init__.py +541 -543
- localstack/aws/api/iam/__init__.py +646 -572
- localstack/aws/api/kinesis/__init__.py +251 -144
- localstack/aws/api/kms/__init__.py +343 -333
- localstack/aws/api/lambda_/__init__.py +585 -571
- localstack/aws/api/logs/__init__.py +682 -666
- localstack/aws/api/opensearch/__init__.py +814 -785
- localstack/aws/api/pipes/__init__.py +336 -336
- localstack/aws/api/redshift/__init__.py +1192 -1164
- localstack/aws/api/resource_groups/__init__.py +175 -175
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
- localstack/aws/api/route53/__init__.py +256 -254
- localstack/aws/api/route53resolver/__init__.py +396 -396
- localstack/aws/api/s3/__init__.py +1358 -1345
- localstack/aws/api/s3control/__init__.py +616 -584
- localstack/aws/api/scheduler/__init__.py +118 -118
- localstack/aws/api/secretsmanager/__init__.py +193 -193
- localstack/aws/api/ses/__init__.py +227 -227
- localstack/aws/api/sns/__init__.py +115 -115
- localstack/aws/api/sqs/__init__.py +100 -100
- localstack/aws/api/ssm/__init__.py +1978 -1970
- localstack/aws/api/stepfunctions/__init__.py +323 -323
- localstack/aws/api/sts/__init__.py +90 -66
- localstack/aws/api/support/__init__.py +112 -112
- localstack/aws/api/swf/__init__.py +378 -386
- localstack/aws/api/transcribe/__init__.py +425 -425
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +43 -10
- localstack/aws/protocol/parser.py +440 -21
- localstack/aws/protocol/serializer.py +684 -64
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/scaffold.py +15 -17
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +33 -13
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +10 -5
- localstack/cli/lpm.py +3 -4
- localstack/cli/profiles.py +1 -2
- localstack/config.py +18 -12
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +39 -4
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +12 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/init.py +1 -1
- localstack/services/apigateway/helpers.py +5 -9
- localstack/services/apigateway/legacy/provider.py +85 -12
- localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
- localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/apigateway/patches.py +0 -9
- localstack/services/cloudformation/engine/entities.py +12 -1
- localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +6 -4
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/provider.py +2 -2
- localstack/services/cloudformation/resource_provider.py +5 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/v2/entities.py +6 -3
- localstack/services/cloudformation/v2/provider.py +178 -33
- localstack/services/cloudformation/v2/types.py +8 -4
- localstack/services/cloudwatch/provider_v2.py +25 -28
- localstack/services/dynamodb/packages.py +2 -1
- localstack/services/dynamodb/provider.py +42 -0
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/es/provider.py +2 -2
- localstack/services/events/event_rule_engine.py +31 -13
- localstack/services/events/models.py +4 -5
- localstack/services/events/target.py +17 -9
- localstack/services/iam/provider.py +11 -116
- localstack/services/iam/resources/policy_simulator.py +133 -0
- localstack/services/kinesis/models.py +15 -2
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kinesis/provider.py +77 -0
- localstack/services/kms/models.py +34 -4
- localstack/services/kms/provider.py +107 -21
- localstack/services/lambda_/api_utils.py +3 -1
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +1 -1
- localstack/services/lambda_/runtimes.py +8 -3
- localstack/services/logs/provider.py +36 -19
- localstack/services/moto.py +2 -1
- localstack/services/opensearch/cluster.py +15 -7
- localstack/services/opensearch/packages.py +26 -7
- localstack/services/opensearch/provider.py +6 -1
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/notifications.py +1 -1
- localstack/services/s3/presigned_url.py +27 -43
- localstack/services/s3/provider.py +68 -12
- localstack/services/s3/utils.py +42 -11
- localstack/services/ses/provider.py +16 -7
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/v2/models.py +190 -0
- localstack/services/sns/v2/provider.py +992 -2
- localstack/services/sns/v2/utils.py +138 -0
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +79 -13
- localstack/services/sqs/provider.py +8 -309
- localstack/services/sqs/query_api.py +1 -1
- localstack/services/sqs/utils.py +121 -2
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +20 -19
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +13 -4
- localstack/testing/pytest/stepfunctions/utils.py +4 -3
- localstack/testing/pytest/util.py +1 -1
- localstack/testing/pytest/validation_tracking.py +1 -2
- localstack/testing/snapshots/transformer_utility.py +7 -0
- localstack/testing/testselection/matching.py +0 -1
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +1 -2
- localstack/utils/analytics/metrics/counter.py +6 -8
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/aws/arns.py +17 -9
- localstack/utils/aws/aws_responses.py +7 -7
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/client_types.py +0 -8
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +7 -7
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +119 -0
- localstack/utils/catalog/common.py +58 -0
- localstack/utils/catalog/plugins.py +28 -0
- localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
- localstack/utils/collections.py +7 -8
- localstack/utils/config_listener.py +1 -1
- localstack/utils/container_networking.py +2 -3
- localstack/utils/container_utils/container_client.py +115 -131
- localstack/utils/container_utils/docker_cmd_client.py +42 -42
- localstack/utils/container_utils/docker_sdk_client.py +63 -62
- localstack/utils/crypto.py +109 -0
- localstack/utils/diagnose.py +2 -3
- localstack/utils/docker_utils.py +3 -4
- localstack/utils/files.py +31 -7
- localstack/utils/functions.py +3 -2
- localstack/utils/http.py +4 -5
- localstack/utils/json.py +19 -5
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +6 -6
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +6 -5
- localstack/utils/patch.py +2 -1
- localstack/utils/run.py +10 -9
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +2 -3
- localstack/utils/strings.py +10 -11
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +1 -4
- localstack/utils/testutil.py +5 -4
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +11 -3
- localstack/utils/urls.py +1 -3
- localstack/version.py +2 -2
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
- localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
- localstack/packages/terraform.py +0 -46
- localstack/services/cloudformation/deploy.html +0 -144
- localstack/services/cloudformation/deploy_ui.py +0 -47
- localstack/services/cloudformation/plugins.py +0 -12
- localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
- {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"],
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
150
|
-
return
|
|
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
|
-
|
|
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
|
|
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(
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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)
|