From 63ea482a0177e4b0aa099c336a62d1df8f3bcadb Mon Sep 17 00:00:00 2001 From: mabuaisha Date: Sun, 16 Mar 2025 09:38:11 +0100 Subject: [PATCH 1/4] Secret Manager: Solve the issue for rotate secret after sub-sequent requests without providing rotation configuration --- .../services/secretsmanager/provider.py | 14 +- .../functions/lambda_rotate_secret.py | 11 + .../secretsmanager/test_secretsmanager.py | 174 +++++--- .../test_secretsmanager.snapshot.json | 370 +++++++++++++++++- .../test_secretsmanager.validation.json | 13 +- 5 files changed, 510 insertions(+), 72 deletions(-) diff --git a/localstack-core/localstack/services/secretsmanager/provider.py b/localstack-core/localstack/services/secretsmanager/provider.py index efefe6220819d..6e6d2d91aa81c 100644 --- a/localstack-core/localstack/services/secretsmanager/provider.py +++ b/localstack-core/localstack/services/secretsmanager/provider.py @@ -729,11 +729,15 @@ def backend_rotate_secret( if not self._is_valid_identifier(secret_id): raise SecretNotFoundException() - if self.secrets[secret_id].is_deleted(): + secret = self.secrets[secret_id] + if secret.is_deleted(): raise InvalidRequestException( "An error occurred (InvalidRequestException) when calling the RotateSecret operation: You tried to \ perform the operation on a secret that's currently marked deleted." ) + # Resolve rotation_lambda_arn and fallback to previous value if its missing + # from the current request + rotation_lambda_arn = rotation_lambda_arn or secret.rotation_lambda_arn if rotation_lambda_arn: if len(rotation_lambda_arn) > 2048: @@ -746,6 +750,10 @@ def backend_rotate_secret( if rotation_period < 1 or rotation_period > 1000: msg = "RotationRules.AutomaticallyAfterDays must be within 1-1000." raise InvalidParameterException(msg) + else: + # Resolve auto_rotate_after_days and fallback to previous value + # if its missing from the current request + rotation_period = secret.auto_rotate_after_days try: lm_client = connect_to(region_name=self.region_name).lambda_ @@ -753,8 +761,6 @@ def backend_rotate_secret( except Exception: raise ResourceNotFoundException("Lambda does not exist or could not be accessed") - secret = self.secrets[secret_id] - # The rotation function must end with the versions of the secret in # one of two states: # @@ -782,7 +788,7 @@ def backend_rotate_secret( pass secret.rotation_lambda_arn = rotation_lambda_arn - secret.auto_rotate_after_days = rotation_rules.get(rotation_days, 0) + secret.auto_rotate_after_days = rotation_period if secret.auto_rotate_after_days > 0: wait_interval_s = int(rotation_period) * 86400 secret.next_rotation_date = int(time.time()) + wait_interval_s diff --git a/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py b/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py index 97dcf17736256..ccfebb8621bf3 100644 --- a/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py +++ b/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py @@ -224,3 +224,14 @@ def finish_secret(service_client, arn, token): token, arn, ) + if "AWSPENDING" in metadata["VersionIdsToStages"].get(token, []): + service_client.update_secret_version_stage( + SecretId=arn, + VersionStage="AWSPENDING", + RemoveFromVersionId=token, + ) + logger.info( + "finishSecret: Successfully removed AWSPENDING stage from version %s for secret %s.", + token, + arn, + ) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index e8c7456156047..0376ebe524868 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime from math import isclose -from typing import Optional +from typing import Any, Optional import pytest import requests @@ -103,6 +103,81 @@ def _is_secret_deleted(): secret_id, ) + @staticmethod + def _setup_rotation_secret( + sm_snapshot, + secret_name, + create_secret, + create_lambda_function, + aws_client, + ) -> (str, str): + cre_res = create_secret( + Name=secret_name, + SecretString="my_secret", + Description="testing rotation of secrets", + ) + + sm_snapshot.add_transformer( + sm_snapshot.transform.key_value("RotationLambdaARN", "lambda-arn") + ) + sm_snapshot.add_transformers_list( + sm_snapshot.transform.secretsmanager_secret_id_arn(cre_res, 0) + ) + + function_name = f"s-{short_uid()}" + function_arn = create_lambda_function( + handler_file=TEST_LAMBDA_ROTATE_SECRET, + func_name=function_name, + runtime=Runtime.python3_12, + )["CreateFunctionResponse"]["FunctionArn"] + + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId="secretsManagerPermission", + Action="lambda:InvokeFunction", + Principal="secretsmanager.amazonaws.com", + ) + return cre_res["VersionId"], function_arn + + def _assert_after_rotate_secret_with_lambda_success( + self, + sm_snapshot, + secret_name, + initial_secret_version, + aws_client, + snapshot_key_suffix: str = "1", + function_arn: str | None = None, + rotation_config: dict[str, Any] = None, + ): + rotation_config = rotation_config or {} + if function_arn: + rotation_config["RotationLambdaARN"] = function_arn + + rot_res = aws_client.secretsmanager.rotate_secret( + SecretId=secret_name, + **rotation_config, + ) + + sm_snapshot.match(f"rotate_secret_immediately_{snapshot_key_suffix}", rot_res) + + self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) + + response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) + sm_snapshot.match(f"describe_secret_rotated_{snapshot_key_suffix}", response) + + list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name + ) + + sm_snapshot.match( + f"list_secret_versions_rotated_1_{snapshot_key_suffix}", list_secret_versions_1 + ) + + # As a result of the Lambda invocations. current version should be + # pointed to `AWSCURRENT` & previous version to `AWSPREVIOUS` + assert response["VersionIdsToStages"][initial_secret_version] == ["AWSPREVIOUS"] + assert response["VersionIdsToStages"][rot_res["VersionId"]] == ["AWSCURRENT"] + @staticmethod def _wait_rotation(client, secret_id: str, secret_version: str): def _is_secret_rotated(): @@ -533,58 +608,65 @@ def test_rotate_secret_with_lambda_success( Tests secret rotation via a lambda function. Parametrization ensures we test the default behavior which is an immediate rotation. """ - cre_res = create_secret( - Name=secret_name, - SecretString="my_secret", - Description="testing rotation of secrets", - ) - - sm_snapshot.add_transformer( - sm_snapshot.transform.key_value("RotationLambdaARN", "lambda-arn") - ) - sm_snapshot.add_transformers_list( - sm_snapshot.transform.secretsmanager_secret_id_arn(cre_res, 0) + rotation_config = { + "RotationRules": {"AutomaticallyAfterDays": 1}, + } + if rotate_immediately: + rotation_config["RotateImmediately"] = rotate_immediately + initial_secret_version, function_arn = self._setup_rotation_secret( + sm_snapshot, secret_name, create_secret, create_lambda_function, aws_client ) - - function_name = f"s-{short_uid()}" - function_arn = create_lambda_function( - handler_file=TEST_LAMBDA_ROTATE_SECRET, - func_name=function_name, - runtime=Runtime.python3_12, - )["CreateFunctionResponse"]["FunctionArn"] - - aws_client.lambda_.add_permission( - FunctionName=function_name, - StatementId="secretsManagerPermission", - Action="lambda:InvokeFunction", - Principal="secretsmanager.amazonaws.com", + self._assert_after_rotate_secret_with_lambda_success( + sm_snapshot, + secret_name, + initial_secret_version, + aws_client, + function_arn=function_arn, + rotation_config=rotation_config, ) - rotation_kwargs = {} - if rotate_immediately is not None: - rotation_kwargs["RotateImmediately"] = rotate_immediately - rot_res = aws_client.secretsmanager.rotate_secret( - SecretId=secret_name, - RotationLambdaARN=function_arn, - RotationRules={ - "AutomaticallyAfterDays": 1, - }, - **rotation_kwargs, + @pytest.mark.parametrize("set_rotation_on_second_run", [False, True]) + @markers.snapshot.skip_snapshot_verify( + paths=["$..VersionIdsToStages", "$..Versions", "$..VersionId"] + ) + @markers.aws.validated + def test_rotate_secret_multiple_times_with_lambda_success( + self, + sm_snapshot, + secret_name, + create_secret, + create_lambda_function, + aws_client, + set_rotation_on_second_run, + ): + rotation_config = { + "RotationRules": {"AutomaticallyAfterDays": 1}, + "RotateImmediately": True, + } + secret_initial_version, function_arn = self._setup_rotation_secret( + sm_snapshot, secret_name, create_secret, create_lambda_function, aws_client ) - sm_snapshot.match("rotate_secret_immediately", rot_res) - - self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) - - response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) - sm_snapshot.match("describe_secret_rotated", response) - - list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( - SecretId=secret_name + self._assert_after_rotate_secret_with_lambda_success( + sm_snapshot, + secret_name, + secret_initial_version, + aws_client, + function_arn=function_arn, + rotation_config=rotation_config, + ) + # Fetch the current version after first rotation + secret = aws_client.secretsmanager.get_secret_value(SecretId=secret_name) + self._assert_after_rotate_secret_with_lambda_success( + sm_snapshot, + secret_name, + secret["VersionId"], + aws_client, + snapshot_key_suffix="2", + function_arn=function_arn if set_rotation_on_second_run else None, + rotation_config=rotation_config if set_rotation_on_second_run else None, ) - sm_snapshot.match("list_secret_versions_rotated_1", list_secret_versions_1) - @markers.snapshot.skip_snapshot_verify(paths=["$..Error", "$..Message"]) @markers.aws.validated def test_rotate_secret_invalid_lambda_arn( diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 003987e7c32e2..87d31b71e7f9b 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -3687,18 +3687,18 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "recorded-date": "28-03-2024, 06:58:46", + "recorded-date": "16-03-2025, 07:41:39", "recorded-content": { - "rotate_secret_immediately": { + "rotate_secret_immediately_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe_secret_rotated": { + "describe_secret_rotated_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "CreatedDate": "datetime", "Description": "testing rotation of secrets", @@ -3714,11 +3714,10 @@ }, "VersionIdsToStages": { "": [ - "AWSCURRENT", - "AWSPENDING" + "AWSPREVIOUS" ], "": [ - "AWSPREVIOUS" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -3726,7 +3725,7 @@ "HTTPStatusCode": 200 } }, - "list_secret_versions_rotated_1": { + "list_secret_versions_rotated_1_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", "Versions": [ @@ -3736,7 +3735,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3746,10 +3745,9 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ - "AWSCURRENT", - "AWSPENDING" + "AWSCURRENT" ] } ], @@ -3761,9 +3759,9 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "recorded-date": "28-03-2024, 06:58:58", + "recorded-date": "16-03-2025, 07:41:47", "recorded-content": { - "rotate_secret_immediately": { + "rotate_secret_immediately_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", "VersionId": "", @@ -3772,7 +3770,7 @@ "HTTPStatusCode": 200 } }, - "describe_secret_rotated": { + "describe_secret_rotated_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "CreatedDate": "datetime", "Description": "testing rotation of secrets", @@ -3791,8 +3789,7 @@ "AWSPREVIOUS" ], "": [ - "AWSCURRENT", - "AWSPENDING" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -3800,7 +3797,7 @@ "HTTPStatusCode": 200 } }, - "list_secret_versions_rotated_1": { + "list_secret_versions_rotated_1_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", "Versions": [ @@ -3822,8 +3819,7 @@ ], "VersionId": "", "VersionStages": [ - "AWSCURRENT", - "AWSPENDING" + "AWSCURRENT" ] } ], @@ -4586,5 +4582,339 @@ } } } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[False]": { + "recorded-date": "15-03-2025, 08:34:38", + "recorded-content": { + "rotate_secret_immediately_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastChangedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[True]": { + "recorded-date": "16-03-2025, 07:43:09", + "recorded-content": { + "rotate_secret_immediately_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSPREVIOUS" + ], + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "LastAccessedDate": "datetime", + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "rotate_secret_immediately_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSCURRENT" + ], + "": [ + "AWSPREVIOUS" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "LastAccessedDate": "datetime", + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[False]": { + "recorded-date": "16-03-2025, 07:42:51", + "recorded-content": { + "rotate_secret_immediately_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSCURRENT" + ], + "": [ + "AWSPREVIOUS" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "LastAccessedDate": "datetime", + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "rotate_secret_immediately_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSPREVIOUS" + ], + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "LastAccessedDate": "datetime", + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index a85ca0d9e3e4a..c785874c3ab3d 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -101,14 +101,23 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_invalid_lambda_arn": { "last_validated_date": "2024-03-15T10:11:13+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[False]": { + "last_validated_date": "2025-03-16T07:42:50+00:00" + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[True]": { + "last_validated_date": "2025-03-16T07:43:08+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success": { "last_validated_date": "2024-03-15T08:12:22+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[False]": { + "last_validated_date": "2025-03-15T08:34:38+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "last_validated_date": "2024-03-28T06:58:56+00:00" + "last_validated_date": "2025-03-16T07:41:47+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "last_validated_date": "2024-03-28T06:58:44+00:00" + "last_validated_date": "2025-03-16T07:41:39+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists": { "last_validated_date": "2024-03-15T08:14:33+00:00" From 1203f02c366428a0ddd8ed5142467ca1d92b9a5b Mon Sep 17 00:00:00 2001 From: mabuaisha Date: Tue, 25 Mar 2025 04:40:26 +0100 Subject: [PATCH 2/4] Remove invalid snapshot test for secret manager --- .../test_secretsmanager.snapshot.json | 80 +++---------------- .../test_secretsmanager.validation.json | 7 +- 2 files changed, 14 insertions(+), 73 deletions(-) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 87d31b71e7f9b..88b1db0594015 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -3687,12 +3687,12 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "recorded-date": "16-03-2025, 07:41:39", + "recorded-date": "25-03-2025, 03:33:11", "recorded-content": { "rotate_secret_immediately_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3714,10 +3714,10 @@ }, "VersionIdsToStages": { "": [ - "AWSPREVIOUS" + "AWSCURRENT" ], "": [ - "AWSCURRENT" + "AWSPREVIOUS" ] }, "ResponseMetadata": { @@ -3735,7 +3735,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3745,7 +3745,7 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSCURRENT" ] @@ -3759,12 +3759,12 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "recorded-date": "16-03-2025, 07:41:47", + "recorded-date": "25-03-2025, 03:33:20", "recorded-content": { "rotate_secret_immediately_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3786,10 +3786,10 @@ }, "VersionIdsToStages": { "": [ - "AWSPREVIOUS" + "AWSCURRENT" ], "": [ - "AWSCURRENT" + "AWSPREVIOUS" ] }, "ResponseMetadata": { @@ -3807,7 +3807,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3817,7 +3817,7 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSCURRENT" ] @@ -4583,62 +4583,6 @@ } } }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[False]": { - "recorded-date": "15-03-2025, 08:34:38", - "recorded-content": { - "rotate_secret_immediately_1": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "Name": "", - "VersionId": "", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe_secret_rotated_1": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "CreatedDate": "datetime", - "Description": "testing rotation of secrets", - "LastChangedDate": "datetime", - "Name": "", - "NextRotationDate": "datetime", - "RotationEnabled": true, - "RotationLambdaARN": "", - "RotationRules": { - "AutomaticallyAfterDays": 1 - }, - "VersionIdsToStages": { - "": [ - "AWSCURRENT" - ] - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "list_secret_versions_rotated_1_1": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "Name": "", - "Versions": [ - { - "CreatedDate": "datetime", - "KmsKeyIds": [ - "DefaultEncryptionKey" - ], - "VersionId": "", - "VersionStages": [ - "AWSCURRENT" - ] - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[True]": { "recorded-date": "16-03-2025, 07:43:09", "recorded-content": { diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index c785874c3ab3d..e7e3c5dc06607 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -110,14 +110,11 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success": { "last_validated_date": "2024-03-15T08:12:22+00:00" }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[False]": { - "last_validated_date": "2025-03-15T08:34:38+00:00" - }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "last_validated_date": "2025-03-16T07:41:47+00:00" + "last_validated_date": "2025-03-25T03:33:19+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "last_validated_date": "2025-03-16T07:41:39+00:00" + "last_validated_date": "2025-03-25T03:33:10+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists": { "last_validated_date": "2024-03-15T08:14:33+00:00" From 2148b3d29cb665f72d4190764bf2789bb8937600 Mon Sep 17 00:00:00 2001 From: mabuaisha Date: Thu, 27 Mar 2025 17:41:51 +0100 Subject: [PATCH 3/4] Address minor suggestions & concerns raised by core team --- .../services/secretsmanager/provider.py | 4 +- .../testing/snapshots/transformer_utility.py | 13 ++++ .../secretsmanager/test_secretsmanager.py | 59 ++++++++++++------- .../test_secretsmanager.snapshot.json | 40 +++++++++++++ .../test_secretsmanager.validation.json | 3 + 5 files changed, 97 insertions(+), 22 deletions(-) diff --git a/localstack-core/localstack/services/secretsmanager/provider.py b/localstack-core/localstack/services/secretsmanager/provider.py index 6e6d2d91aa81c..d7757fc1016bd 100644 --- a/localstack-core/localstack/services/secretsmanager/provider.py +++ b/localstack-core/localstack/services/secretsmanager/provider.py @@ -738,7 +738,7 @@ def backend_rotate_secret( # Resolve rotation_lambda_arn and fallback to previous value if its missing # from the current request rotation_lambda_arn = rotation_lambda_arn or secret.rotation_lambda_arn - + rotation_period = 0 if rotation_lambda_arn: if len(rotation_lambda_arn) > 2048: msg = "RotationLambdaARN must <= 2048 characters long." @@ -788,7 +788,7 @@ def backend_rotate_secret( pass secret.rotation_lambda_arn = rotation_lambda_arn - secret.auto_rotate_after_days = rotation_period + secret.auto_rotate_after_days = rotation_period or 0 if secret.auto_rotate_after_days > 0: wait_interval_s = int(rotation_period) * 86400 secret.next_rotation_date = int(time.time()) + wait_interval_s diff --git a/localstack-core/localstack/testing/snapshots/transformer_utility.py b/localstack-core/localstack/testing/snapshots/transformer_utility.py index 77a9bdfc6e0b5..7d2d73c844dbb 100644 --- a/localstack-core/localstack/testing/snapshots/transformer_utility.py +++ b/localstack-core/localstack/testing/snapshots/transformer_utility.py @@ -648,6 +648,19 @@ def secretsmanager_api(): ), "version_uuid", ), + KeyValueBasedTransformer( + lambda k, v: ( + v + if ( + isinstance(k, str) + and k == "RotationLambdaARN" + and isinstance(v, str) + and re.match(PATTERN_ARN, v) + ) + else None + ), + "lambda-arn", + ), SortingTransformer("VersionStages"), SortingTransformer("Versions", lambda e: e.get("CreatedDate")), ] diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index 0376ebe524868..0c52767a63c8c 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -117,9 +117,6 @@ def _setup_rotation_secret( Description="testing rotation of secrets", ) - sm_snapshot.add_transformer( - sm_snapshot.transform.key_value("RotationLambdaARN", "lambda-arn") - ) sm_snapshot.add_transformers_list( sm_snapshot.transform.secretsmanager_secret_id_arn(cre_res, 0) ) @@ -139,6 +136,29 @@ def _setup_rotation_secret( ) return cre_res["VersionId"], function_arn + @staticmethod + def _setup_invalid_rotation_secret( + invalid_arn: str | None, invalid_snapshot_key: str, sm_snapshot, secret_name, aws_client + ): + create_secret = aws_client.secretsmanager.create_secret( + Name=secret_name, SecretString="init" + ) + sm_snapshot.add_transformer( + sm_snapshot.transform.secretsmanager_secret_id_arn(create_secret, 0) + ) + sm_snapshot.match("create_secret", create_secret) + rotation_config = { + "SecretId": secret_name, + "RotationRules": { + "AutomaticallyAfterDays": 1, + }, + } + if invalid_arn: + rotation_config["RotationLambdaARN"] = invalid_arn + with pytest.raises(Exception) as e: + aws_client.secretsmanager.rotate_secret(**rotation_config) + sm_snapshot.match(invalid_snapshot_key, e.value.response) + def _assert_after_rotate_secret_with_lambda_success( self, sm_snapshot, @@ -672,28 +692,27 @@ def test_rotate_secret_multiple_times_with_lambda_success( def test_rotate_secret_invalid_lambda_arn( self, secret_name, aws_client, account_id, sm_snapshot ): - create_secret = aws_client.secretsmanager.create_secret( - Name=secret_name, SecretString="init" - ) - sm_snapshot.add_transformer( - sm_snapshot.transform.secretsmanager_secret_id_arn(create_secret, 0) - ) - sm_snapshot.match("create_secret", create_secret) - region_name = aws_client.secretsmanager.meta.region_name invalid_arn = ( f"arn:aws:lambda:{region_name}:{account_id}:function:rotate_secret_invalid_lambda_arn" ) - with pytest.raises(Exception) as e: - aws_client.secretsmanager.rotate_secret( - SecretId=secret_name, - RotationLambdaARN=invalid_arn, - RotationRules={ - "AutomaticallyAfterDays": 1, - }, - ) - sm_snapshot.match("rotate_secret_invalid_arn_exc", e.value.response) + self._setup_invalid_rotation_secret( + invalid_arn, "rotate_secret_invalid_arn_exc", sm_snapshot, secret_name, aws_client + ) + describe_secret = aws_client.secretsmanager.describe_secret(SecretId=secret_name) + sm_snapshot.match("describe_secret", describe_secret) + assert "RotationEnabled" not in describe_secret + assert "RotationRules" not in describe_secret + assert "RotationLambdaARN" not in describe_secret + @markers.snapshot.skip_snapshot_verify(paths=["$..Error", "$..Message"]) + @markers.aws.validated + def test_first_rotate_secret_with_missing_lambda_arn( + self, secret_name, aws_client, account_id, sm_snapshot + ): + self._setup_invalid_rotation_secret( + None, "rotate_secret_no_arn_exc", sm_snapshot, secret_name, aws_client + ) describe_secret = aws_client.secretsmanager.describe_secret(SecretId=secret_name) sm_snapshot.match("describe_secret", describe_secret) assert "RotationEnabled" not in describe_secret diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 88b1db0594015..e1a0cc9f261a9 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -4860,5 +4860,45 @@ } } } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_first_rotate_secret_with_missing_lambda_arn": { + "recorded-date": "27-03-2025, 16:33:46", + "recorded-content": { + "create_secret": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "rotate_secret_no_arn_exc": { + "Error": { + "Code": "InvalidRequestException", + "Message": "No Lambda rotation function ARN is associated with this secret." + }, + "Message": "No Lambda rotation function ARN is associated with this secret.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "describe_secret": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "VersionIdsToStages": { + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index e7e3c5dc06607..fd6106b3abc98 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -41,6 +41,9 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_exp_raised_on_creation_of_secret_scheduled_for_deletion": { "last_validated_date": "2024-03-15T08:13:16+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_first_rotate_secret_with_missing_lambda_arn": { + "last_validated_date": "2025-03-27T16:33:46+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_force_delete_deleted_secret": { "last_validated_date": "2024-10-11T14:33:45+00:00" }, From 8b3a1a3d6715f07471d747792febfb6183ba8ad4 Mon Sep 17 00:00:00 2001 From: mabuaisha Date: Sat, 29 Mar 2025 11:00:12 +0100 Subject: [PATCH 4/4] Convert helper methods into fixtures and implement a proper tests for missing function arn for the first time rotation --- .../services/secretsmanager/provider.py | 15 +- .../secretsmanager/test_secretsmanager.py | 265 +++++++++--------- .../test_secretsmanager.snapshot.json | 217 +++----------- .../test_secretsmanager.validation.json | 11 +- 4 files changed, 177 insertions(+), 331 deletions(-) diff --git a/localstack-core/localstack/services/secretsmanager/provider.py b/localstack-core/localstack/services/secretsmanager/provider.py index d7757fc1016bd..5838732f2c4b0 100644 --- a/localstack-core/localstack/services/secretsmanager/provider.py +++ b/localstack-core/localstack/services/secretsmanager/provider.py @@ -738,22 +738,25 @@ def backend_rotate_secret( # Resolve rotation_lambda_arn and fallback to previous value if its missing # from the current request rotation_lambda_arn = rotation_lambda_arn or secret.rotation_lambda_arn - rotation_period = 0 + if not rotation_lambda_arn: + raise InvalidRequestException( + "No Lambda rotation function ARN is associated with this secret." + ) + if rotation_lambda_arn: if len(rotation_lambda_arn) > 2048: msg = "RotationLambdaARN must <= 2048 characters long." raise InvalidParameterException(msg) + # In case rotation_period is not provided, resolve auto_rotate_after_days + # and fallback to previous value if its missing from the current request. + rotation_period = secret.auto_rotate_after_days or 0 if rotation_rules: if rotation_days in rotation_rules: rotation_period = rotation_rules[rotation_days] if rotation_period < 1 or rotation_period > 1000: msg = "RotationRules.AutomaticallyAfterDays must be within 1-1000." raise InvalidParameterException(msg) - else: - # Resolve auto_rotate_after_days and fallback to previous value - # if its missing from the current request - rotation_period = secret.auto_rotate_after_days try: lm_client = connect_to(region_name=self.region_name).lambda_ @@ -788,7 +791,7 @@ def backend_rotate_secret( pass secret.rotation_lambda_arn = rotation_lambda_arn - secret.auto_rotate_after_days = rotation_period or 0 + secret.auto_rotate_after_days = rotation_period if secret.auto_rotate_after_days > 0: wait_interval_s = int(rotation_period) * 86400 secret.next_rotation_date = int(time.time()) + wait_interval_s diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index 0c52767a63c8c..7a91414c6879e 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime from math import isclose -from typing import Any, Optional +from typing import Optional import pytest import requests @@ -70,47 +70,37 @@ def sm_snapshot(self, snapshot): snapshot.add_transformers_list(snapshot.transform.secretsmanager_api()) return snapshot - @staticmethod - def _wait_created_is_listed(client, secret_id: str): - def _is_secret_in_list(): - lst: ListSecretsResponse = ( - client.get_paginator("list_secrets").paginate().build_full_result() + @pytest.fixture + def setup_invalid_rotation_secret(self, secret_name, aws_client, account_id, sm_snapshot): + def _setup(invalid_arn: str | None): + create_secret = aws_client.secretsmanager.create_secret( + Name=secret_name, SecretString="init" ) - secret_ids: set[str] = {secret["Name"] for secret in lst.get("SecretList", [])} - return secret_id in secret_ids - - assert poll_condition(condition=_is_secret_in_list, timeout=60, interval=2), ( - f"Retried check for listing of {secret_id=} timed out" - ) - - @staticmethod - def _wait_force_deletion_completed(client, secret_id: str): - def _is_secret_deleted(): - deleted = False - try: - client.describe_secret(SecretId=secret_id) - except Exception as ex: - if ex.response["Error"]["Code"] == "ResourceNotFoundException": - deleted = True - else: - raise ex - return deleted - - success = poll_condition(condition=_is_secret_deleted, timeout=120, interval=30) - if not success: - LOG.warning( - "Timed out whilst awaiting for force deletion of secret '%s' to complete.", - secret_id, + sm_snapshot.add_transformer( + sm_snapshot.transform.secretsmanager_secret_id_arn(create_secret, 0) ) + sm_snapshot.match("create_secret", create_secret) + rotation_config = { + "SecretId": secret_name, + "RotationRules": { + "AutomaticallyAfterDays": 1, + }, + } + if invalid_arn: + rotation_config["RotationLambdaARN"] = invalid_arn + aws_client.secretsmanager.rotate_secret(**rotation_config) - @staticmethod - def _setup_rotation_secret( + return _setup + + @pytest.fixture + def setup_rotation_secret( + self, sm_snapshot, secret_name, create_secret, create_lambda_function, aws_client, - ) -> (str, str): + ): cre_res = create_secret( Name=secret_name, SecretString="my_secret", @@ -137,66 +127,37 @@ def _setup_rotation_secret( return cre_res["VersionId"], function_arn @staticmethod - def _setup_invalid_rotation_secret( - invalid_arn: str | None, invalid_snapshot_key: str, sm_snapshot, secret_name, aws_client - ): - create_secret = aws_client.secretsmanager.create_secret( - Name=secret_name, SecretString="init" - ) - sm_snapshot.add_transformer( - sm_snapshot.transform.secretsmanager_secret_id_arn(create_secret, 0) - ) - sm_snapshot.match("create_secret", create_secret) - rotation_config = { - "SecretId": secret_name, - "RotationRules": { - "AutomaticallyAfterDays": 1, - }, - } - if invalid_arn: - rotation_config["RotationLambdaARN"] = invalid_arn - with pytest.raises(Exception) as e: - aws_client.secretsmanager.rotate_secret(**rotation_config) - sm_snapshot.match(invalid_snapshot_key, e.value.response) - - def _assert_after_rotate_secret_with_lambda_success( - self, - sm_snapshot, - secret_name, - initial_secret_version, - aws_client, - snapshot_key_suffix: str = "1", - function_arn: str | None = None, - rotation_config: dict[str, Any] = None, - ): - rotation_config = rotation_config or {} - if function_arn: - rotation_config["RotationLambdaARN"] = function_arn - - rot_res = aws_client.secretsmanager.rotate_secret( - SecretId=secret_name, - **rotation_config, - ) - - sm_snapshot.match(f"rotate_secret_immediately_{snapshot_key_suffix}", rot_res) - - self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) - - response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) - sm_snapshot.match(f"describe_secret_rotated_{snapshot_key_suffix}", response) + def _wait_created_is_listed(client, secret_id: str): + def _is_secret_in_list(): + lst: ListSecretsResponse = ( + client.get_paginator("list_secrets").paginate().build_full_result() + ) + secret_ids: set[str] = {secret["Name"] for secret in lst.get("SecretList", [])} + return secret_id in secret_ids - list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( - SecretId=secret_name + assert poll_condition(condition=_is_secret_in_list, timeout=60, interval=2), ( + f"Retried check for listing of {secret_id=} timed out" ) - sm_snapshot.match( - f"list_secret_versions_rotated_1_{snapshot_key_suffix}", list_secret_versions_1 - ) + @staticmethod + def _wait_force_deletion_completed(client, secret_id: str): + def _is_secret_deleted(): + deleted = False + try: + client.describe_secret(SecretId=secret_id) + except Exception as ex: + if ex.response["Error"]["Code"] == "ResourceNotFoundException": + deleted = True + else: + raise ex + return deleted - # As a result of the Lambda invocations. current version should be - # pointed to `AWSCURRENT` & previous version to `AWSPREVIOUS` - assert response["VersionIdsToStages"][initial_secret_version] == ["AWSPREVIOUS"] - assert response["VersionIdsToStages"][rot_res["VersionId"]] == ["AWSCURRENT"] + success = poll_condition(condition=_is_secret_deleted, timeout=120, interval=30) + if not success: + LOG.warning( + "Timed out whilst awaiting for force deletion of secret '%s' to complete.", + secret_id, + ) @staticmethod def _wait_rotation(client, secret_id: str, secret_version: str): @@ -622,6 +583,7 @@ def test_rotate_secret_with_lambda_success( create_secret, create_lambda_function, aws_client, + setup_rotation_secret, rotate_immediately, ): """ @@ -633,19 +595,35 @@ def test_rotate_secret_with_lambda_success( } if rotate_immediately: rotation_config["RotateImmediately"] = rotate_immediately - initial_secret_version, function_arn = self._setup_rotation_secret( - sm_snapshot, secret_name, create_secret, create_lambda_function, aws_client + initial_secret_version, function_arn = setup_rotation_secret + + rotation_config = rotation_config or {} + if function_arn: + rotation_config["RotationLambdaARN"] = function_arn + + rot_res = aws_client.secretsmanager.rotate_secret( + SecretId=secret_name, + **rotation_config, ) - self._assert_after_rotate_secret_with_lambda_success( - sm_snapshot, - secret_name, - initial_secret_version, - aws_client, - function_arn=function_arn, - rotation_config=rotation_config, + + sm_snapshot.match("rotate_secret_immediately", rot_res) + + self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) + + response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) + sm_snapshot.match("describe_secret_rotated", response) + + list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name ) - @pytest.mark.parametrize("set_rotation_on_second_run", [False, True]) + sm_snapshot.match("list_secret_versions_rotated_1", list_secret_versions_1) + + # As a result of the Lambda invocations. current version should be + # pointed to `AWSCURRENT` & previous version to `AWSPREVIOUS` + assert response["VersionIdsToStages"][initial_secret_version] == ["AWSPREVIOUS"] + assert response["VersionIdsToStages"][rot_res["VersionId"]] == ["AWSCURRENT"] + @markers.snapshot.skip_snapshot_verify( paths=["$..VersionIdsToStages", "$..Versions", "$..VersionId"] ) @@ -657,67 +635,74 @@ def test_rotate_secret_multiple_times_with_lambda_success( create_secret, create_lambda_function, aws_client, - set_rotation_on_second_run, + setup_rotation_secret, ): - rotation_config = { - "RotationRules": {"AutomaticallyAfterDays": 1}, - "RotateImmediately": True, + secret_initial_version, function_arn = setup_rotation_secret + runs_config = { + 1: { + "RotationRules": {"AutomaticallyAfterDays": 1}, + "RotateImmediately": True, + "RotationLambdaARN": function_arn, + }, + 2: {}, } - secret_initial_version, function_arn = self._setup_rotation_secret( - sm_snapshot, secret_name, create_secret, create_lambda_function, aws_client - ) - self._assert_after_rotate_secret_with_lambda_success( - sm_snapshot, - secret_name, - secret_initial_version, - aws_client, - function_arn=function_arn, - rotation_config=rotation_config, - ) - # Fetch the current version after first rotation - secret = aws_client.secretsmanager.get_secret_value(SecretId=secret_name) - self._assert_after_rotate_secret_with_lambda_success( - sm_snapshot, - secret_name, - secret["VersionId"], - aws_client, - snapshot_key_suffix="2", - function_arn=function_arn if set_rotation_on_second_run else None, - rotation_config=rotation_config if set_rotation_on_second_run else None, - ) + for index in range(1, 3): + rotation_config = runs_config[index] + + rot_res = aws_client.secretsmanager.rotate_secret( + SecretId=secret_name, + **rotation_config, + ) + + sm_snapshot.match(f"rotate_secret_immediately_{index}", rot_res) + + self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) + + response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) + sm_snapshot.match(f"describe_secret_rotated_{index}", response) + + list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name + ) + + sm_snapshot.match(f"list_secret_versions_rotated_1_{index}", list_secret_versions_1) + + # As a result of the Lambda invocations. current version should be + # pointed to `AWSCURRENT` & previous version to `AWSPREVIOUS` + assert response["VersionIdsToStages"][secret_initial_version] == ["AWSPREVIOUS"] + assert response["VersionIdsToStages"][rot_res["VersionId"]] == ["AWSCURRENT"] + + secret_initial_version = aws_client.secretsmanager.get_secret_value( + SecretId=secret_name + )["VersionId"] @markers.snapshot.skip_snapshot_verify(paths=["$..Error", "$..Message"]) @markers.aws.validated def test_rotate_secret_invalid_lambda_arn( - self, secret_name, aws_client, account_id, sm_snapshot + self, setup_invalid_rotation_secret, aws_client, sm_snapshot, secret_name, account_id ): region_name = aws_client.secretsmanager.meta.region_name invalid_arn = ( f"arn:aws:lambda:{region_name}:{account_id}:function:rotate_secret_invalid_lambda_arn" ) - self._setup_invalid_rotation_secret( - invalid_arn, "rotate_secret_invalid_arn_exc", sm_snapshot, secret_name, aws_client - ) + with pytest.raises(Exception) as e: + setup_invalid_rotation_secret(invalid_arn) + sm_snapshot.match("rotate_secret_invalid_arn_exc", e.value.response) + describe_secret = aws_client.secretsmanager.describe_secret(SecretId=secret_name) sm_snapshot.match("describe_secret", describe_secret) assert "RotationEnabled" not in describe_secret assert "RotationRules" not in describe_secret assert "RotationLambdaARN" not in describe_secret - @markers.snapshot.skip_snapshot_verify(paths=["$..Error", "$..Message"]) @markers.aws.validated def test_first_rotate_secret_with_missing_lambda_arn( - self, secret_name, aws_client, account_id, sm_snapshot + self, setup_invalid_rotation_secret, sm_snapshot ): - self._setup_invalid_rotation_secret( - None, "rotate_secret_no_arn_exc", sm_snapshot, secret_name, aws_client - ) - describe_secret = aws_client.secretsmanager.describe_secret(SecretId=secret_name) - sm_snapshot.match("describe_secret", describe_secret) - assert "RotationEnabled" not in describe_secret - assert "RotationRules" not in describe_secret - assert "RotationLambdaARN" not in describe_secret + with pytest.raises(Exception) as e: + setup_invalid_rotation_secret(None) + sm_snapshot.match("rotate_secret_no_arn_exc", e.value.response) @markers.aws.validated def test_put_secret_value_with_version_stages(self, sm_snapshot, secret_name, aws_client): diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index e1a0cc9f261a9..8e52ed68a419c 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -3687,18 +3687,18 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "recorded-date": "25-03-2025, 03:33:11", + "recorded-date": "30-03-2025, 11:45:42", "recorded-content": { - "rotate_secret_immediately_1": { + "rotate_secret_immediately": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe_secret_rotated_1": { + "describe_secret_rotated": { "ARN": "arn::secretsmanager::111111111111:secret:", "CreatedDate": "datetime", "Description": "testing rotation of secrets", @@ -3714,10 +3714,10 @@ }, "VersionIdsToStages": { "": [ - "AWSCURRENT" + "AWSPREVIOUS" ], "": [ - "AWSPREVIOUS" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -3725,7 +3725,7 @@ "HTTPStatusCode": 200 } }, - "list_secret_versions_rotated_1_1": { + "list_secret_versions_rotated_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", "Versions": [ @@ -3735,7 +3735,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3745,7 +3745,7 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSCURRENT" ] @@ -3759,18 +3759,18 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "recorded-date": "25-03-2025, 03:33:20", + "recorded-date": "30-03-2025, 11:45:54", "recorded-content": { - "rotate_secret_immediately_1": { + "rotate_secret_immediately": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe_secret_rotated_1": { + "describe_secret_rotated": { "ARN": "arn::secretsmanager::111111111111:secret:", "CreatedDate": "datetime", "Description": "testing rotation of secrets", @@ -3786,10 +3786,10 @@ }, "VersionIdsToStages": { "": [ - "AWSCURRENT" + "AWSPREVIOUS" ], "": [ - "AWSPREVIOUS" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -3797,7 +3797,7 @@ "HTTPStatusCode": 200 } }, - "list_secret_versions_rotated_1_1": { + "list_secret_versions_rotated_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", "Versions": [ @@ -3807,7 +3807,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3817,7 +3817,7 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSCURRENT" ] @@ -4583,152 +4583,53 @@ } } }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[True]": { - "recorded-date": "16-03-2025, 07:43:09", + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_first_rotate_secret_with_missing_lambda_arn": { + "recorded-date": "27-03-2025, 16:33:46", "recorded-content": { - "rotate_secret_immediately_1": { + "create_secret": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe_secret_rotated_1": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "CreatedDate": "datetime", - "Description": "testing rotation of secrets", - "LastAccessedDate": "datetime", - "LastChangedDate": "datetime", - "LastRotatedDate": "datetime", - "Name": "", - "NextRotationDate": "datetime", - "RotationEnabled": true, - "RotationLambdaARN": "", - "RotationRules": { - "AutomaticallyAfterDays": 1 - }, - "VersionIdsToStages": { - "": [ - "AWSPREVIOUS" - ], - "": [ - "AWSCURRENT" - ] + "rotate_secret_no_arn_exc": { + "Error": { + "Code": "InvalidRequestException", + "Message": "No Lambda rotation function ARN is associated with this secret." }, + "Message": "No Lambda rotation function ARN is associated with this secret.", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "list_secret_versions_rotated_1_1": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "Name": "", - "Versions": [ - { - "CreatedDate": "datetime", - "KmsKeyIds": [ - "DefaultEncryptionKey" - ], - "LastAccessedDate": "datetime", - "VersionId": "", - "VersionStages": [ - "AWSPREVIOUS" - ] - }, - { - "CreatedDate": "datetime", - "KmsKeyIds": [ - "DefaultEncryptionKey" - ], - "VersionId": "", - "VersionStages": [ - "AWSCURRENT" - ] - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "rotate_secret_immediately_2": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "Name": "", - "VersionId": "", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 + "HTTPStatusCode": 400 } }, - "describe_secret_rotated_2": { + "describe_secret": { "ARN": "arn::secretsmanager::111111111111:secret:", "CreatedDate": "datetime", - "Description": "testing rotation of secrets", - "LastAccessedDate": "datetime", "LastChangedDate": "datetime", - "LastRotatedDate": "datetime", "Name": "", - "NextRotationDate": "datetime", - "RotationEnabled": true, - "RotationLambdaARN": "", - "RotationRules": { - "AutomaticallyAfterDays": 1 - }, "VersionIdsToStages": { - "": [ + "": [ "AWSCURRENT" - ], - "": [ - "AWSPREVIOUS" ] }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - }, - "list_secret_versions_rotated_1_2": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "Name": "", - "Versions": [ - { - "CreatedDate": "datetime", - "KmsKeyIds": [ - "DefaultEncryptionKey" - ], - "LastAccessedDate": "datetime", - "VersionId": "", - "VersionStages": [ - "AWSPREVIOUS" - ] - }, - { - "CreatedDate": "datetime", - "KmsKeyIds": [ - "DefaultEncryptionKey" - ], - "VersionId": "", - "VersionStages": [ - "AWSCURRENT" - ] - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } } } }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[False]": { - "recorded-date": "16-03-2025, 07:42:51", + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success": { + "recorded-date": "29-03-2025, 09:40:15", "recorded-content": { "rotate_secret_immediately_1": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -4750,10 +4651,10 @@ }, "VersionIdsToStages": { "": [ - "AWSCURRENT" + "AWSPREVIOUS" ], "": [ - "AWSPREVIOUS" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -4771,7 +4672,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -4781,7 +4682,7 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSCURRENT" ] @@ -4816,7 +4717,7 @@ "AutomaticallyAfterDays": 1 }, "VersionIdsToStages": { - "": [ + "": [ "AWSPREVIOUS" ], "": [ @@ -4838,7 +4739,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -4860,45 +4761,5 @@ } } } - }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_first_rotate_secret_with_missing_lambda_arn": { - "recorded-date": "27-03-2025, 16:33:46", - "recorded-content": { - "create_secret": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "Name": "", - "VersionId": "", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "rotate_secret_no_arn_exc": { - "Error": { - "Code": "InvalidRequestException", - "Message": "No Lambda rotation function ARN is associated with this secret." - }, - "Message": "No Lambda rotation function ARN is associated with this secret.", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "describe_secret": { - "ARN": "arn::secretsmanager::111111111111:secret:", - "CreatedDate": "datetime", - "LastChangedDate": "datetime", - "Name": "", - "VersionIdsToStages": { - "": [ - "AWSCURRENT" - ] - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index fd6106b3abc98..d44fb5cb56bc5 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -104,20 +104,17 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_invalid_lambda_arn": { "last_validated_date": "2024-03-15T10:11:13+00:00" }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[False]": { - "last_validated_date": "2025-03-16T07:42:50+00:00" - }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success[True]": { - "last_validated_date": "2025-03-16T07:43:08+00:00" + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success": { + "last_validated_date": "2025-03-29T09:40:15+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success": { "last_validated_date": "2024-03-15T08:12:22+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "last_validated_date": "2025-03-25T03:33:19+00:00" + "last_validated_date": "2025-03-30T11:45:54+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "last_validated_date": "2025-03-25T03:33:10+00:00" + "last_validated_date": "2025-03-30T11:45:41+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists": { "last_validated_date": "2024-03-15T08:14:33+00:00"