localstack-core 4.10.1.dev7__py3-none-any.whl → 4.11.2.dev14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +604 -561
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1201 -969
  5. localstack/aws/api/cloudwatch/__init__.py +375 -375
  6. localstack/aws/api/config/__init__.py +784 -786
  7. localstack/aws/api/dynamodb/__init__.py +753 -759
  8. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  9. localstack/aws/api/ec2/__init__.py +10062 -8826
  10. localstack/aws/api/es/__init__.py +453 -453
  11. localstack/aws/api/events/__init__.py +552 -552
  12. localstack/aws/api/firehose/__init__.py +541 -543
  13. localstack/aws/api/iam/__init__.py +866 -572
  14. localstack/aws/api/kinesis/__init__.py +235 -147
  15. localstack/aws/api/kms/__init__.py +341 -336
  16. localstack/aws/api/lambda_/__init__.py +974 -621
  17. localstack/aws/api/logs/__init__.py +988 -675
  18. localstack/aws/api/opensearch/__init__.py +903 -785
  19. localstack/aws/api/pipes/__init__.py +336 -336
  20. localstack/aws/api/redshift/__init__.py +1257 -1166
  21. localstack/aws/api/resource_groups/__init__.py +175 -175
  22. localstack/aws/api/resourcegroupstaggingapi/__init__.py +103 -67
  23. localstack/aws/api/route53/__init__.py +296 -254
  24. localstack/aws/api/route53resolver/__init__.py +397 -396
  25. localstack/aws/api/s3/__init__.py +1412 -1349
  26. localstack/aws/api/s3control/__init__.py +594 -594
  27. localstack/aws/api/scheduler/__init__.py +118 -118
  28. localstack/aws/api/secretsmanager/__init__.py +221 -216
  29. localstack/aws/api/ses/__init__.py +227 -227
  30. localstack/aws/api/sns/__init__.py +115 -115
  31. localstack/aws/api/sqs/__init__.py +100 -100
  32. localstack/aws/api/ssm/__init__.py +1977 -1971
  33. localstack/aws/api/stepfunctions/__init__.py +375 -333
  34. localstack/aws/api/sts/__init__.py +142 -66
  35. localstack/aws/api/support/__init__.py +112 -112
  36. localstack/aws/api/swf/__init__.py +378 -386
  37. localstack/aws/api/transcribe/__init__.py +425 -425
  38. localstack/aws/handlers/logging.py +8 -4
  39. localstack/aws/handlers/service.py +22 -3
  40. localstack/aws/protocol/parser.py +1 -1
  41. localstack/aws/protocol/serializer.py +1 -1
  42. localstack/aws/scaffold.py +15 -17
  43. localstack/cli/localstack.py +6 -1
  44. localstack/deprecations.py +0 -6
  45. localstack/dev/kubernetes/__main__.py +38 -3
  46. localstack/services/acm/provider.py +4 -0
  47. localstack/services/apigateway/helpers.py +5 -9
  48. localstack/services/apigateway/legacy/provider.py +60 -24
  49. localstack/services/apigateway/patches.py +0 -9
  50. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
  52. localstack/services/cloudformation/provider.py +2 -2
  53. localstack/services/cloudformation/v2/provider.py +6 -6
  54. localstack/services/cloudwatch/provider.py +10 -3
  55. localstack/services/cloudwatch/provider_v2.py +6 -3
  56. localstack/services/configservice/provider.py +5 -1
  57. localstack/services/dynamodb/provider.py +1 -0
  58. localstack/services/dynamodb/v2/provider.py +1 -0
  59. localstack/services/dynamodbstreams/provider.py +6 -0
  60. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  61. localstack/services/ec2/provider.py +6 -0
  62. localstack/services/es/provider.py +6 -0
  63. localstack/services/events/provider.py +4 -0
  64. localstack/services/events/v1/provider.py +9 -0
  65. localstack/services/firehose/provider.py +5 -0
  66. localstack/services/iam/provider.py +4 -0
  67. localstack/services/kinesis/packages.py +1 -1
  68. localstack/services/kms/models.py +44 -24
  69. localstack/services/kms/provider.py +97 -16
  70. localstack/services/lambda_/api_utils.py +40 -21
  71. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  72. localstack/services/lambda_/invocation/assignment.py +4 -1
  73. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  74. localstack/services/lambda_/invocation/lambda_models.py +27 -2
  75. localstack/services/lambda_/invocation/lambda_service.py +51 -3
  76. localstack/services/lambda_/invocation/models.py +9 -1
  77. localstack/services/lambda_/invocation/version_manager.py +18 -3
  78. localstack/services/lambda_/packages.py +1 -1
  79. localstack/services/lambda_/provider.py +240 -96
  80. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  81. localstack/services/lambda_/runtimes.py +10 -3
  82. localstack/services/logs/provider.py +45 -19
  83. localstack/services/opensearch/provider.py +53 -3
  84. localstack/services/resource_groups/provider.py +5 -1
  85. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  86. localstack/services/s3/provider.py +29 -16
  87. localstack/services/s3/utils.py +35 -14
  88. localstack/services/s3control/provider.py +101 -2
  89. localstack/services/s3control/validation.py +50 -0
  90. localstack/services/sns/constants.py +3 -1
  91. localstack/services/sns/publisher.py +15 -6
  92. localstack/services/sns/v2/models.py +30 -1
  93. localstack/services/sns/v2/provider.py +794 -31
  94. localstack/services/sns/v2/utils.py +20 -0
  95. localstack/services/sqs/models.py +37 -10
  96. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  101. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  102. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  103. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  107. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  108. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  109. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  110. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  111. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  112. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  113. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  114. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  115. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  116. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  117. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +170 -22
  118. localstack/services/stepfunctions/backend/execution.py +6 -6
  119. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  120. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  121. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  122. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  123. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  124. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  125. localstack/services/stepfunctions/provider.py +78 -27
  126. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  127. localstack/testing/pytest/fixtures.py +28 -0
  128. localstack/testing/snapshots/transformer_utility.py +7 -0
  129. localstack/testing/testselection/matching.py +0 -1
  130. localstack/utils/analytics/publisher.py +37 -155
  131. localstack/utils/analytics/service_request_aggregator.py +6 -4
  132. localstack/utils/aws/arns.py +7 -0
  133. localstack/utils/aws/client_types.py +0 -8
  134. localstack/utils/batching.py +258 -0
  135. localstack/utils/catalog/catalog_loader.py +111 -3
  136. localstack/utils/collections.py +23 -11
  137. localstack/utils/crypto.py +109 -0
  138. localstack/version.py +2 -2
  139. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +7 -6
  140. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +149 -141
  141. localstack_core-4.11.2.dev14.dist-info/plux.json +1 -0
  142. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  143. localstack/utils/batch_policy.py +0 -124
  144. localstack_core-4.10.1.dev7.dist-info/plux.json +0 -1
  145. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  146. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
  147. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
  148. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
  149. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
  150. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
  151. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
  152. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/top_level.txt +0 -0
@@ -135,15 +135,15 @@ SSM_PARAMETER_TYPE_RE = re.compile(
135
135
 
136
136
 
137
137
  def is_stack_arn(stack_name_or_id: str) -> bool:
138
- return ARN_STACK_REGEX.match(stack_name_or_id) is not None
138
+ return stack_name_or_id and ARN_STACK_REGEX.match(stack_name_or_id) is not None
139
139
 
140
140
 
141
141
  def is_changeset_arn(change_set_name_or_id: str) -> bool:
142
- return ARN_CHANGESET_REGEX.match(change_set_name_or_id) is not None
142
+ return change_set_name_or_id and ARN_CHANGESET_REGEX.match(change_set_name_or_id) is not None
143
143
 
144
144
 
145
145
  def is_stack_set_arn(stack_set_name_or_id: str) -> bool:
146
- return ARN_STACK_SET_REGEX.match(stack_set_name_or_id) is not None
146
+ return stack_set_name_or_id and ARN_STACK_SET_REGEX.match(stack_set_name_or_id) is not None
147
147
 
148
148
 
149
149
  class StackNotFoundError(ValidationError):
@@ -1349,8 +1349,8 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
1349
1349
  def describe_stack_events(
1350
1350
  self,
1351
1351
  context: RequestContext,
1352
- stack_name: StackName = None,
1353
- next_token: NextToken = None,
1352
+ stack_name: StackName,
1353
+ next_token: NextToken | None = None,
1354
1354
  **kwargs,
1355
1355
  ) -> DescribeStackEventsOutput:
1356
1356
  if not stack_name:
@@ -1388,7 +1388,7 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
1388
1388
  stack_name, message_override=f"Stack with id {stack_name} does not exist"
1389
1389
  )
1390
1390
  else:
1391
- raise StackNotFoundError(stack_name)
1391
+ raise ValidationError("StackName is required if ChangeSetName is not specified.")
1392
1392
 
1393
1393
  if template_stage == TemplateStage.Processed and "Transform" in stack.template_body:
1394
1394
  template_body = json.dumps(stack.processed_template)
@@ -35,6 +35,7 @@ from localstack.services import moto
35
35
  from localstack.services.cloudwatch.alarm_scheduler import AlarmScheduler
36
36
  from localstack.services.edge import ROUTER
37
37
  from localstack.services.plugins import SERVICE_PLUGINS, ServiceLifecycleHook
38
+ from localstack.state import StateVisitor
38
39
  from localstack.utils.aws import arns
39
40
  from localstack.utils.aws.arns import extract_account_id_from_arn, lambda_function_name
40
41
  from localstack.utils.aws.request_context import (
@@ -306,8 +307,13 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
306
307
  self.tags = TaggingService()
307
308
  self.alarm_scheduler = None
308
309
 
310
+ def accept_state_visitor(self, visitor: StateVisitor):
311
+ visitor.visit(cloudwatch_backends)
312
+
309
313
  def on_after_init(self):
310
314
  ROUTER.add(PATH_GET_RAW_METRICS, self.get_raw_metrics)
315
+
316
+ def on_before_start(self):
311
317
  self.start_alarm_scheduler()
312
318
 
313
319
  def on_before_state_reset(self):
@@ -337,9 +343,10 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
337
343
  self.alarm_scheduler = AlarmScheduler()
338
344
 
339
345
  def shutdown_alarm_scheduler(self):
340
- LOG.debug("stopping cloudwatch scheduler")
341
- self.alarm_scheduler.shutdown_scheduler()
342
- self.alarm_scheduler = None
346
+ if self.alarm_scheduler:
347
+ LOG.debug("stopping cloudwatch scheduler")
348
+ self.alarm_scheduler.shutdown_scheduler()
349
+ self.alarm_scheduler = None
343
350
 
344
351
  def delete_alarms(self, context: RequestContext, alarm_names: AlarmNames, **kwargs) -> None:
345
352
  moto.call_moto(context)
@@ -161,6 +161,8 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
161
161
 
162
162
  def on_after_init(self):
163
163
  ROUTER.add(PATH_GET_RAW_METRICS, self.get_raw_metrics)
164
+
165
+ def on_before_start(self):
164
166
  self.start_alarm_scheduler()
165
167
 
166
168
  def on_before_state_reset(self):
@@ -192,9 +194,10 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
192
194
  self.alarm_scheduler = AlarmScheduler()
193
195
 
194
196
  def shutdown_alarm_scheduler(self):
195
- LOG.debug("stopping cloudwatch scheduler")
196
- self.alarm_scheduler.shutdown_scheduler()
197
- self.alarm_scheduler = None
197
+ if self.alarm_scheduler:
198
+ LOG.debug("stopping cloudwatch scheduler")
199
+ self.alarm_scheduler.shutdown_scheduler()
200
+ self.alarm_scheduler = None
198
201
 
199
202
  def delete_alarms(self, context: RequestContext, alarm_names: AlarmNames, **kwargs) -> None:
200
203
  """
@@ -1,5 +1,9 @@
1
1
  from localstack.aws.api.config import ConfigApi
2
+ from localstack.state import StateVisitor
2
3
 
3
4
 
4
5
  class ConfigProvider(ConfigApi):
5
- pass
6
+ def accept_state_visitor(self, visitor: StateVisitor):
7
+ from moto.config.models import config_backends
8
+
9
+ visitor.visit(config_backends)
@@ -535,6 +535,7 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
535
535
  self.server = self._new_dynamodb_server()
536
536
  self._expired_items_worker = ExpiredItemsWorker()
537
537
  self._router_rules = []
538
+ # TODO: fix _event_forwarder to have lazy instantiation of the ThreadPoolExecutor
538
539
  self._event_forwarder = EventForwarder()
539
540
 
540
541
  def on_before_start(self):
@@ -392,6 +392,7 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
392
392
 
393
393
  def accept_state_visitor(self, visitor: StateVisitor):
394
394
  visitor.visit(dynamodb_stores)
395
+ # FIXME: DDB v2 does not depend on dynamodbstreams_stores as DynamoDB Local holds all the state
395
396
  visitor.visit(dynamodbstreams_stores)
396
397
  visitor.visit(AssetDirectory(self.service, os.path.join(config.dirs.data, self.service)))
397
398
 
@@ -37,6 +37,7 @@ from localstack.services.dynamodbstreams.dynamodbstreams_api import (
37
37
  table_name_from_stream_arn,
38
38
  )
39
39
  from localstack.services.plugins import ServiceLifecycleHook
40
+ from localstack.state import StateVisitor
40
41
  from localstack.utils.collections import select_from_typed_dict
41
42
 
42
43
  LOG = logging.getLogger(__name__)
@@ -57,6 +58,11 @@ class DynamoDBStreamsProvider(DynamodbstreamsApi, ServiceLifecycleHook):
57
58
  def __init__(self):
58
59
  self.shard_to_region = {}
59
60
 
61
+ def accept_state_visitor(self, visitor: StateVisitor):
62
+ from localstack.services.dynamodbstreams.models import dynamodbstreams_stores
63
+
64
+ visitor.visit(dynamodbstreams_stores)
65
+
60
66
  def describe_stream(
61
67
  self,
62
68
  context: RequestContext,
@@ -18,6 +18,7 @@ from localstack.services.dynamodb.utils import modify_ddblocal_arns
18
18
  from localstack.services.dynamodb.v2.provider import DynamoDBProvider, modify_context_region
19
19
  from localstack.services.dynamodbstreams.dynamodbstreams_api import get_original_region
20
20
  from localstack.services.plugins import ServiceLifecycleHook
21
+ from localstack.state import StateVisitor
21
22
  from localstack.utils.aws.arns import parse_arn
22
23
 
23
24
  LOG = logging.getLogger(__name__)
@@ -32,6 +33,11 @@ class DynamoDBStreamsProvider(DynamodbstreamsApi, ServiceLifecycleHook):
32
33
  self.server = DynamodbServer.get()
33
34
  self.shard_to_region = {}
34
35
 
36
+ def accept_state_visitor(self, visitor: StateVisitor):
37
+ # DynamoDB Streams state is entirely dependent on DynamoDB Local state, and does not hold state itself
38
+ # the DynamoDB provider is responsible for the persistence of DDB Streams
39
+ pass
40
+
35
41
  def on_after_init(self):
36
42
  # add response processor specific to ddblocal
37
43
  handlers.modify_service_response.append(self.service, modify_ddblocal_arns)
@@ -94,6 +94,7 @@ from localstack.services.ec2.models import get_ec2_backend
94
94
  from localstack.services.ec2.patches import apply_patches
95
95
  from localstack.services.moto import call_moto, call_moto_with_request
96
96
  from localstack.services.plugins import ServiceLifecycleHook
97
+ from localstack.state import StateVisitor
97
98
  from localstack.utils.patch import patch
98
99
  from localstack.utils.strings import first_char_to_upper, long_uid, short_uid
99
100
 
@@ -107,6 +108,11 @@ class Ec2Provider(Ec2Api, ABC, ServiceLifecycleHook):
107
108
  def on_after_init(self):
108
109
  apply_patches()
109
110
 
111
+ def accept_state_visitor(self, visitor: StateVisitor):
112
+ from moto.ec2.models import ec2_backends
113
+
114
+ visitor.visit(ec2_backends)
115
+
110
116
  @handler("DescribeAvailabilityZones", expand=False)
111
117
  def describe_availability_zones(
112
118
  self,
@@ -68,6 +68,7 @@ from localstack.aws.api.opensearch import (
68
68
  )
69
69
  from localstack.aws.connect import connect_to
70
70
  from localstack.services.opensearch.packages import ELASTICSEARCH_DEFAULT_VERSION
71
+ from localstack.state import StateVisitor
71
72
 
72
73
 
73
74
  def _version_to_opensearch(
@@ -208,6 +209,11 @@ def exception_mapper():
208
209
 
209
210
 
210
211
  class EsProvider(EsApi):
212
+ def accept_state_visitor(self, visitor: StateVisitor):
213
+ # ES state entirely depends on `opensearch`, and delegates its entire state to it
214
+ # we do not need to manage state in ES
215
+ pass
216
+
211
217
  def create_elasticsearch_domain(
212
218
  self,
213
219
  context: RequestContext,
@@ -168,6 +168,7 @@ from localstack.services.events.utils import (
168
168
  recursive_remove_none_values_from_dict,
169
169
  )
170
170
  from localstack.services.plugins import ServiceLifecycleHook
171
+ from localstack.state import StateVisitor
171
172
  from localstack.utils.common import truncate
172
173
  from localstack.utils.event_matcher import matches_event
173
174
  from localstack.utils.strings import long_uid
@@ -246,6 +247,9 @@ class EventsProvider(EventsApi, ServiceLifecycleHook):
246
247
  self._connection_service_store: ConnectionServiceDict = {}
247
248
  self._api_destination_service_store: ApiDestinationServiceDict = {}
248
249
 
250
+ def accept_state_visitor(self, visitor: StateVisitor):
251
+ visitor.visit(events_stores)
252
+
249
253
  def on_before_start(self):
250
254
  JobScheduler.start()
251
255
 
@@ -45,6 +45,7 @@ from localstack.services.events.scheduler import JobScheduler
45
45
  from localstack.services.events.v1.models import EventsStore, events_stores
46
46
  from localstack.services.moto import call_moto
47
47
  from localstack.services.plugins import ServiceLifecycleHook
48
+ from localstack.state import StateVisitor
48
49
  from localstack.utils.aws.arns import event_bus_arn, parse_arn
49
50
  from localstack.utils.aws.client_types import ServicePrincipal
50
51
  from localstack.utils.aws.message_forwarding import send_event_to_target
@@ -83,6 +84,14 @@ class EventsProvider(EventsApi, ServiceLifecycleHook):
83
84
  def on_before_stop(self):
84
85
  JobScheduler.shutdown()
85
86
 
87
+ def accept_state_visitor(self, visitor: StateVisitor):
88
+ from moto.events.models import events_backends
89
+
90
+ from localstack.services.events.v1.models import events_stores
91
+
92
+ visitor.visit(events_backends)
93
+ visitor.visit(events_stores)
94
+
86
95
  @route("/_aws/events/rules/<path:rule_arn>/trigger")
87
96
  def trigger_scheduled_rule(self, request: Request, rule_arn: str):
88
97
  """Developer endpoint to trigger a scheduled rule."""
@@ -94,6 +94,7 @@ from localstack.services.firehose.mappers import (
94
94
  convert_source_config_to_desc,
95
95
  )
96
96
  from localstack.services.firehose.models import FirehoseStore, firehose_stores
97
+ from localstack.state import StateVisitor
97
98
  from localstack.utils.aws.arns import (
98
99
  extract_account_id_from_arn,
99
100
  extract_region_from_arn,
@@ -251,8 +252,12 @@ class FirehoseProvider(FirehoseApi):
251
252
 
252
253
  def __init__(self) -> None:
253
254
  super().__init__()
255
+ # TODO: stop/restart the kinesis listeners when stopping the service / reset the state / restore the state
254
256
  self.kinesis_listeners = {}
255
257
 
258
+ def accept_state_visitor(self, visitor: StateVisitor):
259
+ visitor.visit(firehose_stores)
260
+
256
261
  @staticmethod
257
262
  def get_store(account_id: str, region_name: str) -> FirehoseStore:
258
263
  return firehose_stores[account_id][region_name]
@@ -75,6 +75,7 @@ from localstack.services.iam.resources.policy_simulator import (
75
75
  )
76
76
  from localstack.services.iam.resources.service_linked_roles import SERVICE_LINKED_ROLES
77
77
  from localstack.services.moto import call_moto
78
+ from localstack.state import StateVisitor
78
79
  from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
79
80
 
80
81
  LOG = logging.getLogger(__name__)
@@ -110,6 +111,9 @@ class IamProvider(IamApi):
110
111
  apply_iam_patches()
111
112
  self.policy_simulator = BasicIAMPolicySimulator()
112
113
 
114
+ def accept_state_visitor(self, visitor: StateVisitor):
115
+ visitor.visit(iam_backends)
116
+
113
117
  @handler("CreateRole", expand=False)
114
118
  def create_role(
115
119
  self, context: RequestContext, request: CreateRoleRequest
@@ -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):
@@ -20,7 +20,6 @@ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
20
20
  from cryptography.hazmat.primitives.asymmetric.padding import PSS, PKCS1v15
21
21
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
22
22
  from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
23
- from cryptography.hazmat.primitives.kdf.hkdf import HKDF
24
23
  from cryptography.hazmat.primitives.serialization import load_der_public_key
25
24
 
26
25
  from localstack.aws.api.kms import (
@@ -173,6 +172,7 @@ class KmsCryptoKey:
173
172
  public_key: bytes | None
174
173
  private_key: bytes | None
175
174
  key_material: bytes
175
+ pending_key_material: bytes | None
176
176
  key_spec: str
177
177
 
178
178
  @staticmethod
@@ -217,6 +217,7 @@ class KmsCryptoKey:
217
217
  def __init__(self, key_spec: str, key_material: bytes | None = None):
218
218
  self.private_key = None
219
219
  self.public_key = None
220
+ self.pending_key_material = None
220
221
  # Technically, key_material, being a symmetric encryption key, is only relevant for
221
222
  # key_spec == SYMMETRIC_DEFAULT.
222
223
  # But LocalStack uses symmetric encryption with this key_material even for other specs. Asymmetric keys are
@@ -248,8 +249,9 @@ class KmsCryptoKey:
248
249
  self._serialize_key(key)
249
250
 
250
251
  def load_key_material(self, material: bytes):
251
- if self.key_spec in [
252
- KeySpec.SYMMETRIC_DEFAULT,
252
+ if self.key_spec == KeySpec.SYMMETRIC_DEFAULT:
253
+ self.pending_key_material = material
254
+ elif self.key_spec in [
253
255
  KeySpec.HMAC_224,
254
256
  KeySpec.HMAC_256,
255
257
  KeySpec.HMAC_384,
@@ -323,9 +325,28 @@ class KmsKey:
323
325
  # remove the _custom_key_material_ tag from the tags to not readily expose the custom key material
324
326
  del self.tags[TAG_KEY_CUSTOM_KEY_MATERIAL]
325
327
  self.crypto_key = KmsCryptoKey(self.metadata.get("KeySpec"), custom_key_material)
328
+ self._internal_key_id = uuid.uuid4()
329
+
330
+ # The KMS implementation always provides a crypto key with key material which doesn't suit scenarios where a
331
+ # KMS Key may have no key material e.g. for external keys. Don't expose the CurrentKeyMaterialId in those cases.
332
+ if custom_key_material or (
333
+ self.metadata["Origin"] == "AWS_KMS"
334
+ and self.metadata["KeySpec"] == KeySpec.SYMMETRIC_DEFAULT
335
+ ):
336
+ self.metadata["CurrentKeyMaterialId"] = self.generate_key_material_id(
337
+ self.crypto_key.key_material
338
+ )
339
+
326
340
  self.rotation_period_in_days = 365
327
341
  self.next_rotation_date = None
328
342
 
343
+ def generate_key_material_id(self, key_material: bytes) -> str:
344
+ # The KeyMaterialId depends on the key material and the KeyId. Use an internal ID to prevent brute forcing
345
+ # the value of the key material from the public KeyId and KeyMaterialId.
346
+ # https://docs.aws.amazon.com/kms/latest/APIReference/API_ImportKeyMaterial.html
347
+ key_material_id_hex = uuid.uuid5(self._internal_key_id, key_material).hex
348
+ return str(key_material_id_hex) * 2
349
+
329
350
  def calculate_and_set_arn(self, account_id, region):
330
351
  self.metadata["Arn"] = kms_key_arn(self.metadata.get("KeyId"), account_id, region)
331
352
 
@@ -420,17 +441,15 @@ class KmsKey:
420
441
 
421
442
  def derive_shared_secret(self, public_key: bytes) -> bytes:
422
443
  key_spec = self.metadata.get("KeySpec")
423
- match key_spec:
424
- case KeySpec.ECC_NIST_P256 | KeySpec.ECC_SECG_P256K1:
425
- algorithm = hashes.SHA256()
426
- case KeySpec.ECC_NIST_P384:
427
- algorithm = hashes.SHA384()
428
- case KeySpec.ECC_NIST_P521:
429
- algorithm = hashes.SHA512()
430
- case _:
431
- raise InvalidKeyUsageException(
432
- f"{self.metadata['Arn']} key usage is {self.metadata['KeyUsage']} which is not valid for DeriveSharedSecret."
433
- )
444
+ if key_spec not in (
445
+ KeySpec.ECC_NIST_P256,
446
+ KeySpec.ECC_SECG_P256K1,
447
+ KeySpec.ECC_NIST_P384,
448
+ KeySpec.ECC_NIST_P521,
449
+ ):
450
+ raise InvalidKeyUsageException(
451
+ f"{self.metadata['Arn']} key usage is {self.metadata['KeyUsage']} which is not valid for DeriveSharedSecret."
452
+ )
434
453
 
435
454
  # Deserialize public key from DER encoded data to EllipticCurvePublicKey.
436
455
  try:
@@ -438,14 +457,7 @@ class KmsKey:
438
457
  except (UnsupportedAlgorithm, ValueError):
439
458
  raise ValidationException("")
440
459
  shared_secret = self.crypto_key.key.exchange(ec.ECDH(), pub_key)
441
- # Perform shared secret derivation.
442
- return HKDF(
443
- algorithm=algorithm,
444
- salt=None,
445
- info=b"",
446
- length=algorithm.digest_size,
447
- backend=default_backend(),
448
- ).derive(shared_secret)
460
+ return shared_secret
449
461
 
450
462
  # This method gets called when a key is replicated to another region. It's meant to populate the required metadata
451
463
  # fields in a new replica key.
@@ -746,8 +758,16 @@ class KmsKey:
746
758
  f"The on-demand rotations limit has been reached for the given keyId. "
747
759
  f"No more on-demand rotations can be performed for this key: {self.metadata['Arn']}"
748
760
  )
749
- self.previous_keys.append(self.crypto_key.key_material)
750
- self.crypto_key = KmsCryptoKey(KeySpec.SYMMETRIC_DEFAULT)
761
+ current_key_material = self.crypto_key.key_material
762
+ pending_key_material = self.crypto_key.pending_key_material
763
+
764
+ self.previous_keys.append(current_key_material)
765
+
766
+ # If there is no pending material stored on the key, then key material will be generated.
767
+ self.crypto_key = KmsCryptoKey(KeySpec.SYMMETRIC_DEFAULT, pending_key_material)
768
+ self.metadata["CurrentKeyMaterialId"] = self.generate_key_material_id(
769
+ self.crypto_key.key_material
770
+ )
751
771
 
752
772
 
753
773
  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 (
@@ -135,9 +138,11 @@ from localstack.services.kms.utils import (
135
138
  validate_alias_name,
136
139
  )
137
140
  from localstack.services.plugins import ServiceLifecycleHook
141
+ from localstack.state import StateVisitor
138
142
  from localstack.utils.aws.arns import get_partition, kms_alias_arn, parse_arn
139
143
  from localstack.utils.collections import PaginatedList
140
144
  from localstack.utils.common import select_attributes
145
+ from localstack.utils.crypto import pkcs7_envelope_encrypt
141
146
  from localstack.utils.strings import short_uid, to_bytes, to_str
142
147
 
143
148
  LOG = logging.getLogger(__name__)
@@ -197,6 +202,9 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
197
202
  - VerifyMac
198
203
  """
199
204
 
205
+ def accept_state_visitor(self, visitor: StateVisitor):
206
+ visitor.visit(kms_stores)
207
+
200
208
  #
201
209
  # Helpers
202
210
  #
@@ -518,7 +526,12 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
518
526
 
519
527
  self.update_primary_key_with_replica_keys(primary_key, replica_key, replica_region)
520
528
 
521
- return ReplicateKeyResponse(ReplicaKeyMetadata=replica_key.metadata)
529
+ # CurrentKeyMaterialId is not returned in the ReplicaKeyMetadata. May be due to not being evaluated until
530
+ # the key has been successfully replicated as it does not show up in DescribeKey immediately either.
531
+ replica_key_metadata_response = copy.deepcopy(replica_key.metadata)
532
+ replica_key_metadata_response.pop("CurrentKeyMaterialId", None)
533
+
534
+ return ReplicateKeyResponse(ReplicaKeyMetadata=replica_key_metadata_response)
522
535
 
523
536
  @staticmethod
524
537
  # Adds new multi region replica key to the primary key's metadata.
@@ -1075,6 +1088,25 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1075
1088
  self._validate_key_for_encryption_decryption(context, key)
1076
1089
  self._validate_key_state_not_pending_import(key)
1077
1090
 
1091
+ # Handle the recipient field. This is used by AWS Nitro to re-encrypt the plaintext to the key specified
1092
+ # by the enclave. Proper support for this will take significant work to figure out how to model enforcing
1093
+ # the attestation measurements; for now, if recipient is specified and has an attestation doc in it including
1094
+ # a public key where it's expected to be, we encrypt to that public key. This at least allows users to use
1095
+ # localstack as a drop-in replacement for AWS when testing without having to skip the secondary decryption
1096
+ # when using localstack.
1097
+ recipient_pubkey = None
1098
+ if recipient:
1099
+ attestation_document = recipient["AttestationDocument"]
1100
+ # We do all of this in a try/catch and warn if it fails so that if users are currently passing a nonsense
1101
+ # value we don't break it for them. In the future we could do a breaking change to require a valid attestation
1102
+ # (or at least one that contains the public key).
1103
+ try:
1104
+ recipient_pubkey = self._extract_attestation_pubkey(attestation_document)
1105
+ except Exception as e:
1106
+ logging.warning(
1107
+ "Unable to extract public key from non-empty attestation document: %s", e
1108
+ )
1109
+
1078
1110
  try:
1079
1111
  # TODO: Extend the implementation to handle additional encryption/decryption scenarios
1080
1112
  # beyond the current support for offline encryption and online decryption using RSA keys if key id exists in
@@ -1088,20 +1120,27 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1088
1120
  plaintext = key.decrypt(ciphertext, encryption_context)
1089
1121
  except InvalidTag:
1090
1122
  raise InvalidCiphertextException()
1123
+
1091
1124
  # For compatibility, we return EncryptionAlgorithm values expected from AWS. But LocalStack currently always
1092
1125
  # encrypts with symmetric encryption no matter the key settings.
1093
1126
  #
1094
1127
  # We return a key ARN instead of KeyId despite the name of the parameter, as this is what AWS does and states
1095
1128
  # in its docs.
1096
- # TODO add support for "recipient"
1097
1129
  # https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html#API_Decrypt_RequestSyntax
1098
1130
  # TODO add support for "dry_run"
1099
- return DecryptResponse(
1131
+ response = DecryptResponse(
1100
1132
  KeyId=key.metadata.get("Arn"),
1101
- Plaintext=plaintext,
1102
1133
  EncryptionAlgorithm=encryption_algorithm,
1103
1134
  )
1104
1135
 
1136
+ # Encrypt to the recipient pubkey if specified. Otherwise, return the actual plaintext
1137
+ if recipient_pubkey:
1138
+ response["CiphertextForRecipient"] = pkcs7_envelope_encrypt(plaintext, recipient_pubkey)
1139
+ else:
1140
+ response["Plaintext"] = plaintext
1141
+
1142
+ return response
1143
+
1105
1144
  def get_parameters_for_import(
1106
1145
  self,
1107
1146
  context: RequestContext,
@@ -1176,13 +1215,10 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1176
1215
  # TODO check if there was already a key imported for this kms key
1177
1216
  # if so, it has to be identical. We cannot change keys by reimporting after deletion/expiry
1178
1217
  key_material = self._decrypt_wrapped_key_material(import_state, encrypted_key_material)
1179
-
1180
- if expiration_model:
1181
- key_to_import_material_to.metadata["ExpirationModel"] = expiration_model
1182
- else:
1183
- key_to_import_material_to.metadata["ExpirationModel"] = (
1184
- ExpirationModelType.KEY_MATERIAL_EXPIRES
1185
- )
1218
+ key_material_id = key_to_import_material_to.generate_key_material_id(key_material)
1219
+ key_to_import_material_to.metadata["ExpirationModel"] = (
1220
+ expiration_model or ExpirationModelType.KEY_MATERIAL_EXPIRES
1221
+ )
1186
1222
  if (
1187
1223
  key_to_import_material_to.metadata["ExpirationModel"]
1188
1224
  == ExpirationModelType.KEY_MATERIAL_EXPIRES
@@ -1191,12 +1227,42 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1191
1227
  raise ValidationException(
1192
1228
  "A validTo date must be set if the ExpirationModel is KEY_MATERIAL_EXPIRES"
1193
1229
  )
1230
+ if existing_pending_material := key_to_import_material_to.crypto_key.pending_key_material:
1231
+ pending_key_material_id = key_to_import_material_to.generate_key_material_id(
1232
+ existing_pending_material
1233
+ )
1234
+ raise KMSInvalidStateException(
1235
+ f"New key material (id: {key_material_id}) cannot be imported into KMS key "
1236
+ f"{key_to_import_material_to.metadata['Arn']}, because another key material "
1237
+ f"(id: {pending_key_material_id}) is pending rotation."
1238
+ )
1239
+
1194
1240
  # TODO actually set validTo and make the key expire
1195
1241
  key_to_import_material_to.metadata["Enabled"] = True
1196
1242
  key_to_import_material_to.metadata["KeyState"] = KeyState.Enabled
1197
1243
  key_to_import_material_to.crypto_key.load_key_material(key_material)
1198
1244
 
1199
- return ImportKeyMaterialResponse()
1245
+ # KeyMaterialId / CurrentKeyMaterialId is only exposed for symmetric encryption keys.
1246
+ key_material_id_response = None
1247
+ if key_to_import_material_to.metadata["KeySpec"] == KeySpec.SYMMETRIC_DEFAULT:
1248
+ key_material_id_response = key_to_import_material_to.generate_key_material_id(
1249
+ key_material
1250
+ )
1251
+
1252
+ # If there is no CurrentKeyMaterialId, instantly promote the pending key material to the current.
1253
+ if key_to_import_material_to.metadata.get("CurrentKeyMaterialId") is None:
1254
+ key_to_import_material_to.metadata["CurrentKeyMaterialId"] = (
1255
+ key_material_id_response
1256
+ )
1257
+ key_to_import_material_to.crypto_key.key_material = (
1258
+ key_to_import_material_to.crypto_key.pending_key_material
1259
+ )
1260
+ key_to_import_material_to.crypto_key.pending_key_material = None
1261
+
1262
+ return ImportKeyMaterialResponse(
1263
+ KeyId=key_to_import_material_to.metadata["Arn"],
1264
+ KeyMaterialId=key_material_id_response,
1265
+ )
1200
1266
 
1201
1267
  def delete_imported_key_material(
1202
1268
  self,
@@ -1323,7 +1389,7 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1323
1389
  key = self._get_kms_key(account_id, region_name, key_id, any_key_state_allowed=True)
1324
1390
 
1325
1391
  response = GetKeyRotationStatusResponse(
1326
- KeyId=key_id,
1392
+ KeyId=key.metadata["Arn"],
1327
1393
  KeyRotationEnabled=key.is_key_rotation_enabled,
1328
1394
  NextRotationDate=key.next_rotation_date,
1329
1395
  )
@@ -1415,13 +1481,13 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1415
1481
 
1416
1482
  if key.metadata["KeySpec"] != KeySpec.SYMMETRIC_DEFAULT:
1417
1483
  raise UnsupportedOperationException()
1418
- if key.metadata["Origin"] == OriginType.EXTERNAL:
1419
- raise NotImplementedError("Rotation of imported keys is not supported yet.")
1484
+ self._validate_key_state_not_pending_import(key)
1485
+ self._validate_external_key_has_pending_material(key)
1420
1486
 
1421
1487
  key.rotate_key_on_demand()
1422
1488
 
1423
1489
  return RotateKeyOnDemandResponse(
1424
- KeyId=key_id,
1490
+ KeyId=key.metadata["Arn"],
1425
1491
  )
1426
1492
 
1427
1493
  @handler("TagResource", expand=False)
@@ -1498,6 +1564,12 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1498
1564
  if key.metadata["KeyState"] == KeyState.PendingImport:
1499
1565
  raise KMSInvalidStateException(f"{key.metadata['Arn']} is pending import.")
1500
1566
 
1567
+ def _validate_external_key_has_pending_material(self, key: KmsKey):
1568
+ if key.metadata["Origin"] == "EXTERNAL" and key.crypto_key.pending_key_material is None:
1569
+ raise KMSInvalidStateException(
1570
+ f"No available key material pending rotation for the key: {key.metadata['Arn']}."
1571
+ )
1572
+
1501
1573
  def _validate_key_for_encryption_decryption(self, context: RequestContext, key: KmsKey):
1502
1574
  key_usage = key.metadata["KeyUsage"]
1503
1575
  if key_usage != "ENCRYPT_DECRYPT":
@@ -1559,6 +1631,15 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1559
1631
  f" constraint: [Member must satisfy enum value set: {VALID_OPERATIONS}]"
1560
1632
  )
1561
1633
 
1634
+ def _extract_attestation_pubkey(self, attestation_document: bytes) -> RSAPublicKey:
1635
+ # The attestation document comes as a COSE (CBOR Object Signing and Encryption) object: the CBOR
1636
+ # attestation is signed and then the attestation and signature are again CBOR-encoded. For now
1637
+ # we don't bother validating the signature, though in the future we could.
1638
+ cose_document = cbor2_loads(attestation_document)
1639
+ attestation = cbor2_loads(cose_document[2])
1640
+ public_key_bytes = attestation["public_key"]
1641
+ return load_der_public_key(public_key_bytes)
1642
+
1562
1643
  def _decrypt_wrapped_key_material(
1563
1644
  self,
1564
1645
  import_state: KeyImportState,