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
@@ -0,0 +1,133 @@
1
+ import abc
2
+ import json
3
+
4
+ from moto.iam import iam_backends
5
+ from moto.iam.models import IAMBackend
6
+
7
+ from localstack.aws.api import RequestContext
8
+ from localstack.aws.api.iam import (
9
+ ActionNameType,
10
+ EvaluationResult,
11
+ PolicyEvaluationDecisionType,
12
+ ResourceNameType,
13
+ SimulatePolicyResponse,
14
+ SimulatePrincipalPolicyRequest,
15
+ )
16
+
17
+
18
+ class IAMPolicySimulator(abc.ABC):
19
+ @abc.abstractmethod
20
+ def simulate_principal_policy(
21
+ self, context: RequestContext, request: SimulatePrincipalPolicyRequest
22
+ ) -> SimulatePolicyResponse:
23
+ """
24
+ Simulate principal policy
25
+ :param request: SimulatePrincipalPolicyRequest
26
+ :param context: RequestContext
27
+ :return: SimulatePrincipalResponse
28
+ """
29
+ pass
30
+
31
+
32
+ class BasicIAMPolicySimulator(IAMPolicySimulator):
33
+ def simulate_principal_policy(
34
+ self,
35
+ context: RequestContext,
36
+ request: SimulatePrincipalPolicyRequest,
37
+ ) -> SimulatePolicyResponse:
38
+ backend = self.get_iam_backend(context)
39
+ policies = self.get_policies_from_principal(backend, request.get("PolicySourceArn"))
40
+
41
+ def _get_statements_from_policy_list(_policies: list[str]):
42
+ statements = []
43
+ for policy_str in _policies:
44
+ policy_dict = json.loads(policy_str)
45
+ if isinstance(policy_dict["Statement"], list):
46
+ statements.extend(policy_dict["Statement"])
47
+ else:
48
+ statements.append(policy_dict["Statement"])
49
+ return statements
50
+
51
+ policy_statements = _get_statements_from_policy_list(policies)
52
+
53
+ evaluations = [
54
+ self.build_evaluation_result(action_name, resource_arn, policy_statements)
55
+ for action_name in request.get("ActionNames")
56
+ for resource_arn in request.get("ResourceArns")
57
+ ]
58
+
59
+ response = SimulatePolicyResponse()
60
+ response["IsTruncated"] = False
61
+ response["EvaluationResults"] = evaluations
62
+
63
+ return response
64
+
65
+ @staticmethod
66
+ def build_evaluation_result(
67
+ action_name: ActionNameType, resource_name: ResourceNameType, policy_statements: list[dict]
68
+ ) -> EvaluationResult:
69
+ eval_res = EvaluationResult()
70
+ eval_res["EvalActionName"] = action_name
71
+ eval_res["EvalResourceName"] = resource_name
72
+ eval_res["EvalDecision"] = PolicyEvaluationDecisionType.explicitDeny
73
+ for statement in policy_statements:
74
+ # TODO Implement evaluation logic here
75
+ if (
76
+ action_name in statement["Action"]
77
+ and resource_name in statement["Resource"]
78
+ and statement["Effect"] == "Allow"
79
+ ):
80
+ eval_res["EvalDecision"] = PolicyEvaluationDecisionType.allowed
81
+ eval_res["MatchedStatements"] = [] # TODO: add support for statement compilation.
82
+ return eval_res
83
+
84
+ @staticmethod
85
+ def get_iam_backend(context: RequestContext) -> IAMBackend:
86
+ return iam_backends[context.account_id][context.partition]
87
+
88
+ @staticmethod
89
+ def get_policies_from_principal(backend: IAMBackend, principal_arn: str) -> list[dict]:
90
+ policies = []
91
+ if ":role" in principal_arn:
92
+ role_name = principal_arn.split("/")[-1]
93
+
94
+ policies.append(backend.get_role(role_name=role_name).assume_role_policy_document)
95
+
96
+ policy_names = backend.list_role_policies(role_name=role_name)
97
+ policies.extend(
98
+ [
99
+ backend.get_role_policy(role_name=role_name, policy_name=policy_name)[1]
100
+ for policy_name in policy_names
101
+ ]
102
+ )
103
+
104
+ attached_policies, _ = backend.list_attached_role_policies(role_name=role_name)
105
+ policies.extend([policy.document for policy in attached_policies])
106
+
107
+ if ":group" in principal_arn:
108
+ group_name = principal_arn.split("/")[-1]
109
+ policy_names = backend.list_group_policies(group_name=group_name)
110
+ policies.extend(
111
+ [
112
+ backend.get_group_policy(group_name=group_name, policy_name=policy_name)[1]
113
+ for policy_name in policy_names
114
+ ]
115
+ )
116
+
117
+ attached_policies, _ = backend.list_attached_group_policies(group_name=group_name)
118
+ policies.extend([policy.document for policy in attached_policies])
119
+
120
+ if ":user" in principal_arn:
121
+ user_name = principal_arn.split("/")[-1]
122
+ policy_names = backend.list_user_policies(user_name=user_name)
123
+ policies.extend(
124
+ [
125
+ backend.get_user_policy(user_name=user_name, policy_name=policy_name)[1]
126
+ for policy_name in policy_names
127
+ ]
128
+ )
129
+
130
+ attached_policies, _ = backend.list_attached_user_policies(user_name=user_name)
131
+ policies.extend([policy.document for policy in attached_policies])
132
+
133
+ return policies
@@ -1,7 +1,18 @@
1
1
  from collections import defaultdict
2
2
 
3
- from localstack.aws.api.kinesis import ConsumerDescription, MetricsName, StreamName
4
- from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
3
+ from localstack.aws.api.kinesis import (
4
+ ConsumerDescription,
5
+ MetricsName,
6
+ Policy,
7
+ ResourceARN,
8
+ StreamName,
9
+ )
10
+ from localstack.services.stores import (
11
+ AccountRegionBundle,
12
+ BaseStore,
13
+ CrossAccountAttribute,
14
+ LocalAttribute,
15
+ )
5
16
 
6
17
 
7
18
  class KinesisStore(BaseStore):
@@ -13,5 +24,7 @@ class KinesisStore(BaseStore):
13
24
  default=lambda: defaultdict(set)
14
25
  )
15
26
 
27
+ resource_policies: dict[ResourceARN, Policy] = CrossAccountAttribute(default=dict)
28
+
16
29
 
17
30
  kinesis_stores = AccountRegionBundle("kinesis", KinesisStore)
@@ -7,7 +7,7 @@ from localstack.packages import InstallTarget, Package
7
7
  from localstack.packages.core import GitHubReleaseInstaller, NodePackageInstaller
8
8
  from localstack.packages.java import JavaInstallerMixin, java_package
9
9
 
10
- _KINESIS_MOCK_VERSION = os.environ.get("KINESIS_MOCK_VERSION") or "0.4.13"
10
+ _KINESIS_MOCK_VERSION = os.environ.get("KINESIS_MOCK_VERSION") or "0.5.1"
11
11
 
12
12
 
13
13
  class KinesisMockEngine(StrEnum):
@@ -1,5 +1,7 @@
1
+ import json
1
2
  import logging
2
3
  import os
4
+ import re
3
5
  import time
4
6
  from random import random
5
7
 
@@ -8,14 +10,18 @@ from localstack.aws.api import RequestContext
8
10
  from localstack.aws.api.kinesis import (
9
11
  ConsumerARN,
10
12
  Data,
13
+ GetResourcePolicyOutput,
11
14
  HashKey,
12
15
  KinesisApi,
13
16
  PartitionKey,
17
+ Policy,
14
18
  ProvisionedThroughputExceededException,
15
19
  PutRecordOutput,
16
20
  PutRecordsOutput,
17
21
  PutRecordsRequestEntryList,
18
22
  PutRecordsResultEntry,
23
+ ResourceARN,
24
+ ResourceNotFoundException,
19
25
  SequenceNumber,
20
26
  ShardId,
21
27
  StartingPosition,
@@ -24,6 +30,7 @@ from localstack.aws.api.kinesis import (
24
30
  SubscribeToShardEvent,
25
31
  SubscribeToShardEventStream,
26
32
  SubscribeToShardOutput,
33
+ ValidationException,
27
34
  )
28
35
  from localstack.aws.connect import connect_to
29
36
  from localstack.constants import LOCALHOST
@@ -39,6 +46,13 @@ LOG = logging.getLogger(__name__)
39
46
  MAX_SUBSCRIPTION_SECONDS = 300
40
47
  SERVER_STARTUP_TIMEOUT = 120
41
48
 
49
+ DATA_STREAM_ARN_REGEX = re.compile(
50
+ r"^arn:aws(?:-[a-z]+)*:kinesis:[a-z0-9-]+:\d{12}:stream\/[a-zA-Z0-9_.\-]+$"
51
+ )
52
+ CONSUMER_ARN_REGEX = re.compile(
53
+ r"^arn:aws(?:-[a-z]+)*:kinesis:[a-z0-9-]+:\d{12}:stream\/[a-zA-Z0-9_.\-]+\/consumer\/[a-zA-Z0-9_.\-]+:\d+$"
54
+ )
55
+
42
56
 
43
57
  def find_stream_for_consumer(consumer_arn):
44
58
  account_id = extract_account_id_from_arn(consumer_arn)
@@ -52,6 +66,11 @@ def find_stream_for_consumer(consumer_arn):
52
66
  raise Exception(f"Unable to find stream for stream consumer {consumer_arn}")
53
67
 
54
68
 
69
+ def is_valid_kinesis_arn(resource_arn: ResourceARN) -> bool:
70
+ """Check if the provided ARN is a valid Kinesis ARN."""
71
+ return bool(CONSUMER_ARN_REGEX.match(resource_arn) or DATA_STREAM_ARN_REGEX.match(resource_arn))
72
+
73
+
55
74
  class KinesisProvider(KinesisApi, ServiceLifecycleHook):
56
75
  server_manager: KinesisServerManager
57
76
 
@@ -81,6 +100,64 @@ class KinesisProvider(KinesisApi, ServiceLifecycleHook):
81
100
  def get_store(account_id: str, region_name: str) -> KinesisStore:
82
101
  return kinesis_stores[account_id][region_name]
83
102
 
103
+ def put_resource_policy(
104
+ self,
105
+ context: RequestContext,
106
+ resource_arn: ResourceARN,
107
+ policy: Policy,
108
+ **kwargs,
109
+ ) -> None:
110
+ if not is_valid_kinesis_arn(resource_arn):
111
+ raise ValidationException(f"invalid kinesis arn {resource_arn}")
112
+
113
+ kinesis = connect_to(
114
+ aws_access_key_id=context.account_id, region_name=context.region
115
+ ).kinesis
116
+ try:
117
+ kinesis.describe_stream_summary(StreamARN=resource_arn)
118
+ except kinesis.exceptions.ResourceNotFoundException:
119
+ raise ResourceNotFoundException(f"Stream with ARN {resource_arn} not found")
120
+
121
+ store = self.get_store(context.account_id, context.region)
122
+ store.resource_policies[resource_arn] = policy
123
+
124
+ def get_resource_policy(
125
+ self,
126
+ context: RequestContext,
127
+ resource_arn: ResourceARN,
128
+ **kwargs,
129
+ ) -> GetResourcePolicyOutput:
130
+ if not is_valid_kinesis_arn(resource_arn):
131
+ raise ValidationException(f"invalid kinesis arn {resource_arn}")
132
+
133
+ kinesis = connect_to(
134
+ aws_access_key_id=context.account_id, region_name=context.region
135
+ ).kinesis
136
+ try:
137
+ kinesis.describe_stream_summary(StreamARN=resource_arn)
138
+ except kinesis.exceptions.ResourceNotFoundException:
139
+ raise ResourceNotFoundException(f"Stream with ARN {resource_arn} not found")
140
+
141
+ store = self.get_store(context.account_id, context.region)
142
+ policy = store.resource_policies.get(resource_arn, json.dumps({}))
143
+ return GetResourcePolicyOutput(Policy=policy)
144
+
145
+ def delete_resource_policy(
146
+ self,
147
+ context: RequestContext,
148
+ resource_arn: ResourceARN,
149
+ **kwargs,
150
+ ) -> None:
151
+ if not is_valid_kinesis_arn(resource_arn):
152
+ raise ValidationException(f"invalid kinesis arn {resource_arn}")
153
+
154
+ store = self.get_store(context.account_id, context.region)
155
+ if resource_arn not in store.resource_policies:
156
+ raise ResourceNotFoundException(
157
+ f"No resource policy found for resource ARN {resource_arn}"
158
+ )
159
+ del store.resource_policies[resource_arn]
160
+
84
161
  def subscribe_to_shard(
85
162
  self,
86
163
  context: RequestContext,
@@ -173,6 +173,7 @@ class KmsCryptoKey:
173
173
  public_key: bytes | None
174
174
  private_key: bytes | None
175
175
  key_material: bytes
176
+ pending_key_material: bytes | None
176
177
  key_spec: str
177
178
 
178
179
  @staticmethod
@@ -217,6 +218,7 @@ class KmsCryptoKey:
217
218
  def __init__(self, key_spec: str, key_material: bytes | None = None):
218
219
  self.private_key = None
219
220
  self.public_key = None
221
+ self.pending_key_material = None
220
222
  # Technically, key_material, being a symmetric encryption key, is only relevant for
221
223
  # key_spec == SYMMETRIC_DEFAULT.
222
224
  # But LocalStack uses symmetric encryption with this key_material even for other specs. Asymmetric keys are
@@ -248,8 +250,9 @@ class KmsCryptoKey:
248
250
  self._serialize_key(key)
249
251
 
250
252
  def load_key_material(self, material: bytes):
251
- if self.key_spec in [
252
- KeySpec.SYMMETRIC_DEFAULT,
253
+ if self.key_spec == KeySpec.SYMMETRIC_DEFAULT:
254
+ self.pending_key_material = material
255
+ elif self.key_spec in [
253
256
  KeySpec.HMAC_224,
254
257
  KeySpec.HMAC_256,
255
258
  KeySpec.HMAC_384,
@@ -323,9 +326,28 @@ class KmsKey:
323
326
  # remove the _custom_key_material_ tag from the tags to not readily expose the custom key material
324
327
  del self.tags[TAG_KEY_CUSTOM_KEY_MATERIAL]
325
328
  self.crypto_key = KmsCryptoKey(self.metadata.get("KeySpec"), custom_key_material)
329
+ self._internal_key_id = uuid.uuid4()
330
+
331
+ # The KMS implementation always provides a crypto key with key material which doesn't suit scenarios where a
332
+ # KMS Key may have no key material e.g. for external keys. Don't expose the CurrentKeyMaterialId in those cases.
333
+ if custom_key_material or (
334
+ self.metadata["Origin"] == "AWS_KMS"
335
+ and self.metadata["KeySpec"] == KeySpec.SYMMETRIC_DEFAULT
336
+ ):
337
+ self.metadata["CurrentKeyMaterialId"] = self.generate_key_material_id(
338
+ self.crypto_key.key_material
339
+ )
340
+
326
341
  self.rotation_period_in_days = 365
327
342
  self.next_rotation_date = None
328
343
 
344
+ def generate_key_material_id(self, key_material: bytes) -> str:
345
+ # The KeyMaterialId depends on the key material and the KeyId. Use an internal ID to prevent brute forcing
346
+ # the value of the key material from the public KeyId and KeyMaterialId.
347
+ # https://docs.aws.amazon.com/kms/latest/APIReference/API_ImportKeyMaterial.html
348
+ key_material_id_hex = uuid.uuid5(self._internal_key_id, key_material).hex
349
+ return str(key_material_id_hex) * 2
350
+
329
351
  def calculate_and_set_arn(self, account_id, region):
330
352
  self.metadata["Arn"] = kms_key_arn(self.metadata.get("KeyId"), account_id, region)
331
353
 
@@ -746,8 +768,16 @@ class KmsKey:
746
768
  f"The on-demand rotations limit has been reached for the given keyId. "
747
769
  f"No more on-demand rotations can be performed for this key: {self.metadata['Arn']}"
748
770
  )
749
- self.previous_keys.append(self.crypto_key.key_material)
750
- self.crypto_key = KmsCryptoKey(KeySpec.SYMMETRIC_DEFAULT)
771
+ current_key_material = self.crypto_key.key_material
772
+ pending_key_material = self.crypto_key.pending_key_material
773
+
774
+ self.previous_keys.append(current_key_material)
775
+
776
+ # If there is no pending material stored on the key, then key material will be generated.
777
+ self.crypto_key = KmsCryptoKey(KeySpec.SYMMETRIC_DEFAULT, pending_key_material)
778
+ self.metadata["CurrentKeyMaterialId"] = self.generate_key_material_id(
779
+ self.crypto_key.key_material
780
+ )
751
781
 
752
782
 
753
783
  class KmsGrant:
@@ -4,10 +4,13 @@ import datetime
4
4
  import logging
5
5
  import os
6
6
 
7
+ from cbor2 import loads as cbor2_loads
7
8
  from cryptography.exceptions import InvalidTag
8
9
  from cryptography.hazmat.backends import default_backend
9
10
  from cryptography.hazmat.primitives import hashes, keywrap
10
11
  from cryptography.hazmat.primitives.asymmetric import padding
12
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
13
+ from cryptography.hazmat.primitives.serialization import load_der_public_key
11
14
 
12
15
  from localstack.aws.api import CommonServiceException, RequestContext, handler
13
16
  from localstack.aws.api.kms import (
@@ -85,6 +88,7 @@ from localstack.aws.api.kms import (
85
88
  MacAlgorithmSpec,
86
89
  MarkerType,
87
90
  MultiRegionKey,
91
+ MultiRegionKeyType,
88
92
  NotFoundException,
89
93
  NullableBooleanType,
90
94
  OriginType,
@@ -137,6 +141,7 @@ from localstack.services.plugins import ServiceLifecycleHook
137
141
  from localstack.utils.aws.arns import get_partition, kms_alias_arn, parse_arn
138
142
  from localstack.utils.collections import PaginatedList
139
143
  from localstack.utils.common import select_attributes
144
+ from localstack.utils.crypto import pkcs7_envelope_encrypt
140
145
  from localstack.utils.strings import short_uid, to_bytes, to_str
141
146
 
142
147
  LOG = logging.getLogger(__name__)
@@ -490,26 +495,39 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
490
495
  self, context: RequestContext, request: ReplicateKeyRequest
491
496
  ) -> ReplicateKeyResponse:
492
497
  account_id = context.account_id
493
- key = self._get_kms_key(account_id, context.region, request.get("KeyId"))
494
- key_id = key.metadata.get("KeyId")
495
- if not key.metadata.get("MultiRegion"):
498
+ primary_key = self._get_kms_key(account_id, context.region, request.get("KeyId"))
499
+ key_id = primary_key.metadata.get("KeyId")
500
+ key_arn = primary_key.metadata.get("Arn")
501
+ if not primary_key.metadata.get("MultiRegion"):
496
502
  raise UnsupportedOperationException(
497
503
  f"Unable to replicate a non-MultiRegion key {key_id}"
498
504
  )
499
505
  replica_region = request.get("ReplicaRegion")
500
506
  replicate_to_store = kms_stores[account_id][replica_region]
507
+
508
+ if (
509
+ primary_key.metadata.get("MultiRegionConfiguration", {}).get("MultiRegionKeyType")
510
+ != MultiRegionKeyType.PRIMARY
511
+ ):
512
+ raise UnsupportedOperationException(f"{key_arn} is not a multi-region primary key.")
513
+
501
514
  if key_id in replicate_to_store.keys:
502
515
  raise AlreadyExistsException(
503
516
  f"Unable to replicate key {key_id} to region {replica_region}, as the key "
504
517
  f"already exist there"
505
518
  )
506
- replica_key = copy.deepcopy(key)
519
+ replica_key = copy.deepcopy(primary_key)
507
520
  replica_key.replicate_metadata(request, account_id, replica_region)
508
521
  replicate_to_store.keys[key_id] = replica_key
509
522
 
510
- self.update_primary_key_with_replica_keys(key, replica_key, replica_region)
523
+ self.update_primary_key_with_replica_keys(primary_key, replica_key, replica_region)
511
524
 
512
- return ReplicateKeyResponse(ReplicaKeyMetadata=replica_key.metadata)
525
+ # CurrentKeyMaterialId is not returned in the ReplicaKeyMetadata. May be due to not being evaluated until
526
+ # the key has been successfully replicated as it does not show up in DescribeKey immediately either.
527
+ replica_key_metadata_response = copy.deepcopy(replica_key.metadata)
528
+ replica_key_metadata_response.pop("CurrentKeyMaterialId", None)
529
+
530
+ return ReplicateKeyResponse(ReplicaKeyMetadata=replica_key_metadata_response)
513
531
 
514
532
  @staticmethod
515
533
  # Adds new multi region replica key to the primary key's metadata.
@@ -1066,6 +1084,25 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1066
1084
  self._validate_key_for_encryption_decryption(context, key)
1067
1085
  self._validate_key_state_not_pending_import(key)
1068
1086
 
1087
+ # Handle the recipient field. This is used by AWS Nitro to re-encrypt the plaintext to the key specified
1088
+ # by the enclave. Proper support for this will take significant work to figure out how to model enforcing
1089
+ # the attestation measurements; for now, if recipient is specified and has an attestation doc in it including
1090
+ # a public key where it's expected to be, we encrypt to that public key. This at least allows users to use
1091
+ # localstack as a drop-in replacement for AWS when testing without having to skip the secondary decryption
1092
+ # when using localstack.
1093
+ recipient_pubkey = None
1094
+ if recipient:
1095
+ attestation_document = recipient["AttestationDocument"]
1096
+ # We do all of this in a try/catch and warn if it fails so that if users are currently passing a nonsense
1097
+ # value we don't break it for them. In the future we could do a breaking change to require a valid attestation
1098
+ # (or at least one that contains the public key).
1099
+ try:
1100
+ recipient_pubkey = self._extract_attestation_pubkey(attestation_document)
1101
+ except Exception as e:
1102
+ logging.warning(
1103
+ "Unable to extract public key from non-empty attestation document: %s", e
1104
+ )
1105
+
1069
1106
  try:
1070
1107
  # TODO: Extend the implementation to handle additional encryption/decryption scenarios
1071
1108
  # beyond the current support for offline encryption and online decryption using RSA keys if key id exists in
@@ -1079,20 +1116,27 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1079
1116
  plaintext = key.decrypt(ciphertext, encryption_context)
1080
1117
  except InvalidTag:
1081
1118
  raise InvalidCiphertextException()
1119
+
1082
1120
  # For compatibility, we return EncryptionAlgorithm values expected from AWS. But LocalStack currently always
1083
1121
  # encrypts with symmetric encryption no matter the key settings.
1084
1122
  #
1085
1123
  # We return a key ARN instead of KeyId despite the name of the parameter, as this is what AWS does and states
1086
1124
  # in its docs.
1087
- # TODO add support for "recipient"
1088
1125
  # https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html#API_Decrypt_RequestSyntax
1089
1126
  # TODO add support for "dry_run"
1090
- return DecryptResponse(
1127
+ response = DecryptResponse(
1091
1128
  KeyId=key.metadata.get("Arn"),
1092
- Plaintext=plaintext,
1093
1129
  EncryptionAlgorithm=encryption_algorithm,
1094
1130
  )
1095
1131
 
1132
+ # Encrypt to the recipient pubkey if specified. Otherwise, return the actual plaintext
1133
+ if recipient_pubkey:
1134
+ response["CiphertextForRecipient"] = pkcs7_envelope_encrypt(plaintext, recipient_pubkey)
1135
+ else:
1136
+ response["Plaintext"] = plaintext
1137
+
1138
+ return response
1139
+
1096
1140
  def get_parameters_for_import(
1097
1141
  self,
1098
1142
  context: RequestContext,
@@ -1167,13 +1211,10 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1167
1211
  # TODO check if there was already a key imported for this kms key
1168
1212
  # if so, it has to be identical. We cannot change keys by reimporting after deletion/expiry
1169
1213
  key_material = self._decrypt_wrapped_key_material(import_state, encrypted_key_material)
1170
-
1171
- if expiration_model:
1172
- key_to_import_material_to.metadata["ExpirationModel"] = expiration_model
1173
- else:
1174
- key_to_import_material_to.metadata["ExpirationModel"] = (
1175
- ExpirationModelType.KEY_MATERIAL_EXPIRES
1176
- )
1214
+ key_material_id = key_to_import_material_to.generate_key_material_id(key_material)
1215
+ key_to_import_material_to.metadata["ExpirationModel"] = (
1216
+ expiration_model or ExpirationModelType.KEY_MATERIAL_EXPIRES
1217
+ )
1177
1218
  if (
1178
1219
  key_to_import_material_to.metadata["ExpirationModel"]
1179
1220
  == ExpirationModelType.KEY_MATERIAL_EXPIRES
@@ -1182,12 +1223,42 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1182
1223
  raise ValidationException(
1183
1224
  "A validTo date must be set if the ExpirationModel is KEY_MATERIAL_EXPIRES"
1184
1225
  )
1226
+ if existing_pending_material := key_to_import_material_to.crypto_key.pending_key_material:
1227
+ pending_key_material_id = key_to_import_material_to.generate_key_material_id(
1228
+ existing_pending_material
1229
+ )
1230
+ raise KMSInvalidStateException(
1231
+ f"New key material (id: {key_material_id}) cannot be imported into KMS key "
1232
+ f"{key_to_import_material_to.metadata['Arn']}, because another key material "
1233
+ f"(id: {pending_key_material_id}) is pending rotation."
1234
+ )
1235
+
1185
1236
  # TODO actually set validTo and make the key expire
1186
1237
  key_to_import_material_to.metadata["Enabled"] = True
1187
1238
  key_to_import_material_to.metadata["KeyState"] = KeyState.Enabled
1188
1239
  key_to_import_material_to.crypto_key.load_key_material(key_material)
1189
1240
 
1190
- return ImportKeyMaterialResponse()
1241
+ # KeyMaterialId / CurrentKeyMaterialId is only exposed for symmetric encryption keys.
1242
+ key_material_id_response = None
1243
+ if key_to_import_material_to.metadata["KeySpec"] == KeySpec.SYMMETRIC_DEFAULT:
1244
+ key_material_id_response = key_to_import_material_to.generate_key_material_id(
1245
+ key_material
1246
+ )
1247
+
1248
+ # If there is no CurrentKeyMaterialId, instantly promote the pending key material to the current.
1249
+ if key_to_import_material_to.metadata.get("CurrentKeyMaterialId") is None:
1250
+ key_to_import_material_to.metadata["CurrentKeyMaterialId"] = (
1251
+ key_material_id_response
1252
+ )
1253
+ key_to_import_material_to.crypto_key.key_material = (
1254
+ key_to_import_material_to.crypto_key.pending_key_material
1255
+ )
1256
+ key_to_import_material_to.crypto_key.pending_key_material = None
1257
+
1258
+ return ImportKeyMaterialResponse(
1259
+ KeyId=key_to_import_material_to.metadata["Arn"],
1260
+ KeyMaterialId=key_material_id_response,
1261
+ )
1191
1262
 
1192
1263
  def delete_imported_key_material(
1193
1264
  self,
@@ -1314,7 +1385,7 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1314
1385
  key = self._get_kms_key(account_id, region_name, key_id, any_key_state_allowed=True)
1315
1386
 
1316
1387
  response = GetKeyRotationStatusResponse(
1317
- KeyId=key_id,
1388
+ KeyId=key.metadata["Arn"],
1318
1389
  KeyRotationEnabled=key.is_key_rotation_enabled,
1319
1390
  NextRotationDate=key.next_rotation_date,
1320
1391
  )
@@ -1406,13 +1477,13 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1406
1477
 
1407
1478
  if key.metadata["KeySpec"] != KeySpec.SYMMETRIC_DEFAULT:
1408
1479
  raise UnsupportedOperationException()
1409
- if key.metadata["Origin"] == OriginType.EXTERNAL:
1410
- raise NotImplementedError("Rotation of imported keys is not supported yet.")
1480
+ self._validate_key_state_not_pending_import(key)
1481
+ self._validate_external_key_has_pending_material(key)
1411
1482
 
1412
1483
  key.rotate_key_on_demand()
1413
1484
 
1414
1485
  return RotateKeyOnDemandResponse(
1415
- KeyId=key_id,
1486
+ KeyId=key.metadata["Arn"],
1416
1487
  )
1417
1488
 
1418
1489
  @handler("TagResource", expand=False)
@@ -1489,6 +1560,12 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1489
1560
  if key.metadata["KeyState"] == KeyState.PendingImport:
1490
1561
  raise KMSInvalidStateException(f"{key.metadata['Arn']} is pending import.")
1491
1562
 
1563
+ def _validate_external_key_has_pending_material(self, key: KmsKey):
1564
+ if key.metadata["Origin"] == "EXTERNAL" and key.crypto_key.pending_key_material is None:
1565
+ raise KMSInvalidStateException(
1566
+ f"No available key material pending rotation for the key: {key.metadata['Arn']}."
1567
+ )
1568
+
1492
1569
  def _validate_key_for_encryption_decryption(self, context: RequestContext, key: KmsKey):
1493
1570
  key_usage = key.metadata["KeyUsage"]
1494
1571
  if key_usage != "ENCRYPT_DECRYPT":
@@ -1550,6 +1627,15 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1550
1627
  f" constraint: [Member must satisfy enum value set: {VALID_OPERATIONS}]"
1551
1628
  )
1552
1629
 
1630
+ def _extract_attestation_pubkey(self, attestation_document: bytes) -> RSAPublicKey:
1631
+ # The attestation document comes as a COSE (CBOR Object Signing and Encryption) object: the CBOR
1632
+ # attestation is signed and then the attestation and signature are again CBOR-encoded. For now
1633
+ # we don't bother validating the signature, though in the future we could.
1634
+ cose_document = cbor2_loads(attestation_document)
1635
+ attestation = cbor2_loads(cose_document[2])
1636
+ public_key_bytes = attestation["public_key"]
1637
+ return load_der_public_key(public_key_bytes)
1638
+
1553
1639
  def _decrypt_wrapped_key_material(
1554
1640
  self,
1555
1641
  import_state: KeyImportState,
@@ -722,7 +722,9 @@ def validate_layer_runtimes_and_architectures(
722
722
  validations.append(validation_msg)
723
723
 
724
724
  if compatible_architectures and set(compatible_architectures).difference(ARCHITECTURES):
725
- constraint = "[Member must satisfy enum value set: [x86_64, arm64]]"
725
+ constraint = (
726
+ "[Member must satisfy enum value set: [x86_64, arm64], Member must not be null]"
727
+ )
726
728
  validation_msg = f"Value '[{', '.join(list(compatible_architectures))}]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: {constraint}"
727
729
  validations.append(validation_msg)
728
730
 
@@ -19,13 +19,9 @@ from localstack.aws.api.sqs import (
19
19
  String,
20
20
  TagMap,
21
21
  )
22
- from localstack.services.sqs.models import SqsQueue, StandardQueue
23
- from localstack.services.sqs.provider import (
24
- QueueUpdateWorker,
25
- _create_message_attribute_hash,
26
- to_sqs_api_message,
27
- )
28
- from localstack.services.sqs.utils import generate_message_id
22
+ from localstack.services.sqs.models import SqsQueue, StandardQueue, to_sqs_api_message
23
+ from localstack.services.sqs.provider import QueueUpdateWorker
24
+ from localstack.services.sqs.utils import create_message_attribute_hash, generate_message_id
29
25
  from localstack.utils.objects import singleton_factory
30
26
  from localstack.utils.strings import md5
31
27
  from localstack.utils.time import now
@@ -189,7 +185,7 @@ class FakeSqsClient:
189
185
  MD5OfBody=md5(MessageBody),
190
186
  Body=MessageBody,
191
187
  Attributes=self._create_message_attributes(MessageSystemAttributes),
192
- MD5OfMessageAttributes=_create_message_attribute_hash(MessageAttributes),
188
+ MD5OfMessageAttributes=create_message_attribute_hash(MessageAttributes),
193
189
  MessageAttributes=MessageAttributes,
194
190
  )
195
191
  queue_item = queue.put(
@@ -204,7 +200,7 @@ class FakeSqsClient:
204
200
  "MD5OfMessageBody": message["MD5OfBody"],
205
201
  "MD5OfMessageAttributes": message.get("MD5OfMessageAttributes"),
206
202
  "SequenceNumber": queue_item.sequence_number,
207
- "MD5OfMessageSystemAttributes": _create_message_attribute_hash(MessageSystemAttributes),
203
+ "MD5OfMessageSystemAttributes": create_message_attribute_hash(MessageSystemAttributes),
208
204
  }
209
205
 
210
206
 
@@ -12,7 +12,7 @@ from localstack.utils.platform import get_arch
12
12
  """Customized LocalStack version of the AWS Lambda Runtime Interface Emulator (RIE).
13
13
  https://github.com/localstack/lambda-runtime-init/blob/localstack/README-LOCALSTACK.md
14
14
  """
15
- LAMBDA_RUNTIME_DEFAULT_VERSION = "v0.1.35-pre"
15
+ LAMBDA_RUNTIME_DEFAULT_VERSION = "v0.1.37-pre"
16
16
  LAMBDA_RUNTIME_VERSION = config.LAMBDA_INIT_RELEASE_VERSION or LAMBDA_RUNTIME_DEFAULT_VERSION
17
17
  LAMBDA_RUNTIME_INIT_URL = "https://github.com/localstack/lambda-runtime-init/releases/download/{version}/aws-lambda-rie-{arch}"
18
18