import time import uuid import logging import pytest import httpx from botocore.exceptions import ClientError from httpx import ConnectTimeout from mypy_boto3_secretsmanager import SecretsManagerClient from src.models import M2MToken logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) SECRET_ID = "test/lambda-test-m2m-client/M2M_JWT_ACCESS_TOKEN" QA_CLIENT_CREDENTIALS_SECRET_ID = ( "qa/lambda-test-m2m-client/M2M_AUTH0_CLIENT_CREDENTIALS" ) TEST_CLIENT_CREDENTIALS_SECRET_ID = ( "test/lambda-test-m2m-client/M2M_AUTH0_CLIENT_CREDENTIALS" ) OWS_PDP_SELF_ROLES_PATH = "https://qa-ows-pdp.theorchard.io/identity/self/roles/" def test_secret_rotation_flow( client: SecretsManagerClient, fixture_client_credentials: None ) -> None: """ Integration test to verify the rotation flow of a secret in AWS Secrets Manager. This test: - Verifies the application is allowed to return self roles. - Ensures the new secret value is different from the current. """ try: current_secret_value = client.get_secret_value( SecretId=SECRET_ID, VersionStage="AWSCURRENT" )["SecretString"] new_client_token = rotate_secret(client, SECRET_ID) new_secret_value = get_secret_with_retry(client, new_client_token) current_m2m_token = M2MToken.model_validate_json(current_secret_value) new_m2m_token = M2MToken.model_validate_json(new_secret_value) assert ( current_m2m_token.token != new_m2m_token.token ), "New token shouldn't match current token" assert is_application_allowed_to_return_self_roles( new_secret_value ), "Application should be allowed to return self roles" except ClientError as e: pytest.fail(f"ClientError: {e}") @pytest.fixture() def fixture_client_credentials(client: SecretsManagerClient) -> None: """Ensure test client credentials use the qa environment's client credentials. This is done in case the client credentials were rotated by another process (see PP-977). """ qa_client_credentials = client.get_secret_value( SecretId=QA_CLIENT_CREDENTIALS_SECRET_ID )["SecretString"] client.put_secret_value( SecretId=TEST_CLIENT_CREDENTIALS_SECRET_ID, SecretString=qa_client_credentials ) def rotate_secret(client: SecretsManagerClient, secret_id: str) -> str: """ Rotate the secret in AWS Secrets Manager. """ token = uuid.uuid4() try: client.rotate_secret(SecretId=secret_id, ClientRequestToken=str(token)) logger.info(f"Secret rotation initiated with token: {token}") except ClientError as e: logger.error(f"Failed to rotate secret: {e}") raise return str(token) def is_application_allowed_to_return_self_roles(secret_str: str) -> bool: """ Verify if the application is allowed to return self roles using the provided secret. """ m2m_token = M2MToken.model_validate_json(secret_str) try: response = httpx.get( OWS_PDP_SELF_ROLES_PATH, headers={ "Authorization": f"Bearer {m2m_token.token}", "Content-Type": "application/json", }, ) except ConnectTimeout as e: logger.error(f"ConnectTimeout: {e}") return False return response.status_code == 200 and response.json() is not None def get_secret_with_retry( client: SecretsManagerClient, token: str, max_attempts: int = 10, wait_time: int = 10, ) -> str: """ Attempt to retrieve the pending secret value with retries. """ attempts = 0 while attempts < max_attempts: time.sleep(wait_time) try: secret_value = client.get_secret_value( SecretId=SECRET_ID, VersionStage="AWSCURRENT", VersionId=token ) if "SecretString" in secret_value: return secret_value["SecretString"] except ClientError as e: logger.warning(f"Attempt {attempts + 1} failed: {e}") attempts += 1 pytest.fail("Failed to retrieve pending secret after multiple attempts.")