diff --git a/.github/workflows/ci_examples_python.yml b/.github/workflows/ci_examples_python.yml index 6eb659ca0..c400bdc40 100644 --- a/.github/workflows/ci_examples_python.yml +++ b/.github/workflows/ci_examples_python.yml @@ -95,4 +95,4 @@ jobs: # Run simple examples tox -e dynamodbencryption # Run migration examples - # tox -e migration + tox -e migration diff --git a/.github/workflows/ci_test_python.yml b/.github/workflows/ci_test_python.yml index 51be31292..3d43479e6 100644 --- a/.github/workflows/ci_test_python.yml +++ b/.github/workflows/ci_test_python.yml @@ -121,6 +121,7 @@ jobs: shell: bash run: | tox -e integ + tox -e legacyinteg - name: Test ${{ matrix.library }} Python coverage working-directory: ./${{ matrix.library }}/runtimes/python diff --git a/DynamoDbEncryption/runtimes/python/pyproject.toml b/DynamoDbEncryption/runtimes/python/pyproject.toml index 8456520b3..e30d9a1e7 100644 --- a/DynamoDbEncryption/runtimes/python/pyproject.toml +++ b/DynamoDbEncryption/runtimes/python/pyproject.toml @@ -13,6 +13,12 @@ include = ["**/internaldafny/generated/*.py"] [tool.poetry.dependencies] python = "^3.11.0" aws-cryptographic-material-providers = { path = "../../../submodules/MaterialProviders/AwsCryptographicMaterialProviders/runtimes/python", develop = false} +# Optional dependencies +# Should only include the legacy library if migrating from the legacy library +dynamodb_encryption_sdk = { version = "^3.3.0", optional = true } + +[tool.poetry.extras] +legacy-ddbec = ["dynamodb_encryption_sdk"] # Package testing diff --git a/DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/internaldafny/extern/InternalLegacyOverride.py b/DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/internaldafny/extern/InternalLegacyOverride.py new file mode 100644 index 000000000..8c42812e4 --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/internaldafny/extern/InternalLegacyOverride.py @@ -0,0 +1,268 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from _dafny import Seq + +import aws_dbesdk_dynamodb.internaldafny.generated.InternalLegacyOverride +from aws_dbesdk_dynamodb.internaldafny.generated.AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes import ( + DynamoDbItemEncryptorConfig_DynamoDbItemEncryptorConfig, + Error_DynamoDbItemEncryptorException, + Error_Opaque, + DecryptItemInput_DecryptItemInput, + EncryptItemInput_EncryptItemInput, +) +from aws_dbesdk_dynamodb.internaldafny.generated.AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes import ( + CryptoAction_ENCRYPT__AND__SIGN, + CryptoAction_SIGN__ONLY, + CryptoAction_DO__NOTHING, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.references import ( + ILegacyDynamoDbEncryptor, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.models import ( + EncryptItemInput, + EncryptItemOutput, + DecryptItemOutput, + DecryptItemInput, +) + +try: + from dynamodb_encryption_sdk.encrypted.client import EncryptedClient + from dynamodb_encryption_sdk.encrypted.table import EncryptedTable + from dynamodb_encryption_sdk.encrypted.resource import EncryptedResource + from dynamodb_encryption_sdk.encrypted.client import EncryptedPaginator + from dynamodb_encryption_sdk.encrypted.item import encrypt_dynamodb_item, decrypt_dynamodb_item + from dynamodb_encryption_sdk.structures import EncryptionContext, AttributeActions + from dynamodb_encryption_sdk.identifiers import CryptoAction + from dynamodb_encryption_sdk.encrypted import CryptoConfig + from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes + + _HAS_LEGACY_DDBEC = True +except ImportError: + _HAS_LEGACY_DDBEC = False + + +class InternalLegacyOverride(aws_dbesdk_dynamodb.internaldafny.generated.InternalLegacyOverride.InternalLegacyOverride): + def __init__(self): + super().__init__() + self.crypto_config = None + self.policy = None + + @staticmethod + def Build(config: DynamoDbItemEncryptorConfig_DynamoDbItemEncryptorConfig): + # Check for early return (Postcondition): If there is no legacyOverride there is nothing to do. + if config.legacyOverride.is_None: + return InternalLegacyOverride.CreateBuildSuccess(InternalLegacyOverride.CreateInternalLegacyOverrideNone()) + + legacy_override = config.legacyOverride.value + + # Precondition: The encryptor MUST be a DynamoDBEncryptor + if not _HAS_LEGACY_DDBEC: + return InternalLegacyOverride.CreateBuildFailure( + InternalLegacyOverride.CreateError("Could not find aws-dynamodb-encryption-python installation") + ) + + # Precondition: The encryptor MUST be one of the supported legacy types + if not ( + isinstance(legacy_override.encryptor, EncryptedClient) + or isinstance(legacy_override.encryptor, EncryptedTable) + or isinstance(legacy_override.encryptor, EncryptedResource) + ): + return InternalLegacyOverride.CreateBuildFailure( + InternalLegacyOverride.CreateError("Legacy encryptor is not supported") + ) + + # Preconditions: MUST be able to create valid encryption context + maybe_encryption_context = InternalLegacyOverride.legacyEncryptionContext(config) + if maybe_encryption_context.is_Failure: + return maybe_encryption_context + + # Precondition: All actions MUST be supported types + maybe_actions = InternalLegacyOverride.legacyActions(legacy_override.attributeActionsOnEncrypt) + if maybe_actions.is_Failure: + return maybe_actions + + # Create and return the legacy override instance + legacy_instance = InternalLegacyOverride() + legacy_instance.policy = legacy_override.policy + legacy_instance.crypto_config = CryptoConfig( + materials_provider=legacy_override.encryptor._materials_provider, + encryption_context=maybe_encryption_context.value, + attribute_actions=maybe_actions.value, + ) + return InternalLegacyOverride.CreateBuildSuccess( + InternalLegacyOverride.CreateInternalLegacyOverrideSome(legacy_instance) + ) + + @staticmethod + def legacyEncryptionContext(config: DynamoDbItemEncryptorConfig_DynamoDbItemEncryptorConfig): + """Create the legacy encryption context from the config.""" + try: + # Convert Dafny types to Python strings for the encryption context + table_name = InternalLegacyOverride.DafnyStringToNativeString(config.logicalTableName) + partition_key_name = InternalLegacyOverride.DafnyStringToNativeString(config.partitionKeyName) + sort_key_name = ( + InternalLegacyOverride.DafnyStringToNativeString(config.sortKeyName.value) + if config.sortKeyName.is_Some + else None + ) + + # Create the legacy encryption context with the extracted values + encryption_context = EncryptionContext( + table_name=table_name, + partition_key_name=partition_key_name, + sort_key_name=sort_key_name, + ) + + return InternalLegacyOverride.CreateBuildSuccess(encryption_context) + except Exception as ex: + return InternalLegacyOverride.CreateBuildFailure(Error_Opaque(ex)) + + @staticmethod + def legacyActions(attribute_actions_on_encrypt): + """Create the legacy attribute actions from the config.""" + try: + # Create a new AttributeActions with default ENCRYPT_AND_SIGN + # Default Action to take if no specific action is defined in ``attribute_actions`` + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/DDBEC-legacy-concepts.html#legacy-attribute-actions + legacy_actions = AttributeActions(default_action=CryptoAction.ENCRYPT_AND_SIGN) + + # Map the action from the config to legacy actions + attribute_actions = {} + for key, action in attribute_actions_on_encrypt.items: + key_str = InternalLegacyOverride.DafnyStringToNativeString(key) + + # Map the action type to the appropriate CryptoAction + if action == CryptoAction_ENCRYPT__AND__SIGN(): + attribute_actions[key_str] = CryptoAction.ENCRYPT_AND_SIGN + elif action == CryptoAction_SIGN__ONLY(): + attribute_actions[key_str] = CryptoAction.SIGN_ONLY + elif action == CryptoAction_DO__NOTHING(): + attribute_actions[key_str] = CryptoAction.DO_NOTHING + else: + return InternalLegacyOverride.CreateBuildFailure( + InternalLegacyOverride.CreateError(f"Unknown action type: {action}") + ) + + # Update the attribute_actions dictionary + legacy_actions.attribute_actions = attribute_actions + return InternalLegacyOverride.CreateBuildSuccess(legacy_actions) + except Exception as ex: + return InternalLegacyOverride.CreateBuildFailure(Error_Opaque(ex)) + + def EncryptItem(self, input: EncryptItemInput_EncryptItemInput): + """Encrypt an item using the legacy DynamoDB encryptor. + + :param input: EncryptItemInput containing the plaintext item to encrypt + :returns Result containing the encrypted item or an error + """ + try: + # Precondition: Policy MUST allow the caller to encrypt. + if not self.policy.is_FORCE__LEGACY__ENCRYPT__ALLOW__LEGACY__DECRYPT: + return self.CreateEncryptItemFailure( + InternalLegacyOverride.CreateError("Legacy policy does not support encrypt") + ) + + # Get the Native Plaintext Item + native_input = aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.dafny_to_smithy.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor_EncryptItemInput( + input + ) + + # Encrypt the item using the instance attributes + encrypted_item = encrypt_dynamodb_item( + item=native_input.plaintext_item, + crypto_config=self.crypto_config.with_item(native_input.plaintext_item), + ) + + # Return the encrypted item + # The legacy encryption method returns items in the format that Dafny expects, + # so no additional conversion is needed here + native_output = EncryptItemOutput(encrypted_item=encrypted_item, parsed_header=None) + dafny_output = aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.smithy_to_dafny.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor_EncryptItemOutput( + native_output + ) + return self.CreateEncryptItemSuccess(dafny_output) + + except Exception as ex: + return self.CreateEncryptItemFailure(InternalLegacyOverride.CreateError(Error_Opaque(ex))) + + def DecryptItem(self, input: DecryptItemInput_DecryptItemInput): + """Decrypt an item using the legacy DynamoDB encryptor. + + :param input: DecryptItemInput containing the encrypted item to decrypt + :returns Result containing the decrypted item or an error + """ + try: + # Precondition: Policy MUST allow the caller to decrypt. + # = specification/dynamodb-encryption-client/decrypt-item.md#behavior + ## If a [Legacy Policy](./ddb-table-encryption-config.md#legacy-policy) of + ## `FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT` is configured, + ## and the input item [is an item written in the legacy format](#determining-legacy-items), + ## this operation MUST fail. + if not ( + self.policy.is_FORCE__LEGACY__ENCRYPT__ALLOW__LEGACY__DECRYPT + or self.policy.is_FORBID__LEGACY__ENCRYPT__ALLOW__LEGACY__DECRYPT + ): + return self.CreateDecryptItemFailure( + InternalLegacyOverride.CreateError("Legacy policy does not support decrypt") + ) + + # Get the Native DecryptItemInput + native_input: DecryptItemInput = ( + aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.dafny_to_smithy.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor_DecryptItemInput( + input + ) + ) + # Decrypt the item using the instance attributes + decrypted_item = decrypt_dynamodb_item( + item=native_input.encrypted_item, + crypto_config=self.crypto_config.with_item(native_input.encrypted_item), + ) + + native_output = DecryptItemOutput(plaintext_item=decrypted_item, parsed_header=None) + dafny_output = aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.smithy_to_dafny.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor_DecryptItemOutput( + native_output + ) + return self.CreateDecryptItemSuccess(dafny_output) + except Exception as ex: + return self.CreateDecryptItemFailure(InternalLegacyOverride.CreateError(Error_Opaque(ex))) + + def IsLegacyInput(self, input: DecryptItemInput_DecryptItemInput): + """ + Determine if the input is from a legacy client. + + :param input: The decrypt item input to check + :returns Boolean indicating if the input is from a legacy client + """ + if not input.is_DecryptItemInput: + return False + + # Get the Native DecryptItemInput + native_input: DecryptItemInput = ( + aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.dafny_to_smithy.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor_DecryptItemInput( + input + ) + ) + # = specification/dynamodb-encryption-client/decrypt-item.md#determining-legacy-items + ## An item MUST be determined to be encrypted under the legacy format if it contains + ## attributes for the material description and the signature. + return ( + "*amzn-ddb-map-desc*" in native_input.encrypted_item and "*amzn-ddb-map-sig*" in native_input.encrypted_item + ) + + @staticmethod + def DafnyStringToNativeString(dafny_input): + return b"".join(ord(c).to_bytes(2, "big") for c in dafny_input).decode("utf-16-be") + + @staticmethod + def NativeStringToDafnyString(native_input): + return Seq( + "".join([chr(int.from_bytes(pair, "big")) for pair in zip(*[iter(native_input.encode("utf-16-be"))] * 2)]) + ) + + @staticmethod + def CreateError(message): + """Create an Error with the given message.""" + return Error_DynamoDbItemEncryptorException(InternalLegacyOverride.NativeStringToDafnyString(message)) + + +aws_dbesdk_dynamodb.internaldafny.generated.InternalLegacyOverride.InternalLegacyOverride = InternalLegacyOverride diff --git a/DynamoDbEncryption/runtimes/python/test/integ/legacy/__init__.py b/DynamoDbEncryption/runtimes/python/test/integ/legacy/__init__.py new file mode 100644 index 000000000..37c9c2f01 --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/test/integ/legacy/__init__.py @@ -0,0 +1,18 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + + +def sort_dynamodb_json_lists(obj): + """ + Utility that recursively sorts all lists in a DynamoDB JSON-like structure. + DynamoDB JSON uses lists to represent sets, so strict equality can fail. + Sort lists to ensure consistent ordering when comparing expected and actual items. + """ + if isinstance(obj, dict): + return {k: sort_dynamodb_json_lists(v) for k, v in obj.items()} + elif isinstance(obj, list): + try: + return sorted(obj) # Sort lists for consistent comparison + except TypeError: + return obj # Not all lists are sortable; ex. complex_item_ddb's "list" attribute + return obj diff --git a/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_client.py b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_client.py new file mode 100644 index 000000000..0900b5b5e --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_client.py @@ -0,0 +1,256 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import uuid +from copy import deepcopy + +import boto3 +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from ...constants import ( + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME, +) +from ...items import ( + complex_item_ddb, + complex_item_dict, + complex_key_ddb, + complex_key_dict, + simple_item_ddb, + simple_item_dict, + simple_key_ddb, + simple_key_dict, +) +from ...requests import ( + basic_delete_item_request_ddb, + basic_delete_item_request_dict, + basic_get_item_request_ddb, + basic_get_item_request_dict, + basic_put_item_request_ddb, + basic_put_item_request_dict, +) +from . import sort_dynamodb_json_lists +from .utils import ( + create_legacy_encrypted_client, + create_legacy_encrypted_resource, + create_legacy_encrypted_table, + encrypted_client_with_legacy_override, + legacy_actions, +) + + +# Creates a matrix of tests for each value in param, +# with a user-friendly string for test output: +# expect_standard_dictionaries = True -> "standard_dicts" +# expect_standard_dictionaries = False -> "ddb_json" +@pytest.fixture(params=[True, False], ids=["standard_dicts", "ddb_json"]) +def expect_standard_dictionaries(request): + return request.param + + +# Creates a matrix of tests for each value in param, +# with a user-friendly string for test output: +# use_complex_item = True -> "complex_item" +# use_complex_item = False -> "simple_item" +@pytest.fixture(params=[True, False], ids=["complex_item", "simple_item"]) +def use_complex_item(request): + return request.param + + +# Append a suffix to the partition key to avoid collisions between test runs. +@pytest.fixture(scope="module") +def test_run_suffix(): + return "-" + str(uuid.uuid4()) + + +@pytest.fixture +def test_item(expect_standard_dictionaries, use_complex_item, test_run_suffix): + """Get a single test item in the appropriate format for the client.""" + if expect_standard_dictionaries: + if use_complex_item: + item = deepcopy(complex_item_dict) + else: + item = deepcopy(simple_item_dict) + else: + if use_complex_item: + item = deepcopy(complex_item_ddb) + else: + item = deepcopy(simple_item_ddb) + # Add a suffix to the partition key to avoid collisions between test runs. + if isinstance(item["partition_key"], dict): + item["partition_key"]["S"] += test_run_suffix + else: + item["partition_key"] += test_run_suffix + return item + + +@pytest.fixture +def test_key(expect_standard_dictionaries, use_complex_item, test_run_suffix): + """Get a single test item in the appropriate format for the client.""" + if expect_standard_dictionaries: + if use_complex_item: + key = deepcopy(complex_key_dict) + else: + key = deepcopy(simple_key_dict) + else: + if use_complex_item: + key = deepcopy(complex_key_ddb) + else: + key = deepcopy(simple_key_ddb) + # Add a suffix to the partition key to avoid collisions between test runs. + if isinstance(key["partition_key"], dict): + key["partition_key"]["S"] += test_run_suffix + else: + key["partition_key"] += test_run_suffix + return key + + +@pytest.fixture +def put_item_request(expect_standard_dictionaries, test_item): + if expect_standard_dictionaries: + # Client requests with `expect_standard_dictionaries=True` use dict-formatted requests + # with an added "TableName" key. + return {**basic_put_item_request_dict(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME} + return basic_put_item_request_ddb(test_item) + + +@pytest.fixture +def get_item_request(expect_standard_dictionaries, test_item): + if expect_standard_dictionaries: + # Client requests with `expect_standard_dictionaries=True` use dict-formatted requests + # with an added "TableName" key. + return {**basic_get_item_request_dict(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME} + return basic_get_item_request_ddb(test_item) + + +@pytest.fixture +def delete_item_request(expect_standard_dictionaries, test_item): + if expect_standard_dictionaries: + return {**basic_delete_item_request_dict(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME} + return basic_delete_item_request_ddb(test_item) + + +# Fixtures for legacy encryptors and clients + + +@pytest.fixture(params=["client", "table", "resource"], ids=["legacy_client", "legacy_table", "legacy_resource"]) +def legacy_encryptor(request): + """Create a legacy encryptor of the specified type.""" + if request.param == "client": + return create_legacy_encrypted_client() + elif request.param == "table": + return create_legacy_encrypted_table() + elif request.param == "resource": + return create_legacy_encrypted_resource() + + +@pytest.fixture( + params=[ + LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT, + ] +) +def legacy_policy(request): + return request.param + + +@pytest.fixture +def encrypted_client(legacy_encryptor, legacy_policy, expect_standard_dictionaries): + return encrypted_client_with_legacy_override( + legacy_encryptor=legacy_encryptor, + legacy_policy=legacy_policy, + expect_standard_dictionaries=expect_standard_dictionaries, + ) + + +def test_GIVEN_awsdbe_encrypted_item_WHEN_get_with_legacy_client( + encrypted_client, + put_item_request, + get_item_request, + delete_item_request, + expect_standard_dictionaries, + legacy_policy, +): + # Given: Valid put_item request + # When: put_item + put_response = encrypted_client.put_item(**put_item_request) + # Then: put_item succeeds + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Given: Fresh legacy encryptor of the same type as used in the fixture + legacy_encrypted_client = create_legacy_encrypted_client( + attribute_actions=legacy_actions(), + expect_standard_dictionaries=expect_standard_dictionaries, + ) + + if legacy_policy == LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT: + # Given: Valid get_item request for the same item using legacy encryptor with FORCE_LEGACY_ENCRYPT policy + # When: get_item with legacy encryptor + get_response = legacy_encrypted_client.get_item(**get_item_request) + # Then: Response is equal to the original item (legacy encryptor can decrypt item written by AWS DB-ESDK) + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + # DynamoDB JSON uses lists to represent sets, so strict equality can fail. + # Sort lists to ensure consistent ordering when comparing expected and actual items. + expected_item = sort_dynamodb_json_lists(put_item_request["Item"]) + legacy_actual_item = sort_dynamodb_json_lists(get_response["Item"]) + assert expected_item == legacy_actual_item + else: + # Given: Valid get_item request for the same item using legacy encryptor with FORBID_LEGACY_ENCRYPT policy + # When: get_item with legacy encryptor + # Then: throws DecryptionError Exception (i.e. legacy encryptor cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + # Try to read the item with the legacy encryptor + legacy_encrypted_client.get_item(**get_item_request) + + +def test_GIVEN_legacy_encrypted_item_WHEN_get_with_awsdbe( + encrypted_client, + put_item_request, + get_item_request, + delete_item_request, + expect_standard_dictionaries, + legacy_policy, +): + # Given: Fresh legacy encryptor and valid put_item request + legacy_encrypted_client = create_legacy_encrypted_client( + attribute_actions=legacy_actions(), + expect_standard_dictionaries=expect_standard_dictionaries, + ) + # When: put_item using legacy encryptor + put_response = legacy_encrypted_client.put_item(**put_item_request) + # Then: put_item succeeds (item is written using legacy format) + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + if not legacy_policy == LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT: + # Given: Valid get_item request for the same item with ALLOW_LEGACY_DECRYPT policy + # When: get_item using AWS DB-ESDK client + get_response = encrypted_client.get_item(**get_item_request) + # Then: Response is equal to the original item (AWS DB ESDK can decrypt legacy items) + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + # DynamoDB JSON uses lists to represent sets, so strict equality can fail. + # Sort lists to ensure consistent ordering when comparing expected and actual items. + expected_item = sort_dynamodb_json_lists(put_item_request["Item"]) + actual_item = sort_dynamodb_json_lists(get_response["Item"]) + assert expected_item == actual_item + else: + # Given: Valid get_item request for the same item with FORBID_LEGACY_DECRYPT policy + # When: get_item using AWS DBE SDK client + # Then: Throws a DynamoDbItemEncryptor exception (AWS DB-ESDK with FORBID policy cannot decrypt legacy items) + with pytest.raises(DynamoDbItemEncryptor): + encrypted_client.get_item(**get_item_request) + + +# Delete the items in the table after the module runs +@pytest.fixture(scope="module", autouse=True) +def cleanup_after_module(test_run_suffix): + yield + table = boto3.client("dynamodb") + items = [deepcopy(simple_item_ddb), deepcopy(complex_item_ddb)] + for item in items: + item["partition_key"]["S"] += test_run_suffix + table.delete_item(**basic_delete_item_request_ddb(item)) diff --git a/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_paginator.py b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_paginator.py new file mode 100644 index 000000000..6f170ac52 --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_paginator.py @@ -0,0 +1,365 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import uuid +from copy import deepcopy + +import boto3 +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from ...constants import ( + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME, +) +from ...items import ( + complex_item_ddb, + complex_item_dict, + complex_key_ddb, + complex_key_dict, + simple_item_ddb, + simple_item_dict, + simple_key_ddb, + simple_key_dict, +) +from ...requests import ( + basic_delete_item_request_ddb, + basic_put_item_request_ddb, + basic_put_item_request_dict, + basic_query_paginator_request, + basic_scan_paginator_request, +) +from . import sort_dynamodb_json_lists +from .utils import ( + create_legacy_encrypted_client, + create_legacy_encrypted_resource, + create_legacy_encrypted_table, + encrypted_client_with_legacy_override, + legacy_actions, +) + + +# Creates a matrix of tests for each value in param, +# with a user-friendly string for test output: +# expect_standard_dictionaries = True -> "standard_dicts" +# expect_standard_dictionaries = False -> "ddb_json" +@pytest.fixture(params=[True, False], ids=["standard_dicts", "ddb_json"]) +def expect_standard_dictionaries(request): + return request.param + + +# Creates a matrix of tests for each value in param, +# with a user-friendly string for test output: +# use_complex_item = True -> "complex_item" +# use_complex_item = False -> "simple_item" +@pytest.fixture(params=[True, False], ids=["complex_item", "simple_item"]) +def use_complex_item(request): + return request.param + + +# Append a suffix to the partition key to avoid collisions between test runs. +@pytest.fixture(scope="module") +def test_run_suffix(): + return "-" + str(uuid.uuid4()) + + +@pytest.fixture +def test_key(expect_standard_dictionaries, use_complex_item, test_run_suffix): + """Get a single test item in the appropriate format for the client.""" + if expect_standard_dictionaries: + if use_complex_item: + key = deepcopy(complex_key_dict) + else: + key = deepcopy(simple_key_dict) + else: + if use_complex_item: + key = deepcopy(complex_key_ddb) + else: + key = deepcopy(simple_key_ddb) + # Add a suffix to the partition key to avoid collisions between test runs. + if isinstance(key["partition_key"], dict): + key["partition_key"]["S"] += test_run_suffix + else: + key["partition_key"] += test_run_suffix + return key + + +@pytest.fixture +def test_item(expect_standard_dictionaries, use_complex_item, test_run_suffix): + """Get a single test item in the appropriate format for the client.""" + if expect_standard_dictionaries: + if use_complex_item: + item = deepcopy(complex_item_dict) + else: + item = deepcopy(simple_item_dict) + else: + if use_complex_item: + item = deepcopy(complex_item_ddb) + else: + item = deepcopy(simple_item_ddb) + # Add a suffix to the partition key to avoid collisions between test runs. + if isinstance(item["partition_key"], dict): + item["partition_key"]["S"] += test_run_suffix + else: + item["partition_key"] += test_run_suffix + return item + + +@pytest.fixture +def paginate_query_request(expect_standard_dictionaries, test_key): + if expect_standard_dictionaries: + return {**basic_query_paginator_request(test_key), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME} + return basic_query_paginator_request(test_key) + + +@pytest.fixture +def put_item_request(expect_standard_dictionaries, test_item): + if expect_standard_dictionaries: + # Client requests with `expect_standard_dictionaries=True` use dict-formatted requests + # with an added "TableName" key. + return {**basic_put_item_request_dict(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME} + return basic_put_item_request_ddb(test_item) + + +@pytest.fixture +def paginate_scan_request(expect_standard_dictionaries, test_item): + """Get a scan paginator request in the appropriate format for the client.""" + if expect_standard_dictionaries: + request = {**basic_scan_paginator_request(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME} + else: + request = basic_scan_paginator_request(test_item) + return request + + +# Fixtures for legacy encryptors and clients + + +@pytest.fixture(params=["client", "table", "resource"], ids=["legacy_client", "legacy_table", "legacy_resource"]) +def legacy_encryptor(request): + """ + Create a legacy encryptor of the specified type. + + This fixture creates legacy encryptors of three types: + - client: DynamoDB Encryption Client's EncryptedClient + - table: DynamoDB Encryption Client's EncryptedTable + - resource: DynamoDB Encryption Client's EncryptedResource + """ + if request.param == "client": + return create_legacy_encrypted_client() + elif request.param == "table": + return create_legacy_encrypted_table() + elif request.param == "resource": + return create_legacy_encrypted_resource() + + +# Fixtures for each legacy policy +@pytest.fixture( + params=[ + LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT, + ] +) +def legacy_policy(request): + return request.param + + +@pytest.fixture +def encrypted_client(legacy_encryptor, legacy_policy, expect_standard_dictionaries): + return encrypted_client_with_legacy_override( + legacy_encryptor=legacy_encryptor, + legacy_policy=legacy_policy, + expect_standard_dictionaries=expect_standard_dictionaries, + ) + + +@pytest.fixture +def client_legacy_force_encrypt_allow_decrypt(legacy_encryptor, expect_standard_dictionaries): + """Create AWS DBE SDK client with FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT policy.""" + return encrypted_client_with_legacy_override( + legacy_encryptor=legacy_encryptor, + legacy_policy=LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + expect_standard_dictionaries=expect_standard_dictionaries, + ) + + +def test_GIVEN_awsdbe_encrypted_item_WHEN_paginate_with_legacy_query_paginator( + encrypted_client, put_item_request, paginate_query_request, test_item, legacy_policy, expect_standard_dictionaries +): + # Given: Valid put_item request + # When: put_item using AWS DB-ESDK client + put_response = encrypted_client.put_item(**put_item_request) + # Then: Item is stored in the table + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Given: Fresh legacy encrypted client and query paginator + legacy_encrypted_client = create_legacy_encrypted_client( + attribute_actions=legacy_actions(), + expect_standard_dictionaries=expect_standard_dictionaries, + ) + legacy_query_paginator = legacy_encrypted_client.get_paginator("query") + + if legacy_policy == LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT: + # When: Paginate with legacy query paginator using FORCE_LEGACY_ENCRYPT policy + # Then: Legacy paginator can read and decrypt items + response = legacy_query_paginator.paginate(**paginate_query_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + assert len(items) == 1 + expected_item = sort_dynamodb_json_lists(test_item) + legacy_actual_item = sort_dynamodb_json_lists(items[0]) + assert expected_item == legacy_actual_item + else: + # When: Paginate with legacy query paginator using FORBID policies + # Then: Legacy paginator cannot decrypt items created with FORBID_LEGACY_ENCRYPT policy + with pytest.raises(DecryptionError): + response = legacy_query_paginator.paginate(**paginate_query_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + +def test_GIVEN_awsdbe_encrypted_item_WHEN_paginate_with_legacy_scan_paginator( + encrypted_client, put_item_request, paginate_scan_request, test_item, legacy_policy, expect_standard_dictionaries +): + # Given: Valid put_item request + # When: put_item using AWS DB-ESDK client + put_response = encrypted_client.put_item(**put_item_request) + # Then: Item is stored in the table + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Given: Fresh legacy encrypted client and scan paginator + legacy_encrypted_client = create_legacy_encrypted_client( + attribute_actions=legacy_actions(), + expect_standard_dictionaries=expect_standard_dictionaries, + ) + legacy_scan_paginator = legacy_encrypted_client.get_paginator("scan") + + if legacy_policy == LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT: + # When: Paginate with legacy scan paginator using FORCE_LEGACY_ENCRYPT policy + # Then: Legacy paginator can read and decrypt items + response = legacy_scan_paginator.paginate(**paginate_scan_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + assert len(items) == 1 + expected_item = sort_dynamodb_json_lists(test_item) + legacy_actual_item = sort_dynamodb_json_lists(items[0]) + assert expected_item == legacy_actual_item + else: + # When: Paginate with legacy scan paginator using FORBID policies + # Then: Legacy paginator cannot decrypt items created with FORBID_LEGACY_ENCRYPT policy + with pytest.raises(DecryptionError): + response = legacy_scan_paginator.paginate(**paginate_scan_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + +def test_GIVEN_legacy_encrypted_item_WHEN_paginate_with_awsdbe_query_paginator( + encrypted_client, put_item_request, paginate_query_request, test_item, legacy_policy, expect_standard_dictionaries +): + # Given: Fresh legacy encrypted client and valid put_item request + legacy_encrypted_client = create_legacy_encrypted_client( + attribute_actions=legacy_actions(), + expect_standard_dictionaries=expect_standard_dictionaries, + ) + # When: put_item using legacy client + legacy_encrypted_client.put_item(**put_item_request) + # Then: Item is stored in the table + + # Given: Query paginator with AWS DB-ESDK client + query_paginator = encrypted_client.get_paginator("query") + + if not legacy_policy == LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT: + # When: Paginate with AWS DB-ESDK query paginator using ALLOW_LEGACY_DECRYPT policies + # Then: AWS DB-ESDK paginator can read the legacy-encrypted item + response = query_paginator.paginate(**paginate_query_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + assert len(items) == 1 + expected_item = sort_dynamodb_json_lists(test_item) + actual_item = sort_dynamodb_json_lists(items[0]) + assert expected_item == actual_item + else: + # Given: Valid paginate request with FORBID_LEGACY_DECRYPT policy + # When: Paginate with AWS DB-ESDK client + # Then: Throws a DynamoDbItemEncryptor exception (AWS DB-ESDK with FORBID policy cannot decrypt legacy items) + with pytest.raises(DynamoDbItemEncryptor): + response = query_paginator.paginate(**paginate_query_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + +def test_GIVEN_legacy_encrypted_item_WHEN_paginate_with_awsdbe_scan_paginator( + encrypted_client, put_item_request, paginate_scan_request, test_item, legacy_policy, expect_standard_dictionaries +): + # Given: Fresh legacy encrypted client and valid put_item request + legacy_encrypted_client = create_legacy_encrypted_client( + attribute_actions=legacy_actions(), + expect_standard_dictionaries=expect_standard_dictionaries, + ) + # When: put_item using legacy client + legacy_encrypted_client.put_item(**put_item_request) + # Then: Item is stored in the table + + # Given: Scan paginator with AWS DB-ESDK client + scan_paginator = encrypted_client.get_paginator("scan") + + if not legacy_policy == LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT: + # When: Paginate with AWS DB-ESDK scan paginator using ALLOW_LEGACY_DECRYPT policies + # Then: AWS DB-ESDK paginator can read the legacy-encrypted item + response = scan_paginator.paginate(**paginate_scan_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + assert len(items) == 1 + expected_item = sort_dynamodb_json_lists(test_item) + actual_item = sort_dynamodb_json_lists(items[0]) + assert expected_item == actual_item + else: + # Given: Valid paginate request with FORBID_LEGACY_DECRYPT policy + # When: Paginate with AWS DB-ESDK client + # Then: Throws a DynamoDbItemEncryptor exception (AWS DB-ESDK with FORBID policy cannot decrypt legacy items) + with pytest.raises(DynamoDbItemEncryptor): + response = scan_paginator.paginate(**paginate_scan_request) + items = [] + for page in response: + if "Items" in page: + for item in page["Items"]: + items.append(item) + + +# Delete the items in the table after the module runs +@pytest.fixture(scope="module", autouse=True) +def cleanup_after_module(test_run_suffix): + yield + table = boto3.client("dynamodb") + items = [deepcopy(simple_item_ddb), deepcopy(complex_item_ddb)] + for item in items: + item["partition_key"]["S"] += test_run_suffix + table.delete_item(**basic_delete_item_request_ddb(item)) diff --git a/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_resource.py b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_resource.py new file mode 100644 index 000000000..037ac9299 --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_resource.py @@ -0,0 +1,182 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import uuid +from copy import deepcopy + +import boto3 +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from ...constants import ( + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME, +) +from ...items import ( + complex_item_ddb, + complex_item_dict, + complex_key_dict, + simple_item_ddb, + simple_item_dict, + simple_key_dict, +) +from ...requests import ( + basic_batch_get_item_request_dict, + basic_batch_write_item_put_request_dict, + basic_delete_item_request_ddb, +) +from .utils import ( + create_legacy_encrypted_client, + create_legacy_encrypted_resource, + create_legacy_encrypted_table, + encrypted_resource_with_legacy_override, + legacy_actions, +) + + +@pytest.fixture +def tables(resource): + return resource.tables + + +@pytest.fixture(scope="module") +def test_run_suffix(): + return "-" + str(uuid.uuid4()) + + +@pytest.fixture +def test_items(test_run_suffix): + items = [deepcopy(complex_item_dict), deepcopy(simple_item_dict)] + for item in items: + item["partition_key"] += test_run_suffix + return items + + +@pytest.fixture +def test_keys(test_run_suffix): + keys = [deepcopy(complex_key_dict), deepcopy(simple_key_dict)] + for key in keys: + key["partition_key"] += test_run_suffix + return keys + + +@pytest.fixture(params=["client", "table", "resource"], ids=["legacy_client", "legacy_table", "legacy_resource"]) +def legacy_encryptor(request): + """ + Create a legacy encryptor of the specified type. + + This fixture creates legacy encryptors of three types: + - client: DynamoDB Encryption Client's EncryptedClient + - table: DynamoDB Encryption Client's EncryptedTable + - resource: DynamoDB Encryption Client's EncryptedResource + """ + if request.param == "client": + return create_legacy_encrypted_client() + elif request.param == "table": + return create_legacy_encrypted_table() + elif request.param == "resource": + return create_legacy_encrypted_resource() + + +@pytest.fixture( + params=[ + LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT, + ] +) +def legacy_policy(request): + """Fixture providing different legacy policies to test.""" + return request.param + + +@pytest.fixture +def encrypted_resource(legacy_encryptor, legacy_policy): + """Create AWS DBE SDK resource with specified legacy policy.""" + return encrypted_resource_with_legacy_override( + legacy_encryptor=legacy_encryptor, + legacy_policy=legacy_policy, + ) + + +def test_GIVEN_awsdbe_encrypted_item_WHEN_get_with_legacy_resource( + encrypted_resource, + test_items, + test_keys, + legacy_policy, +): + # Given: Valid batch_write_item request with items to put + batch_write_item_put_request = basic_batch_write_item_put_request_dict(test_items) + # When: batch_write_item using AWS DB-ESDK resource + batch_write_response = encrypted_resource.batch_write_item(**batch_write_item_put_request) + # Then: batch_write_item succeeds + assert batch_write_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Given: Fresh legacy encrypted resource and valid batch_get request + legacy_resource = create_legacy_encrypted_resource(attribute_actions=legacy_actions()) + batch_get_item_request = basic_batch_get_item_request_dict(test_keys) + + if legacy_policy == LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT: + # When: batch_get_item with legacy resource using FORCE_LEGACY_ENCRYPT policy + # Then: Items can be decrypted by legacy resource + batch_get_response = legacy_resource.batch_get_item(**batch_get_item_request) + assert batch_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + responses = batch_get_response["Responses"][INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME] + assert len(responses) == 2 + for response in responses: + assert response in test_items + else: + # When: batch_get_item with legacy resource using FORBID policies + # Then: Legacy resource cannot decrypt items created with FORBID_LEGACY_ENCRYPT policy + with pytest.raises(DecryptionError): + legacy_resource.batch_get_item(**batch_get_item_request) + + +def test_GIVEN_legacy_encrypted_item_WHEN_get_with_awsdbe_resource( + encrypted_resource, + test_items, + test_keys, + legacy_policy, +): + # Given: Fresh legacy encrypted resource and valid batch_write request + legacy_resource = create_legacy_encrypted_resource(attribute_actions=legacy_actions()) + + # When: batch_write_item using legacy resource + batch_write_item_put_request = basic_batch_write_item_put_request_dict(test_items) + batch_write_response = legacy_resource.batch_write_item(**batch_write_item_put_request) + # Then: batch_write_item succeeds + assert batch_write_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Create batch_get request + batch_get_item_request = basic_batch_get_item_request_dict(test_keys) + + if not legacy_policy == LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT: + # Given: Valid batch_get_item request for the same items with ALLOW_LEGACY_DECRYPT policy + # When: batch_get_item using AWS DB-ESDK resource + batch_get_response = encrypted_resource.batch_get_item(**batch_get_item_request) + # Then: Legacy resource can decrypt items created with FORCE_LEGACY_ENCRYPT policy + assert batch_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + responses = batch_get_response["Responses"][INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME] + assert len(responses) == 2 + for response in responses: + assert response in test_items + else: + # Given: Valid get_item request for the same item with FORBID_LEGACY_DECRYPT policy + # When: get_item using AWS DB-ESDK client + # Then: Throws DynamoDbItemEncryptor exception (AWS DB-ESDK with FORBID policy cannot decrypt legacy items) + with pytest.raises(DynamoDbItemEncryptor): + encrypted_resource.batch_get_item(**batch_get_item_request) + + +# Delete the items in the table after the module runs +@pytest.fixture(scope="module", autouse=True) +def cleanup_after_module(test_run_suffix): + yield + table = boto3.client("dynamodb") + items = [deepcopy(simple_item_ddb), deepcopy(complex_item_ddb)] + for item in items: + item["partition_key"]["S"] += test_run_suffix + table.delete_item(**basic_delete_item_request_ddb(item)) diff --git a/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_table.py b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_table.py new file mode 100644 index 000000000..0cc4fedef --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/test/integ/legacy/test_table.py @@ -0,0 +1,168 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import uuid +from copy import deepcopy + +import boto3 +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from ...constants import INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME +from ...items import ( + complex_item_dict, + simple_item_dict, +) +from ...requests import basic_delete_item_request_dict, basic_get_item_request_dict, basic_put_item_request_dict +from .utils import ( + create_legacy_encrypted_client, + create_legacy_encrypted_resource, + create_legacy_encrypted_table, + encrypted_table_with_legacy_override, + legacy_actions, +) + + +@pytest.fixture(scope="module") +def test_run_suffix(): + return "-" + str(uuid.uuid4()) + + +# Creates a matrix of tests for each value in param, +# with a user-friendly string for test output: +# use_complex_item = True -> "complex_item" +# use_complex_item = False -> "simple_item" +@pytest.fixture(params=[simple_item_dict, complex_item_dict], ids=["simple_item", "complex_item"]) +def test_item(request, test_run_suffix): + item = deepcopy(request.param) + item["partition_key"] += test_run_suffix + return item + + +# Fixtures for legacy encryptors and tables + + +@pytest.fixture(params=["client", "table", "resource"], ids=["legacy_client", "legacy_table", "legacy_resource"]) +def legacy_encryptor(request): + """ + Create a legacy encryptor of the specified type. + + This fixture creates legacy encryptors of three types: + - client: DynamoDB Encryption Client's EncryptedClient + - table: DynamoDB Encryption Client's EncryptedTable + - resource: DynamoDB Encryption Client's EncryptedResource + """ + if request.param == "client": + return create_legacy_encrypted_client() + elif request.param == "table": + return create_legacy_encrypted_table() + elif request.param == "resource": + return create_legacy_encrypted_resource() + + +# Fixtures for each legacy policy + + +@pytest.fixture( + params=[ + LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT, + LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT, + ] +) +def legacy_policy(request): + return request.param + + +@pytest.fixture +def encrypted_table(legacy_encryptor, legacy_policy): + """Create AWS DBE SDK table with specified legacy policy.""" + return encrypted_table_with_legacy_override( + legacy_encryptor=legacy_encryptor, + legacy_policy=legacy_policy, + ) + + +def test_GIVEN_awsdbe_encrypted_item_WHEN_get_with_legacy_table( + encrypted_table, + test_item, + legacy_policy, +): + # Given: Valid put_item request + put_item_request_dict = basic_put_item_request_dict(test_item) + # When: put_item + put_response = encrypted_table.put_item(**put_item_request_dict) + # Then: put_item succeeds + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Given: Fresh legacy encryptor of the same type as used in the fixture + legacy_encrypted_table = create_legacy_encrypted_table( + attribute_actions=legacy_actions(), + ) + + # Get item request + get_item_request_dict = basic_get_item_request_dict(test_item) + + if legacy_policy == LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT: + # Given: Valid get_item request for the same item using legacy encryptor with FORCE_LEGACY_ENCRYPT policy + # When: get_item with legacy encryptor + get_response = legacy_encrypted_table.get_item(**get_item_request_dict) + # Then: Response is equal to the original item (legacy encryptor can decrypt item written by AWS DB-ESDK) + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + assert get_response["Item"] == put_item_request_dict["Item"] + else: + # Given: Valid get_item request for the same item using legacy encryptor with FORBID_LEGACY_ENCRYPT policy + # When: get_item with legacy encryptor + # Then: throws DecryptionError Exception (i.e. legacy encryptor cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + # Try to read the item with the legacy encryptor + legacy_encrypted_table.get_item(**get_item_request_dict) + + +def test_GIVEN_legacy_encrypted_item_WHEN_get_with_awsdbe( + encrypted_table, + test_item, + legacy_policy, +): + # Given: Fresh legacy encryptor and valid put_item request + legacy_encrypted_table = create_legacy_encrypted_table( + attribute_actions=legacy_actions(), + ) + # Given: Valid put_item request + put_item_request_dict = basic_put_item_request_dict(test_item) + # When: put_item using legacy encryptor + put_response = legacy_encrypted_table.put_item(**put_item_request_dict) + # Then: put_item succeeds (item is written using legacy format) + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get item request + get_item_request_dict = basic_get_item_request_dict(test_item) + + if not legacy_policy == LegacyPolicy.FORBID_LEGACY_ENCRYPT_FORBID_LEGACY_DECRYPT: + # Given: Valid get_item request for the same item with ALLOW_LEGACY_DECRYPT policy + # When: get_item using AWS DB-ESDK client + get_response = encrypted_table.get_item(**get_item_request_dict) + # Then: Table can read the legacy-encrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + assert get_response["Item"] == put_item_request_dict["Item"] + else: + # Given: Valid get_item request for the same item with FORBID_LEGACY_DECRYPT policy + # When: get_item using AWS DB-ESDK client + # Then: Throws a DynamoDbItemEncryptor exception (AWS DB-ESDK with FORBID policy cannot decrypt legacy items) + with pytest.raises(DynamoDbItemEncryptor): + encrypted_table.get_item(**get_item_request_dict) + + +# Delete the items in the table after the module runs +@pytest.fixture(scope="module", autouse=True) +def cleanup_after_module(test_run_suffix): + yield + table = boto3.resource("dynamodb").Table(INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME) + items = [deepcopy(simple_item_dict), deepcopy(complex_item_dict)] + for item in items: + item["partition_key"] = item["partition_key"] + test_run_suffix + table.delete_item(**basic_delete_item_request_dict(item)) diff --git a/DynamoDbEncryption/runtimes/python/test/integ/legacy/utils.py b/DynamoDbEncryption/runtimes/python/test/integ/legacy/utils.py new file mode 100644 index 000000000..8e12cc935 --- /dev/null +++ b/DynamoDbEncryption/runtimes/python/test/integ/legacy/utils.py @@ -0,0 +1,158 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import uuid + +import boto3 +from dynamodb_encryption_sdk.encrypted.client import EncryptedClient as LegacyEncryptedClient +from dynamodb_encryption_sdk.encrypted.resource import EncryptedResource as LegacyEncryptedResource +from dynamodb_encryption_sdk.encrypted.table import EncryptedTable as LegacyEncryptedTable +from dynamodb_encryption_sdk.identifiers import CryptoAction +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider +from dynamodb_encryption_sdk.structures import AttributeActions + +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.encrypted.resource import EncryptedResource +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + LegacyOverride, +) + +from ...constants import ( + INTEG_TEST_DEFAULT_ALGORITHM_SUITE_ID, + INTEG_TEST_DEFAULT_ATTRIBUTE_ACTIONS_ON_ENCRYPT, + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME, + INTEG_TEST_DEFAULT_KEYRING, + INTEG_TEST_DEFAULT_KMS_KEY_ID, + INTEG_TEST_DEFAULT_UNSIGNED_ATTRIBUTE_PREFIX, +) + + +def generate_unique_suffix(): + """Generate a unique suffix for test items.""" + return "-" + str(uuid.uuid4()) + + +# Legacy Attribute Actions +def legacy_actions(): + return AttributeActions( + default_action=CryptoAction.ENCRYPT_AND_SIGN, + attribute_actions={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + ) + + +# Legacy interface creation functions + + +def create_legacy_encrypted_client(attribute_actions=None, expect_standard_dictionaries=False): + """Create a legacy DynamoDB encrypted client.""" + cmp = AwsKmsCryptographicMaterialsProvider(key_id=INTEG_TEST_DEFAULT_KMS_KEY_ID) + return LegacyEncryptedClient( + client=plaintext_client(expect_standard_dictionaries), + materials_provider=cmp, + attribute_actions=attribute_actions, + expect_standard_dictionaries=expect_standard_dictionaries, + ) + + +def create_legacy_encrypted_table(attribute_actions=None): + """Create a legacy DynamoDB encrypted table.""" + plaintext_table = boto3.resource("dynamodb").Table(INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME) + cmp = AwsKmsCryptographicMaterialsProvider(key_id=INTEG_TEST_DEFAULT_KMS_KEY_ID) + return LegacyEncryptedTable( + table=plaintext_table, + materials_provider=cmp, + attribute_actions=attribute_actions, + ) + + +def create_legacy_encrypted_resource(attribute_actions=None): + """Create a legacy DynamoDB encrypted resource.""" + plaintext_resource = boto3.resource("dynamodb") + cmp = AwsKmsCryptographicMaterialsProvider(key_id=INTEG_TEST_DEFAULT_KMS_KEY_ID) + return LegacyEncryptedResource( + resource=plaintext_resource, + materials_provider=cmp, + attribute_actions=attribute_actions, + ) + + +# AWS DBE SDK interface creation functions with legacy override + + +def create_encryption_config(legacy_encryptor, legacy_policy): + """Create a DynamoDbTableEncryptionConfig with optional legacy override.""" + # Configure legacy behavior + legacy_override = LegacyOverride( + encryptor=legacy_encryptor, + attribute_actions_on_encrypt=INTEG_TEST_DEFAULT_ATTRIBUTE_ACTIONS_ON_ENCRYPT, + policy=legacy_policy, + ) + + # Create the table config with legacy override + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=INTEG_TEST_DEFAULT_ATTRIBUTE_ACTIONS_ON_ENCRYPT, + keyring=INTEG_TEST_DEFAULT_KEYRING, + legacy_override=legacy_override, + allowed_unsigned_attribute_prefix=INTEG_TEST_DEFAULT_UNSIGNED_ATTRIBUTE_PREFIX, + algorithm_suite_id=INTEG_TEST_DEFAULT_ALGORITHM_SUITE_ID, + ) + + # Create the tables config + table_configs = {INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME: table_config} + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + +def plaintext_client(expect_standard_dictionaries): + if expect_standard_dictionaries: + client = boto3.resource("dynamodb").Table(INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME).meta.client + else: + client = boto3.client("dynamodb") + return client + + +def encrypted_client_with_legacy_override( + legacy_encryptor=None, legacy_policy=None, expect_standard_dictionaries=False +): + """Create an AWS Database Encryption SDK client with optional legacy override.""" + tables_config = create_encryption_config(legacy_encryptor=legacy_encryptor, legacy_policy=legacy_policy) + + # Create the EncryptedClient + return EncryptedClient( + client=plaintext_client(expect_standard_dictionaries), + encryption_config=tables_config, + expect_standard_dictionaries=expect_standard_dictionaries, + ) + + +def encrypted_table_with_legacy_override(legacy_encryptor=None, legacy_policy=None): + """Create an AWS Database Encryption SDK table from a client.""" + tables_config = create_encryption_config(legacy_encryptor=legacy_encryptor, legacy_policy=legacy_policy) + + # Create the EncryptedTable + table = boto3.resource("dynamodb").Table(INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME) + return EncryptedTable( + table=table, + encryption_config=tables_config, + ) + + +def encrypted_resource_with_legacy_override(legacy_encryptor=None, legacy_policy=None): + """Create an AWS Database Encryption SDK resource with optional legacy override.""" + tables_config = create_encryption_config(legacy_encryptor=legacy_encryptor, legacy_policy=legacy_policy) + + # Create the EncryptedResource + return EncryptedResource( + resource=boto3.resource("dynamodb"), + encryption_config=tables_config, + ) diff --git a/DynamoDbEncryption/runtimes/python/tox.ini b/DynamoDbEncryption/runtimes/python/tox.ini index a4edd1639..dbd30d8e4 100644 --- a/DynamoDbEncryption/runtimes/python/tox.ini +++ b/DynamoDbEncryption/runtimes/python/tox.ini @@ -1,7 +1,7 @@ [tox] isolated_build = True envlist = - py{311,312,313}-{dafnytests,unit,integ}, + py{311,312,313}-{dafnytests,unit,integ,legacyinteg}, encrypted-interface-coverage, client-to-resource-conversions-coverage, resource-to-client-conversions-coverage, @@ -22,7 +22,14 @@ commands_pre = commands = dafnytests: {[testenv:base-command]commands} test/internaldafny/ unit: {[testenv:base-command]commands} test/unit/ - integ: {[testenv:base-command]commands} test/integ/ + integ: {[testenv:base-command]commands} test/integ/encrypted/ + +[testenv:legacyinteg] +description = Run integ tests for legacy extern compatibility +commands_pre = + poetry lock + poetry install --with test --extras legacy-ddbec +commands = {[testenv:base-command]commands} test/integ/legacy/ [testenv:encrypted-interface-coverage] description = Run integ + unit tests for encrypted interfaces with coverage @@ -62,6 +69,7 @@ commands = ruff check \ src/aws_dbesdk_dynamodb/ \ ../../../Examples/runtimes/python/DynamoDBEncryption/ \ + ../../../Examples/runtimes/python/Migration/ \ test/ \ {posargs} @@ -75,6 +83,7 @@ commands = black --line-length 120 \ src/aws_dbesdk_dynamodb/ \ ../../../Examples/runtimes/python/DynamoDBEncryption/ \ + ../../../Examples/runtimes/python/Migration/ \ test/ \ {posargs} diff --git a/Examples/runtimes/python/Migration/.gitignore b/Examples/runtimes/python/Migration/.gitignore new file mode 100644 index 000000000..61d5202d8 --- /dev/null +++ b/Examples/runtimes/python/Migration/.gitignore @@ -0,0 +1,17 @@ +# Python build artifacts +__pycache__ +**/__pycache__ +*.pyc +src/**.egg-info/ +build +poetry.lock +**/poetry.lock +dist + +# Dafny-generated Python +**/internaldafny/generated/*.py + +# Python test artifacts +.tox +.pytest_cache + diff --git a/Examples/runtimes/python/Migration/__init__.py b/Examples/runtimes/python/Migration/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/__init__.py b/Examples/runtimes/python/Migration/src/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/README.md b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/README.md new file mode 100644 index 000000000..505ab11b8 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/README.md @@ -0,0 +1,45 @@ +# DyanmoDb Encryption Client to AWS Database Encryption SDK for DynamoDb Migration + +This projects demonstrates the three Steps necessary to migration to the AWS Database Encryption SDK for DynamoDb +if you are currently using the DynamoDb Encryption Client. + +[Step 0](./ddbec/README.md) demonstrates the starting state for your system. + +## Step 1 + +In Step 1, you update your system to do the following: + +- continue to read items in the old format +- continue to write items in the old format +- prepare to read items in the new format + +When you deploy changes in Step 1, you should not expect any behavior change in your system, +and your dataset still consists of data written in the old format. + +You must ensure that the changes in Step 1 make it to all your reads before you proceed to step 2. + +## Step 2 + +In Step 2, you update your system to do the following: + +- continue to read items in the old format +- start writing items in the new format +- continue to read items in the new format + +When you deploy changes in Step 2, you are introducing a new encryption format to your system, +and must make sure that all your readers are updated with the changes from Step 1. + +Before you move onto the next step, you will need to re-encrypt all old items in your dataset +to use the newest format. How you will want to do this, and how long you may want to remain in this Step, +depends on your system and your desired security properties for old and new items. + +## Step 3 + +Once all old items are re-encrypted to use the new format, +you may update your system to do the following: + +- continue to write items in the new format +- continue to read items in the new format +- do not accept reading items in the old format + +Once you have deployed these changes to your system, you have completed migration. diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/common.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/common.py new file mode 100644 index 000000000..2e74ece9c --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/common.py @@ -0,0 +1,235 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Common Utilities for Migration Examples.""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkMultiKeyringInput, + DBEAlgorithmSuiteId, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + LegacyOverride, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + +# Import from legacy DynamoDB Encryption Client +from dynamodb_encryption_sdk.encrypted.client import EncryptedClient as LegacyEncryptedClient +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider + + +def setup_pure_awsdbe_client(kms_key_id: str, ddb_table_name: str): + """ + Set up a pure AWS Database Encryption SDK EncryptedClient without legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :returns EncryptedClient for DynamoDB + """ + # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `CreateMrkMultiKeyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput( + generator=kms_key_id, + ) + kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsignAttrPrefix: str = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + # without the legacy override + table_configs = {} + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=kms_mrk_multi_keyring, + allowed_unsigned_attribute_prefix=unsignAttrPrefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + table_configs[ddb_table_name] = table_config + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedClient + return EncryptedClient( + client=boto3.client("dynamodb"), + encryption_config=tables_config, + ) + + +def setup_awsdbe_client_with_legacy_override(kms_key_id: str, ddb_table_name: str, policy: str): + """ + Set up an AWS Database Encryption SDK EncryptedClient with legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param policy: The policy required for the Legacy Override configuration + :returns EncryptedClient for DynamoDB + + """ + # 0. Create AWS SDK DynamoDB Client + ddb_client = boto3.client("dynamodb") + + # 1. Create the legacy EncryptedClient + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + legacy_encrypted_client = LegacyEncryptedClient( + client=ddb_client, + materials_provider=cmp, + ) + + # 2. Configure our legacy behavior, inputting the DynamoDBEncryptor, attribute actions + # created above, and legacy policy. + legacy_override = LegacyOverride( + encryptor=legacy_encrypted_client, + attribute_actions_on_encrypt={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + policy=policy, + ) + + # 3. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `CreateMrkMultiKeyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput( + generator=kms_key_id, + ) + kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input) + + # 4. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 5. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsignAttrPrefix: str = ":" + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # without the legacy override + table_configs = {} + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=kms_mrk_multi_keyring, + legacy_override=legacy_override, + allowed_unsigned_attribute_prefix=unsignAttrPrefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + table_configs[ddb_table_name] = table_config + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedClient + return EncryptedClient( + client=boto3.client("dynamodb"), + encryption_config=tables_config, + ) diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_1.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_1.py new file mode 100644 index 000000000..892cb2ca5 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_1.py @@ -0,0 +1,87 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 1. + +This is an example demonstrating how to start using the +AWS Database Encryption SDK with a pre-existing table used with DynamoDB Encryption Client. +In this example, you configure a EncryptedClient to do the following: + - Read items encrypted in the old format + - Continue to encrypt items in the old format on write + - Read items encrypted in the new format +While this step configures your client to be ready to start reading items encrypted, +we do not yet expect to be reading any items in the new format. +Before you move on to step 2, ensure that these changes have successfully been deployed +to all of your readers. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from .common import setup_awsdbe_client_with_legacy_override + + +def migration_step_1_with_client(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 1): + """ + Migration Step 1: Using the AWS Database Encryption SDK with Legacy Override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create a EncryptedClient with legacy override. + # For Legacy Policy, use `FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read and write items using the old format, + # but will be able to start reading new items in the new format as soon as they appear + policy = LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + encrypted_client = setup_awsdbe_client_with_legacy_override( + kms_key_id=kms_key_id, ddb_table_name=ddb_table_name, policy=policy + ) + + # 2. Put an item in the old format since we are using a legacy override + # with FORCE_LEGACY_ENCRYPT_ALLOW_DECRYPT policy + item_to_encrypt = { + "partition_key": {"S": "MigrationExampleForPython"}, + "sort_key": {"N": str(1)}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_request = { + "TableName": ddb_table_name, + "Item": item_to_encrypt, + } + + put_item_response = encrypted_client.put_item(**put_item_request) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get an item back from the table using the DynamoDb Enhanced Client. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + key_to_get = {"partition_key": {"S": "MigrationExampleForPython"}, "sort_key": {"N": str(sort_read_value)}} + + get_item_request = {"TableName": ddb_table_name, "Key": key_to_get} + get_item_response = encrypted_client.get_item(**get_item_request) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"]["S"] == "MigrationExampleForPython" + assert decrypted_item["sort_key"]["N"] == str(sort_read_value) + assert decrypted_item["attribute1"]["S"] == "encrypt and sign me!" + assert decrypted_item["attribute2"]["S"] == "sign me!" + assert decrypted_item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_2.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_2.py new file mode 100644 index 000000000..776825585 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_2.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 2. + +This is an example demonstrating how to update your configuration +to start writing items using the latest encryption format, but still continue +to read any items written using the old encryption format. + +Once you deploy this change to your system, you will have a dataset +containing items in both the old and new format. +Because the changes in Step 1 have been deployed to all our readers, +we can be sure that our entire system is ready to read this new data. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +# Import from new AWS Database Encryption SDK +from .common import setup_awsdbe_client_with_legacy_override + + +def migration_step_2_with_client(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 2): + """ + Migration Step 2: Using pure AWS DBESDK and legacy override together with EncryptedClient. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create a EncryptedClient with legacy override. + # When configuring our legacy behavior, use `FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read items in both formats, + # but will only write new items using the new format. + encrypted_client = setup_awsdbe_client_with_legacy_override( + kms_key_id, ddb_table_name, policy=LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + ) + + # 2. Put an item into your table using the DB ESDK Client. + # This item will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + item_to_encrypt = { + "partition_key": {"S": "MigrationExampleForPython"}, + "sort_key": {"N": str(2)}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_request = { + "TableName": ddb_table_name, + "Item": item_to_encrypt, + } + + put_item_response = encrypted_client.put_item(**put_item_request) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get an item back from the table using the Client. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + key_to_get = {"partition_key": {"S": "MigrationExampleForPython"}, "sort_key": {"N": str(sort_read_value)}} + + get_item_request = {"TableName": ddb_table_name, "Key": key_to_get} + get_item_response = encrypted_client.get_item(**get_item_request) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"]["S"] == "MigrationExampleForPython" + assert decrypted_item["sort_key"]["N"] == str(sort_read_value) + assert decrypted_item["attribute1"]["S"] == "encrypt and sign me!" + assert decrypted_item["attribute2"]["S"] == "sign me!" + assert decrypted_item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_3.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_3.py new file mode 100644 index 000000000..df4ae758c --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/client/migration_step_3.py @@ -0,0 +1,73 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 3. + +This is an example demonstrating how to update your configuration +to stop accepting reading items encrypted using the old format. +In order to proceed with this step, you will need to re-encrypt all +old items in your table. + +Once you complete Step 3, you can be sure that all items being read by your system +ensure the security properties configured for the new format. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +from .common import setup_pure_awsdbe_client + + +def migration_step_3_with_client(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 3): + """ + Migration Step 3: Using only pure AWS DBESDK (no legacy override) with EncryptedClient. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + """ + # 1. Create the EncryptedClient. + # Do not configure any legacy behavior. + encrypted_client = setup_pure_awsdbe_client(kms_key_id, ddb_table_name) + + # 2. Put an item into your table using the Client. + # This item will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + item = { + "partition_key": {"S": "MigrationExampleForPython"}, + "sort_key": {"N": str(3)}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_response = encrypted_client.put_item(TableName=ddb_table_name, Item=item) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get an item back from the table using the Client. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we fail to return the item. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + key_to_get = {"partition_key": {"S": "MigrationExampleForPython"}, "sort_key": {"N": str(sort_read_value)}} + + get_item_request = {"TableName": ddb_table_name, "Key": key_to_get} + get_item_response = encrypted_client.get_item(**get_item_request) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"]["S"] == "MigrationExampleForPython" + assert decrypted_item["sort_key"]["N"] == str(sort_read_value) + assert decrypted_item["attribute1"]["S"] == "encrypt and sign me!" + assert decrypted_item["attribute2"]["S"] == "sign me!" + assert decrypted_item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_1.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_1.py new file mode 100644 index 000000000..ec89382b7 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_1.py @@ -0,0 +1,94 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 1. + +This is an example demonstrating how to start using the +AWS Database Encryption SDK with a pre-existing table used with DynamoDB Encryption Client. +In this example, you configure a EncryptedPaginator to do the following: + - Read items encrypted in the old format + - Continue to encrypt items in the old format on write + - Read items encrypted in the new format +While this step configures your paginator to be ready to start reading items encrypted, +we do not yet expect to be reading any items in the new format. +Before you move on to step 2, ensure that these changes have successfully been deployed +to all of your readers. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from ..client.common import setup_awsdbe_client_with_legacy_override + + +def migration_step_1_with_paginator(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 1): + """ + Migration Step 1: Using the AWS Database Encryption SDK EncryptedPaginator with Legacy Override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create a EncryptedClient with legacy override. + # For Legacy Policy, use `FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read and write items using the old format, + # but will be able to start reading new items in the new format as soon as they appear + policy = LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + encrypted_client = setup_awsdbe_client_with_legacy_override( + kms_key_id=kms_key_id, ddb_table_name=ddb_table_name, policy=policy + ) + + # 2. Put an item in the old format since we are using a legacy override + # with FORCE_LEGACY_ENCRYPT_ALLOW_DECRYPT policy + item_to_encrypt = { + "partition_key": {"S": "PaginatorMigrationExampleForPython"}, + "sort_key": {"N": "1"}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_request = { + "TableName": ddb_table_name, + "Item": item_to_encrypt, + } + + put_item_response = encrypted_client.put_item(**put_item_request) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get the EncryptedPaginator from the EncryptedClient + encrypted_paginator = encrypted_client.get_paginator("query") + + # 4. Use the EncryptedPaginator to paginate through the items in the table + # If the items were written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If the items were written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + items = [] + for page in encrypted_paginator.paginate( + TableName=ddb_table_name, + KeyConditionExpression="partition_key = :partition_key AND sort_key = :sort_key", + ExpressionAttributeValues={ + ":partition_key": {"S": "PaginatorMigrationExampleForPython"}, + ":sort_key": {"N": str(sort_read_value)}, + }, + ): + for item in page["Items"]: + items.append(item) + + # 5. Verify the decrypted items + assert len(items) == 1 # We should have only one item with above key condition + item = next((i for i in items if i["sort_key"]["N"] == str(sort_read_value)), None) + assert item is not None + assert item["partition_key"]["S"] == "PaginatorMigrationExampleForPython" + assert item["attribute1"]["S"] == "encrypt and sign me!" + assert item["attribute2"]["S"] == "sign me!" + assert item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_2.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_2.py new file mode 100644 index 000000000..6ad018cc5 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_2.py @@ -0,0 +1,94 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 2. + +This is an example demonstrating how to update your configuration +to start writing items using the latest encryption format, but still continue +to read any items written using the old encryption format. + +Once you deploy this change to your system, you will have a dataset +containing items in both the old and new format. +Because the changes in Step 1 have been deployed to all our readers, +we can be sure that our entire system is ready to read this new data. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +# Import from new AWS Database Encryption SDK +from ..client.common import setup_awsdbe_client_with_legacy_override + + +def migration_step_2_with_paginator(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 2): + """ + Migration Step 2: Using the AWS Database Encryption SDK EncryptedPaginator with legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create a EncryptedClient with legacy override. + # When configuring our legacy behavior, use `FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read items in both formats, + # but will only write new items using the new format. + encrypted_client = setup_awsdbe_client_with_legacy_override( + kms_key_id, ddb_table_name, policy=LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + ) + + # 2. Put an item into your table using the EncryptedClient. + # This item will be encrypted in the latest format, using the + # configuration to decide which attribute to encrypt and/or sign. + item_to_encrypt = { + "partition_key": {"S": "PaginatorMigrationExampleForPython"}, + "sort_key": {"N": "2"}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_request = { + "TableName": ddb_table_name, + "Item": item_to_encrypt, + } + + put_item_response = encrypted_client.put_item(**put_item_request) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get the EncryptedPaginator from the EncryptedClient + encrypted_paginator = encrypted_client.get_paginator("query") + + # 4. Use the EncryptedPaginator to paginate through the items in the table + # If the items were written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If the items were written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + items = [] + for page in encrypted_paginator.paginate( + TableName=ddb_table_name, + KeyConditionExpression="partition_key = :partition_key AND sort_key = :sort_key", + ExpressionAttributeValues={ + ":partition_key": {"S": "PaginatorMigrationExampleForPython"}, + ":sort_key": {"N": str(sort_read_value)}, + }, + ): + for item in page["Items"]: + items.append(item) + + # 5. Verify the decrypted items + assert len(items) == 1 # We should have only one item with above key condition + item = next((i for i in items if i["sort_key"]["N"] == str(sort_read_value)), None) + assert item is not None + assert item["partition_key"]["S"] == "PaginatorMigrationExampleForPython" + assert item["attribute1"]["S"] == "encrypt and sign me!" + assert item["attribute2"]["S"] == "sign me!" + assert item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_3.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_3.py new file mode 100644 index 000000000..25192ba91 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/paginator/migration_step_3.py @@ -0,0 +1,85 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 3. + +This is an example demonstrating how to update your configuration +to stop accepting reading items encrypted using the old format. +In order to proceed with this step, you will need to re-encrypt all +old items in your table. + +Once you complete Step 3, you can be sure that all items being read by your system +ensure the security properties configured for the new format. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +from ..client.common import setup_pure_awsdbe_client + + +def migration_step_3_with_paginator(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 3): + """ + Migration Step 3: Using only pure AWS DBESDK (no legacy override) with EncryptedPaginator. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create the EncryptedClient. + # Do not configure any legacy behavior. + encrypted_client = setup_pure_awsdbe_client(kms_key_id, ddb_table_name) + + # 2. Put an item into your table using the Client. + # This item will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + item = { + "partition_key": {"S": "PaginatorMigrationExampleForPython"}, + "sort_key": {"N": "3"}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_response = encrypted_client.put_item(TableName=ddb_table_name, Item=item) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get the EncryptedPaginator from the EncryptedClient + encrypted_paginator = encrypted_client.get_paginator("query") + + # 4. Use the EncryptedPaginator to paginate through the items in the table + # If the items were written in the old format (e.g. any item written + # during Step 0 or 1), then we will fail to decrypt those items. + # If the items were written in the new format (e.g. any item written + # during Step 2 or 3), then we will attempt to decrypt the item using + # the non-legacy behavior. + items = [] + for page in encrypted_paginator.paginate( + TableName=ddb_table_name, + KeyConditionExpression="partition_key = :partition_key AND sort_key = :sort_key", + ExpressionAttributeValues={ + ":partition_key": {"S": "PaginatorMigrationExampleForPython"}, + ":sort_key": {"N": str(sort_read_value)}, + }, + ): + for item in page["Items"]: + items.append(item) + + # 5. Verify the decrypted items + assert len(items) == 1 # We should have only one item with above key condition + item = next((i for i in items if i["sort_key"]["N"] == str(sort_read_value)), None) + assert item is not None + assert item["partition_key"]["S"] == "PaginatorMigrationExampleForPython" + assert item["attribute1"]["S"] == "encrypt and sign me!" + assert item["attribute2"]["S"] == "sign me!" + assert item[":attribute3"]["S"] == "ignore me!" + + # Note: If we tried to query for items with sort_key = 1 or sort_key = 2 that were + # written with the legacy format in previous migration steps and haven't been + # re-encrypted, the operation would fail with a verification exception. diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/common.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/common.py new file mode 100644 index 000000000..f2dc49eac --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/common.py @@ -0,0 +1,235 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Common Utilities for Migration Examples.""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkMultiKeyringInput, + DBEAlgorithmSuiteId, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.resource import EncryptedResource +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + LegacyOverride, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + +# Import from legacy DynamoDB Encryption Client +from dynamodb_encryption_sdk.encrypted.resource import EncryptedResource as LegacyEncryptedResource +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider + + +def setup_pure_awsdbe_resource(kms_key_id: str, ddb_table_name: str): + """ + Set up a pure AWS Database Encryption SDK EncryptedResource without legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :returns EncryptedResource for DynamoDB + """ + # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `CreateMrkMultiKeyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput( + generator=kms_key_id, + ) + kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsignAttrPrefix: str = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + # without the legacy override + table_configs = {} + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=kms_mrk_multi_keyring, + allowed_unsigned_attribute_prefix=unsignAttrPrefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + table_configs[ddb_table_name] = table_config + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedResource + return EncryptedResource( + resource=boto3.resource("dynamodb"), + encryption_config=tables_config, + ) + + +def setup_awsdbe_resource_with_legacy_override(kms_key_id: str, ddb_table_name: str, policy: str): + """ + Set up an AWS Database Encryption SDK EncryptedResource with legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param policy: The policy required for the Legacy Override configuration + :returns EncryptedResource for DynamoDB + + """ + # 0. Create AWS SDK DynamoDB Resource + ddb_resource = boto3.resource("dynamodb") + + # 1. Create the legacy EncryptedResource + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + legacy_encrypted_resource = LegacyEncryptedResource( + resource=ddb_resource, + materials_provider=cmp, + ) + + # 2. Configure our legacy behavior, inputting the DynamoDBEncryptor, attribute actions + # created above, and legacy policy. + legacy_override = LegacyOverride( + encryptor=legacy_encrypted_resource, + attribute_actions_on_encrypt={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + policy=policy, + ) + + # 3. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `CreateMrkMultiKeyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput( + generator=kms_key_id, + ) + kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input) + + # 4. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 5. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsignAttrPrefix: str = ":" + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # with the legacy override + table_configs = {} + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=kms_mrk_multi_keyring, + legacy_override=legacy_override, + allowed_unsigned_attribute_prefix=unsignAttrPrefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + table_configs[ddb_table_name] = table_config + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedResource + return EncryptedResource( + resource=boto3.resource("dynamodb"), + encryption_config=tables_config, + ) diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_1.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_1.py new file mode 100644 index 000000000..e399d28de --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_1.py @@ -0,0 +1,105 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 1. + +This is an example demonstrating how to start using the +AWS Database Encryption SDK with a pre-existing table used with DynamoDB Encryption Client. +In this example, you configure a EncryptedResource to do the following: + - Read items encrypted in the old format + - Continue to encrypt items in the old format on write + - Read items encrypted in the new format +While this step configures your resource to be ready to start reading items encrypted, +we do not yet expect to be reading any items in the new format. +Before you move on to step 2, ensure that these changes have successfully been deployed +to all of your readers. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from .common import setup_awsdbe_resource_with_legacy_override + + +def migration_step_1_with_resource(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 1): + """ + Migration Step 1: Using the AWS Database Encryption SDK with Legacy Override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create a EncryptedResource with legacy override. + # For Legacy Policy, use `FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read and write items using the old format, + # but will be able to start reading new items in the new format as soon as they appear + policy = LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + encrypted_resource = setup_awsdbe_resource_with_legacy_override( + kms_key_id=kms_key_id, ddb_table_name=ddb_table_name, policy=policy + ) + + # 2. Write a batch of items to the table using the old format since we are using + # a legacy override with FORCE_LEGACY_ENCRYPT_ALLOW_DECRYPT policy + items = [ + { + "partition_key": "PythonEncryptedResourceMigrationExample-1", + "sort_key": 1, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + { + "partition_key": "PythonEncryptedResourceMigrationExample-2", + "sort_key": 1, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + ] + + batch_write_items_put_request = { + "RequestItems": { + ddb_table_name: [{"PutRequest": {"Item": item}} for item in items], + }, + } + + batch_write_items_put_response = encrypted_resource.batch_write_item(**batch_write_items_put_request) + + # Demonstrate that BatchWriteItem succeeded + assert batch_write_items_put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Read the items back from the table. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + batch_get_items_request = { + "RequestItems": { + ddb_table_name: { + "Keys": [{"partition_key": item["partition_key"], "sort_key": sort_read_value} for item in items], + } + }, + } + + batch_get_items_response = encrypted_resource.batch_get_item(**batch_get_items_request) + + # Demonstrate that BatchGetItem succeeded with the expected result + assert batch_get_items_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + for item in batch_get_items_response["Responses"][ddb_table_name]: + assert ( + item["partition_key"] == "PythonEncryptedResourceMigrationExample-1" + or item["partition_key"] == "PythonEncryptedResourceMigrationExample-2" + ) + assert item["sort_key"] == sort_read_value + assert item["attribute1"] == "encrypt and sign me!" + assert item["attribute2"] == "sign me!" + assert item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_2.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_2.py new file mode 100644 index 000000000..cf7ba3a33 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_2.py @@ -0,0 +1,106 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 2. + +This is an example demonstrating how to update your configuration +to start writing items using the latest encryption format, but still continue +to read any items written using the old encryption format. + +Once you deploy this change to your system, you will have a dataset +containing items in both the old and new format. +Because the changes in Step 1 have been deployed to all our readers, +we can be sure that our entire system is ready to read this new data. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +# Import from new AWS Database Encryption SDK +from .common import setup_awsdbe_resource_with_legacy_override + + +def migration_step_2_with_resource(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 2): + """ + Migration Step 2: Using pure AWS DBESDK and legacy override together with EncryptedResource. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create a EncryptedResource with legacy override. + # When configuring our legacy behavior, use `FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read items in both formats, + # but will only write new items using the new format. + encrypted_resource = setup_awsdbe_resource_with_legacy_override( + kms_key_id, ddb_table_name, policy=LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + ) + + # 2. Write a batch of items to the table. + # These items will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + items = [ + { + "partition_key": "PythonEncryptedResourceMigrationExample-1", + "sort_key": 2, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + { + "partition_key": "PythonEncryptedResourceMigrationExample-2", + "sort_key": 2, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + ] + + batch_write_items_put_request = { + "RequestItems": { + ddb_table_name: [{"PutRequest": {"Item": item}} for item in items], + }, + } + + batch_write_items_put_response = encrypted_resource.batch_write_item(**batch_write_items_put_request) + + # Demonstrate that BatchWriteItem succeeded + assert batch_write_items_put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Read the items back from the table. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + batch_get_items_request = { + "RequestItems": { + ddb_table_name: { + "Keys": [{"partition_key": item["partition_key"], "sort_key": sort_read_value} for item in items], + } + }, + } + + batch_get_items_response = encrypted_resource.batch_get_item(**batch_get_items_request) + + # Demonstrate that BatchGetItem succeeded with the expected result + assert batch_get_items_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + for item in batch_get_items_response["Responses"][ddb_table_name]: + assert ( + item["partition_key"] == "PythonEncryptedResourceMigrationExample-1" + or item["partition_key"] == "PythonEncryptedResourceMigrationExample-2" + ) + assert item["sort_key"] == sort_read_value + assert item["attribute1"] == "encrypt and sign me!" + assert item["attribute2"] == "sign me!" + assert item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_3.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_3.py new file mode 100644 index 000000000..fc72fcf29 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/resource/migration_step_3.py @@ -0,0 +1,96 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 3. + +This is an example demonstrating how to update your configuration +to stop accepting reading items encrypted using the old format. +In order to proceed with this step, you will need to re-encrypt all +old items in your table. + +Once you complete Step 3, you can be sure that all items being read by your system +ensure the security properties configured for the new format. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +from .common import setup_pure_awsdbe_resource + + +def migration_step_3_with_resource(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 3): + """ + Migration Step 3: Using only pure AWS DBESDK (no legacy override) with EncryptedResource. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + """ + # 1. Create the EncryptedResource. + # Do not configure any legacy behavior. + encrypted_resource = setup_pure_awsdbe_resource(kms_key_id, ddb_table_name) + + # 2. Write a batch of items to the table. + # These items will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + items = [ + { + "partition_key": "PythonEncryptedResourceMigrationExample-1", + "sort_key": 3, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + { + "partition_key": "PythonEncryptedResourceMigrationExample-2", + "sort_key": 3, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + ] + + batch_write_items_put_request = { + "RequestItems": { + ddb_table_name: [{"PutRequest": {"Item": item}} for item in items], + }, + } + + batch_write_items_put_response = encrypted_resource.batch_write_item(**batch_write_items_put_request) + + # Demonstrate that BatchWriteItem succeeded + assert batch_write_items_put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Read the items back from the table. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we fail to return the item. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + batch_get_items_request = { + "RequestItems": { + ddb_table_name: { + "Keys": [{"partition_key": item["partition_key"], "sort_key": sort_read_value} for item in items], + } + }, + } + + batch_get_items_response = encrypted_resource.batch_get_item(**batch_get_items_request) + + # Demonstrate that BatchGetItem succeeded with the expected result + assert batch_get_items_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + for item in batch_get_items_response["Responses"][ddb_table_name]: + assert ( + item["partition_key"] == "PythonEncryptedResourceMigrationExample-1" + or item["partition_key"] == "PythonEncryptedResourceMigrationExample-2" + ) + assert item["sort_key"] == sort_read_value + assert item["attribute1"] == "encrypt and sign me!" + assert item["attribute2"] == "sign me!" + assert item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/common.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/common.py new file mode 100644 index 000000000..1f76897d4 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/common.py @@ -0,0 +1,235 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Common Utilities for Migration Examples.""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkMultiKeyringInput, + DBEAlgorithmSuiteId, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + LegacyOverride, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + +# Import from legacy DynamoDB Encryption Client +from dynamodb_encryption_sdk.encrypted.table import EncryptedTable as LegacyEncryptedTable +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider + + +def setup_pure_awsdbe_table(kms_key_id: str, ddb_table_name: str): + """ + Set up a pure AWS Database Encryption SDK EncryptedTable without legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :returns EncryptedTable for DynamoDB + """ + # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `CreateMrkMultiKeyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput( + generator=kms_key_id, + ) + kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsignAttrPrefix: str = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + # without the legacy override + table_configs = {} + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=kms_mrk_multi_keyring, + allowed_unsigned_attribute_prefix=unsignAttrPrefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + table_configs[ddb_table_name] = table_config + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the DB-ESDK EncryptedTable + return EncryptedTable( + table=boto3.resource("dynamodb").Table(ddb_table_name), + encryption_config=tables_config, + ) + + +def setup_awsdbe_table_with_legacy_override(kms_key_id: str, ddb_table_name: str, policy: str): + """ + Set up an AWS Database Encryption SDK EncryptedTable with legacy override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param policy: The policy required for the Legacy Override configuration + :returns EncryptedTable for DynamoDB + + """ + # 0. Create AWS SDK DynamoDB Client + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + + # 1. Create the legacy EncryptedTable + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + legacy_encrypted_table = LegacyEncryptedTable( + table=ddb_table, + materials_provider=cmp, + ) + + # 2. Configure our legacy behavior, inputting the DynamoDBEncryptor, attribute actions + # created above, and legacy policy. + legacy_override = LegacyOverride( + encryptor=legacy_encrypted_table, + attribute_actions_on_encrypt={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + policy=policy, + ) + + # 3. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `CreateMrkMultiKeyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput( + generator=kms_key_id, + ) + kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input) + + # 4. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 5. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsignAttrPrefix: str = ":" + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # without the legacy override + table_configs = {} + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=kms_mrk_multi_keyring, + legacy_override=legacy_override, + allowed_unsigned_attribute_prefix=unsignAttrPrefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + table_configs[ddb_table_name] = table_config + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the DB-ESDK EncryptedTable + return EncryptedTable( + table=boto3.resource("dynamodb").Table(ddb_table_name), + encryption_config=tables_config, + ) diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_1.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_1.py new file mode 100644 index 000000000..cdf514d8b --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_1.py @@ -0,0 +1,81 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 1. + +This is an example demonstrating how to start using the +AWS Database Encryption SDK with a pre-existing table used with DynamoDB Encryption Client. +In this example, you configure a EncryptedTable to do the following: + - Read items encrypted in the old format + - Continue to encrypt items in the old format on write + - Read items encrypted in the new format +While this step configures your client to be ready to start reading items encrypted, +we do not yet expect to be reading any items in the new format. +Before you move on to step 2, ensure that these changes have successfully been deployed +to all of your readers. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +from .common import setup_awsdbe_table_with_legacy_override + + +def migration_step_1_with_table(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 1): + """ + Migration Step 1: Using the AWS Database Encryption SDK with Legacy Override. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create a EncryptedTable with legacy override. + # For Legacy Policy, use `FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read and write items using the old format, + # but will be able to start reading new items in the new format as soon as they appear + policy = LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + encrypted_table = setup_awsdbe_table_with_legacy_override( + kms_key_id=kms_key_id, ddb_table_name=ddb_table_name, policy=policy + ) + + # 2. Put an item in the old format since we are using a legacy override + # with FORCE_LEGACY_ENCRYPT_ALLOW_DECRYPT policy + item_to_encrypt = { + "partition_key": "MigrationExampleForPythonTable", + "sort_key": 1, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + } + + put_item_response = encrypted_table.put_item(Item=item_to_encrypt) + + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get an item back from the table using the EncryptedTable + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + key_to_get = {"partition_key": "MigrationExampleForPythonTable", "sort_key": sort_read_value} + get_item_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"] == "MigrationExampleForPythonTable" + assert decrypted_item["sort_key"] == sort_read_value + assert decrypted_item["attribute1"] == "encrypt and sign me!" + assert decrypted_item["attribute2"] == "sign me!" + assert decrypted_item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_2.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_2.py new file mode 100644 index 000000000..8795d0e3c --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_2.py @@ -0,0 +1,82 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 2. + +This is an example demonstrating how to update your configuration +to start writing items using the latest encryption format, but still continue +to read any items written using the old encryption format. + +Once you deploy this change to your system, you will have a dataset +containing items in both the old and new format. +Because the changes in Step 1 have been deployed to all our readers, +we can be sure that our entire system is ready to read this new data. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +from aws_dbesdk_dynamodb.structures.dynamodb import LegacyPolicy + +# Import from new AWS Database Encryption SDK +from .common import setup_awsdbe_table_with_legacy_override + + +def migration_step_2_with_table(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 2): + """ + Migration Step 2: Using pure AWS DBESDK and legacy override together. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create a EncryptedTable with legacy override. + # When configuring our legacy behavior, use `FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT`. + # With this policy, you will continue to read items in both formats, + # but will only write new items using the new format. + encrypted_table = setup_awsdbe_table_with_legacy_override( + kms_key_id, ddb_table_name, policy=LegacyPolicy.FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT + ) + + # 2. Put an item into your table using the EncryptedTable. + # This item will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + item_to_encrypt = { + "partition_key": "MigrationExampleForPythonTable", + "sort_key": 2, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + } + + put_item_response = encrypted_table.put_item(Item=item_to_encrypt) + + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get an item back from the table using the EncryptedTable. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we will attempt to decrypt the item + # using the legacy behavior. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + key_to_get = {"partition_key": "MigrationExampleForPythonTable", "sort_key": sort_read_value} + get_item_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"] == "MigrationExampleForPythonTable" + assert decrypted_item["sort_key"] == sort_read_value + assert decrypted_item["attribute1"] == "encrypt and sign me!" + assert decrypted_item["attribute2"] == "sign me!" + assert decrypted_item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_3.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_3.py new file mode 100644 index 000000000..79e4b2c71 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/awsdbe/table/migration_step_3.py @@ -0,0 +1,72 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 3. + +This is an example demonstrating how to update your configuration +to stop accepting reading items encrypted using the old format. +In order to proceed with this step, you will need to re-encrypt all +old items in your table. + +Once you complete Step 3, you can be sure that all items being read by your system +ensure the security properties configured for the new format. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +from .common import setup_pure_awsdbe_table + + +def migration_step_3_with_table(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 3): + """ + Migration Step 3: Using only pure AWS DBESDK (no legacy override). + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + """ + # 1. Create the EncryptedTable + # Do not configure any legacy behavior. + encrypted_table = setup_pure_awsdbe_table(kms_key_id, ddb_table_name) + + # 2. Put an item into your table using the EncryptedTable. + # This item will be encrypted in the latest format, using the + # configuration from your modelled class to decide + # which attribute to encrypt and/or sign. + item_to_encrypt = { + "partition_key": "MigrationExampleForPythonTable", + "sort_key": 3, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + } + + put_item_response = encrypted_table.put_item(Item=item_to_encrypt) + + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 3. Get an item back from the table using the EncryptedTable. + # If this is an item written in the old format (e.g. any item written + # during Step 0 or 1), then we fail to return the item. + # If this is an item written in the new format (e.g. any item written + # during Step 2 or after), then we will attempt to decrypt the item using + # the non-legacy behavior. + key_to_get = {"partition_key": "MigrationExampleForPythonTable", "sort_key": sort_read_value} + get_item_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"] == "MigrationExampleForPythonTable" + assert decrypted_item["sort_key"] == sort_read_value + assert decrypted_item["attribute1"] == "encrypt and sign me!" + assert decrypted_item["attribute2"] == "sign me!" + assert decrypted_item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/README.md b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/README.md new file mode 100644 index 000000000..7da2dc662 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/README.md @@ -0,0 +1,13 @@ +# Step 0 + +In Step 0, your system is in the starting state using the legacy DynamoDB Encryption Client: + +- reads items in the old format using the DynamoDB Encryption Client +- writes items in the old format using the DynamoDB Encryption Client +- cannot read items in the new AWS Database Encryption SDK format + +This represents the baseline configuration before beginning the migration process. +Your dataset consists only of data written in the old format. + +When operating in this state, your system is fully dependent on the legacy DynamoDB Encryption Client library +and its associated encryption format. diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/client/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/client/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/client/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/client/migration_step_0.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/client/migration_step_0.py new file mode 100644 index 000000000..7f694de29 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/client/migration_step_0.py @@ -0,0 +1,83 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 0. + +This is an example demonstrating use with the DynamoDb Encryption Client. +and is the starting state for our migration to the AWS Database Encryption SDK for DynamoDb. +In this example we configure an AWS SDK Client configured to encrypt and decrypt +items. The encryption and decryption of data is configured to use a KMS Key as the root of trust. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +import boto3 + +# Import from legacy DynamoDB Encryption Client +from dynamodb_encryption_sdk.encrypted.client import EncryptedClient +from dynamodb_encryption_sdk.identifiers import CryptoAction +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider +from dynamodb_encryption_sdk.structures import AttributeActions + + +def migration_step_0_with_client(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 0): + """ + Migration Step 0: Using the DynamoDb Encryption Client with EncryptedClient. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create the MaterialProvider that protects your data keys. For this example, + # we create a KmsCryptographicMaterialsProvider which protects data keys using a single kmsKey. + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + + # 2. Create the DynamoDBEncryptor using the Material Provider created above + actions = AttributeActions( + default_action=CryptoAction.ENCRYPT_AND_SIGN, + attribute_actions={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + ) + + # 3. Create a legacy EncryptedClient. + ddb_client = boto3.client("dynamodb") + encrypted_client = EncryptedClient(client=ddb_client, materials_provider=cmp, attribute_actions=actions) + + # 4. Put an example item into our DynamoDb table. + # This item will be encrypted client-side before it is sent to DynamoDb. + item = { + "partition_key": {"S": "MigrationExampleForPython"}, + "sort_key": {"N": str(0)}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + encrypted_client.put_item(TableName=ddb_table_name, Item=item) + + # 5. Get this item back from DynamoDb. + # The item will be decrypted client-side, and the original item returned. + key = {"partition_key": {"S": "MigrationExampleForPython"}, "sort_key": {"N": str(sort_read_value)}} + + get_item_response = encrypted_client.get_item(TableName=ddb_table_name, Key=key) + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"]["S"] == "MigrationExampleForPython" + assert decrypted_item["sort_key"]["N"] == str(sort_read_value) + assert decrypted_item["attribute1"]["S"] == "encrypt and sign me!" + assert decrypted_item["attribute2"]["S"] == "sign me!" + assert decrypted_item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/paginator/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/paginator/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/paginator/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/paginator/migration_step_0.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/paginator/migration_step_0.py new file mode 100644 index 000000000..302376d1d --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/paginator/migration_step_0.py @@ -0,0 +1,95 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 0. + +This is an example demonstrating use with the DynamoDb Encryption Client. +and is the starting state for our migration to the AWS Database Encryption SDK for DynamoDb. +In this example we configure an EncryptedClient which provides an encrypted paginator +configured to encrypt and decrypt items. The encryption and decryption of data is +configured to use a KMS Key as the root of trust. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +import boto3 + +# Import from legacy DynamoDB Encryption Client +from dynamodb_encryption_sdk.encrypted.client import EncryptedClient +from dynamodb_encryption_sdk.identifiers import CryptoAction +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider +from dynamodb_encryption_sdk.structures import AttributeActions + + +def migration_step_0_with_paginator(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 0): + """ + Migration Step 0: Using the DynamoDb Encryption Client with EncryptedClient's paginator. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create the MaterialProvider that protects your data keys. For this example, + # we create a KmsCryptographicMaterialsProvider which protects data keys using a single kmsKey. + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + + # 2. Create the AttributeActions to configure encryption and signing + actions = AttributeActions( + default_action=CryptoAction.ENCRYPT_AND_SIGN, + attribute_actions={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + ) + + # 3. Create a legacy EncryptedClient. + ddb_client = boto3.client("dynamodb") + encrypted_client = EncryptedClient(client=ddb_client, materials_provider=cmp, attribute_actions=actions) + + # 4. Put an example item into our DynamoDb table. + # This item will be encrypted client-side before it is sent to DynamoDb. + item = { + "partition_key": {"S": "PaginatorMigrationExampleForPython"}, + "sort_key": {"N": "0"}, + "attribute1": {"S": "encrypt and sign me!"}, + "attribute2": {"S": "sign me!"}, + ":attribute3": {"S": "ignore me!"}, + } + + put_item_response = encrypted_client.put_item(TableName=ddb_table_name, Item=item) + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Get a paginator from the encrypted client + # The paginator will automatically decrypt items as they are returned. + encrypted_paginator = encrypted_client.get_paginator("query") + + # 6. Use the paginator to get items from the table + items = [] + for page in encrypted_paginator.paginate( + TableName=ddb_table_name, + KeyConditionExpression="partition_key = :partition_key AND sort_key = :sort_key", + ExpressionAttributeValues={ + ":partition_key": {"S": "PaginatorMigrationExampleForPython"}, + ":sort_key": {"N": str(sort_read_value)}, + }, + ): + for item in page["Items"]: + items.append(item) + + # 7. Verify the decrypted items + assert len(items) == 1 # We should have only one item with above key condition + item = next((i for i in items if i["sort_key"]["N"] == str(sort_read_value)), None) + assert item is not None + assert item["partition_key"]["S"] == "PaginatorMigrationExampleForPython" + assert item["attribute1"]["S"] == "encrypt and sign me!" + assert item["attribute2"]["S"] == "sign me!" + assert item[":attribute3"]["S"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/resource/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/resource/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/resource/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/resource/migration_step_0.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/resource/migration_step_0.py new file mode 100644 index 000000000..d4ef3adf9 --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/resource/migration_step_0.py @@ -0,0 +1,111 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 0. + +This is an example demonstrating use with the DynamoDb Encryption Client. +and is the starting state for our migration to the AWS Database Encryption SDK for DynamoDb. +In this example we configure an EncryptedResource configured to encrypt and decrypt +items. The encryption and decryption of data is configured to use a KMS Key as the root of trust. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +import boto3 + +# Import from legacy DynamoDB Encryption Resource +from dynamodb_encryption_sdk.encrypted.resource import EncryptedResource +from dynamodb_encryption_sdk.identifiers import CryptoAction +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider +from dynamodb_encryption_sdk.structures import AttributeActions + + +def migration_step_0_with_resource(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 0): + """ + Migration Step 0: Using the DynamoDb Encryption Client with EncryptedResource. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create the MaterialProvider that protects your data keys. For this example, + # we create a KmsCryptographicMaterialsProvider which protects data keys using a single kmsKey. + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + + # 2. Create the DynamoDBEncryptor using the Material Provider created above + actions = AttributeActions( + default_action=CryptoAction.ENCRYPT_AND_SIGN, + attribute_actions={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + ) + + # 3. Create a legacy EncryptedResource. + encrypted_resource = EncryptedResource( + resource=boto3.resource("dynamodb"), materials_provider=cmp, attribute_actions=actions + ) + + # 4. Write a batch of items to the table. + # These items will be encrypted client-side before they are sent to DynamoDB. + items = [ + { + "partition_key": "PythonEncryptedResourceMigrationExample-1", + "sort_key": 0, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + { + "partition_key": "PythonEncryptedResourceMigrationExample-2", + "sort_key": 0, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + }, + ] + + batch_write_items_put_request = { + "RequestItems": { + ddb_table_name: [{"PutRequest": {"Item": item}} for item in items], + }, + } + + batch_write_items_put_response = encrypted_resource.batch_write_item(**batch_write_items_put_request) + + # Demonstrate that BatchWriteItem succeeded + assert batch_write_items_put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Read the items back from the table. + # The items will be decrypted client-side, and the original items returned. + batch_get_items_request = { + "RequestItems": { + ddb_table_name: { + "Keys": [{"partition_key": item["partition_key"], "sort_key": sort_read_value} for item in items], + } + }, + } + + batch_get_items_response = encrypted_resource.batch_get_item(**batch_get_items_request) + + # Demonstrate that BatchGetItem succeeded with the expected result + assert batch_get_items_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + for item in batch_get_items_response["Responses"][ddb_table_name]: + assert ( + item["partition_key"] == "PythonEncryptedResourceMigrationExample-1" + or item["partition_key"] == "PythonEncryptedResourceMigrationExample-2" + ) + assert item["sort_key"] == sort_read_value + assert item["attribute1"] == "encrypt and sign me!" + assert item["attribute2"] == "sign me!" + assert item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/table/__init__.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/table/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/table/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/table/migration_step_0.py b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/table/migration_step_0.py new file mode 100644 index 000000000..b23f982cf --- /dev/null +++ b/Examples/runtimes/python/Migration/src/ddbec_to_awsdbe/ddbec/table/migration_step_0.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Migration Step 0. + +This is an example demonstrating use with the DynamoDb Encryption Client. +and is the starting state for our migration to the AWS Database Encryption SDK for DynamoDb. +In this example we configure an EncryptedTable configured to encrypt and decrypt +items. The encryption and decryption of data is configured to use a KMS Key as the root of trust. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +import boto3 + +# Import from legacy DynamoDB Encryption Table +from dynamodb_encryption_sdk.encrypted.table import EncryptedTable +from dynamodb_encryption_sdk.identifiers import CryptoAction +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider +from dynamodb_encryption_sdk.structures import AttributeActions + + +def migration_step_0_with_table(kms_key_id: str, ddb_table_name: str, sort_read_value: int = 0): + """ + Migration Step 0: Using the DynamoDb Encryption Client with EncryptedTable. + + :param kms_key_id: The ARN of the KMS key to use for encryption + :param ddb_table_name: The name of the DynamoDB table + :param sort_read_value: The sort key value to read + + """ + # 1. Create the MaterialProvider that protects your data keys. For this example, + # we create a KmsCryptographicMaterialsProvider which protects data keys using a single kmsKey. + cmp = AwsKmsCryptographicMaterialsProvider(key_id=kms_key_id) + + # 2. Create the DynamoDBEncryptor using the Material Provider created above + actions = AttributeActions( + default_action=CryptoAction.ENCRYPT_AND_SIGN, + attribute_actions={ + "partition_key": CryptoAction.SIGN_ONLY, + "sort_key": CryptoAction.SIGN_ONLY, + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + }, + ) + + # 3. Create a legacy EncryptedTable. + encrypted_table = EncryptedTable( + table=boto3.resource("dynamodb").Table(ddb_table_name), materials_provider=cmp, attribute_actions=actions + ) + + # 4. Put an example item into our DynamoDb table. + # This item will be encrypted client-side before it is sent to DynamoDb. + item_to_encrypt = { + "partition_key": "MigrationExampleForPythonTable", + "sort_key": 0, + "attribute1": "encrypt and sign me!", + "attribute2": "sign me!", + ":attribute3": "ignore me!", + } + + put_item_response = encrypted_table.put_item(Item=item_to_encrypt) + + # Demonstrate that PutItem succeeded + assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Get this item back from DynamoDb. + # The item will be decrypted client-side, and the original item returned. + key_to_get = {"partition_key": "MigrationExampleForPythonTable", "sort_key": sort_read_value} + + get_item_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + decrypted_item = get_item_response["Item"] + # Demonstrate we get the expected item back + assert decrypted_item["partition_key"] == "MigrationExampleForPythonTable" + assert decrypted_item["sort_key"] == sort_read_value + assert decrypted_item["attribute1"] == "encrypt and sign me!" + assert decrypted_item["attribute2"] == "sign me!" + assert decrypted_item[":attribute3"] == "ignore me!" diff --git a/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/__init__.py b/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/awsdbe/__init__.py b/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/awsdbe/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/awsdbe/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/plaintext/__init__.py b/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/plaintext/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/src/plaintext_to_awsdbe/plaintext/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/__init__.py b/Examples/runtimes/python/Migration/test/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_1.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_1.py new file mode 100644 index 000000000..691384c47 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_1.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 1. + +This test validates the compatibility between different stages of migration +and ensures that step 1 (using AWS DBESDK with legacy override) can read data +from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.client import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.client import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_1_with_client(): + """Test migration step 1 compatibility with different data formats.""" + # Successfully executes Step 1 + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 1 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 1 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 1 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_2.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_2.py new file mode 100644 index 000000000..d276fc002 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_2.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 2. + +This test validates the compatibility between different stages of migration +and ensures that step 2 (using AWS DBESDK with FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) +can read data from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.client import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.client import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_2_with_client(): + """Test migration step 2 compatibility with different data formats.""" + # Successfully executes Step 2 + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 2 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 2 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 2 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_3.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_3.py new file mode 100644 index 000000000..1f62e4b32 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/client/test_migration_step_3.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 3. + +This test validates the compatibility between different stages of migration +and ensures that step 3 (using only AWS DBESDK) behaves correctly with data +from different migration stages. +""" +import pytest +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) + +from .....src.ddbec_to_awsdbe.awsdbe.client import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.client import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_3_with_client(): + """Test migration step 3 compatibility with different data formats.""" + # Successfully executes Step 3 + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 3 with sort_read_value=0 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 3 with sort_read_value=1 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 3 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_1.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_1.py new file mode 100644 index 000000000..8a284c7a9 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_1.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 1. + +This test validates the compatibility between different stages of migration +and ensures that step 1 (using AWS DBESDK with legacy override) can read data +from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.paginator import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.paginator import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_1_with_paginator(): + """Test migration step 1 compatibility with different data formats.""" + # Successfully executes Step 1 + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 1 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 1 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 1 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_2.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_2.py new file mode 100644 index 000000000..0f690cfa6 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_2.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 2. + +This test validates the compatibility between different stages of migration +and ensures that step 2 (using AWS DBESDK with FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) +can read data from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.paginator import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.paginator import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_2_with_paginator(): + """Test migration step 2 compatibility with different data formats.""" + # Successfully executes Step 2 + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 2 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 2 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 2 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_3.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_3.py new file mode 100644 index 000000000..ffa8ba1e9 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/paginator/test_migration_step_3.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 3. + +This test validates the compatibility between different stages of migration +and ensures that step 3 (using only AWS DBESDK) behaves correctly with data +from different migration stages. +""" +import pytest +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) + +from .....src.ddbec_to_awsdbe.awsdbe.paginator import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.paginator import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_3_with_paginator(): + """Test migration step 3 compatibility with different data formats.""" + # Successfully executes Step 3 + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 3 with sort_read_value=0 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 3 with sort_read_value=1 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 3 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_1.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_1.py new file mode 100644 index 000000000..4bf65a6dd --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_1.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 1. + +This test validates the compatibility between different stages of migration +and ensures that step 1 (using AWS DBESDK with legacy override) can read data +from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.resource import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.resource import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_1_with_resource(): + """Test migration step 1 compatibility with different data formats.""" + # Successfully executes Step 1 + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 1 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 1 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 1 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_2.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_2.py new file mode 100644 index 000000000..7c07af889 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_2.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 2. + +This test validates the compatibility between different stages of migration +and ensures that step 2 (using AWS DBESDK with FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) +can read data from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.resource import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.resource import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_2_with_resource(): + """Test migration step 2 compatibility with different data formats.""" + # Successfully executes Step 2 + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 2 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 2 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 2 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_3.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_3.py new file mode 100644 index 000000000..ba5602ab3 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/resource/test_migration_step_3.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 3. + +This test validates the compatibility between different stages of migration +and ensures that step 3 (using only AWS DBESDK) behaves correctly with data +from different migration stages. +""" +import pytest +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) + +from .....src.ddbec_to_awsdbe.awsdbe.resource import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.resource import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_3_with_resource(): + """Test migration step 3 compatibility with different data formats.""" + # Successfully executes Step 3 + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 3 with sort_read_value=0 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 3 with sort_read_value=1 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 3 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_1.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_1.py new file mode 100644 index 000000000..0a7216ea7 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_1.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 1. + +This test validates the compatibility between different stages of migration +and ensures that step 1 (using AWS DBESDK with legacy override) can read data +from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.table import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.table import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_1_with_table(): + """Test migration step 1 compatibility with different data formats.""" + # Successfully executes Step 1 + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 1 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 1 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 1 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_2.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_2.py new file mode 100644 index 000000000..58f9010c8 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_2.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 2. + +This test validates the compatibility between different stages of migration +and ensures that step 2 (using AWS DBESDK with FORBID_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) +can read data from all other migration steps. +""" +import pytest + +from .....src.ddbec_to_awsdbe.awsdbe.table import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.table import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_2_with_table(): + """Test migration step 2 compatibility with different data formats.""" + # Successfully executes Step 2 + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 2 with sort_read_value=0 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 2 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 2 with sort_read_value=3 + # Then: Success (i.e. can read values in new format) + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_3.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_3.py new file mode 100644 index 000000000..fce9e4a9c --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/awsdbe/table/test_migration_step_3.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 3. + +This test validates the compatibility between different stages of migration +and ensures that step 3 (using only AWS DBESDK) behaves correctly with data +from different migration stages. +""" +import pytest +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import ( + DynamoDbItemEncryptor, +) + +from .....src.ddbec_to_awsdbe.awsdbe.table import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.table import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_3_with_table(): + """Test migration step 3 compatibility with different data formats.""" + # Successfully executes Step 3 + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + + # Given: Step 0 has succeeded + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + # When: Execute Step 3 with sort_read_value=0 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 3 with sort_read_value=1 + # Then: throws DynamoDbItemEncryptor Exception (i.e. cannot read values in old format) + with pytest.raises(DynamoDbItemEncryptor): + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 3 with sort_read_value=2 + # Then: Success (i.e. can read values in new format) + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/client/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/client/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/client/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/client/test_migration_step_0.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/client/test_migration_step_0.py new file mode 100644 index 000000000..23591c113 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/client/test_migration_step_0.py @@ -0,0 +1,61 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 0. + +This test validates the compatibility between different stages of migration +and ensures that step 0 (using legacy DynamoDB Encryption Client) behaves correctly +with data from different migration stages. +""" +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from .....src.ddbec_to_awsdbe.awsdbe.client import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.client import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_0_with_client(): + """Test migration step 0 compatibility with different data formats.""" + # Successfully executes Step 0 + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 0 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 0 with sort_read_value=2 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 0 with sort_read_value=3 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_client( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/paginator/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/paginator/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/paginator/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/paginator/test_migration_step_0.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/paginator/test_migration_step_0.py new file mode 100644 index 000000000..ddcfc32c1 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/paginator/test_migration_step_0.py @@ -0,0 +1,61 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 0. + +This test validates the compatibility between different stages of migration +and ensures that step 0 (using legacy DynamoDB Encryption Client) behaves correctly +with data from different migration stages. +""" +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from .....src.ddbec_to_awsdbe.awsdbe.paginator import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.paginator import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_0_with_paginator(): + """Test migration step 0 compatibility with different data formats.""" + # Successfully executes Step 0 + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 0 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 0 with sort_read_value=2 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 0 with sort_read_value=3 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_paginator( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/resource/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/resource/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/resource/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/resource/test_migration_step_0.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/resource/test_migration_step_0.py new file mode 100644 index 000000000..f39dfb58e --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/resource/test_migration_step_0.py @@ -0,0 +1,61 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 0. + +This test validates the compatibility between different stages of migration +and ensures that step 0 (using legacy DynamoDB Encryption Client) behaves correctly +with data from different migration stages. +""" +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from .....src.ddbec_to_awsdbe.awsdbe.resource import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.resource import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_0_with_resource(): + """Test migration step 0 compatibility with different data formats.""" + # Successfully executes Step 0 + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 0 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 0 with sort_read_value=2 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 0 with sort_read_value=3 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_resource( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/table/__init__.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/table/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/table/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/table/test_migration_step_0.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/table/test_migration_step_0.py new file mode 100644 index 000000000..05e532a24 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/ddbec/table/test_migration_step_0.py @@ -0,0 +1,61 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test for Migration Step 0. + +This test validates the compatibility between different stages of migration +and ensures that step 0 (using legacy DynamoDB Encryption Client with EncryptedTable) behaves correctly +with data from different migration stages. +""" +import pytest +from dynamodb_encryption_sdk.exceptions import DecryptionError + +from .....src.ddbec_to_awsdbe.awsdbe.table import ( + migration_step_1, + migration_step_2, + migration_step_3, +) +from .....src.ddbec_to_awsdbe.ddbec.table import migration_step_0 +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_migration_step_0_with_table(): + """Test migration step 0 compatibility with different data formats.""" + # Successfully executes Step 0 + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=0 + ) + + # Given: Step 1 has succeeded + migration_step_1.migration_step_1_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + # When: Execute Step 0 with sort_read_value=1 + # Then: Success (i.e. can read values in old format) + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=1 + ) + + # Given: Step 2 has succeeded + migration_step_2.migration_step_2_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + # When: Execute Step 0 with sort_read_value=2 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=2 + ) + + # Given: Step 3 has succeeded + migration_step_3.migration_step_3_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) + # When: Execute Step 0 with sort_read_value=3 + # Then: throws DecryptionError Exception (i.e. cannot read values in new format) + with pytest.raises(DecryptionError): # The exact exception may vary in Python implementation + migration_step_0.migration_step_0_with_table( + kms_key_id=TEST_KMS_KEY_ID, ddb_table_name=TEST_DDB_TABLE_NAME, sort_read_value=3 + ) diff --git a/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/test_utils.py b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/test_utils.py new file mode 100644 index 000000000..b7d58f186 --- /dev/null +++ b/Examples/runtimes/python/Migration/test/ddbec_to_awsdbe/test_utils.py @@ -0,0 +1,11 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test constants.""" + +# This is a public KMS Key that MUST only be used for testing, and MUST NOT be used for any production data +TEST_KMS_KEY_ID = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f" +# Personal Testing Resource +# TEST_KMS_KEY_ID = "arn:aws:kms:us-west-2:452750982249:key/773d55bf-c816-48a2-96cd-b386f7980d08" + +# Our tests require access to DDB Table with this name +TEST_DDB_TABLE_NAME = "DynamoDbEncryptionInterceptorTestTable" diff --git a/Examples/runtimes/python/pyproject.toml b/Examples/runtimes/python/pyproject.toml index 7ba0c8341..e292743d5 100644 --- a/Examples/runtimes/python/pyproject.toml +++ b/Examples/runtimes/python/pyproject.toml @@ -6,7 +6,7 @@ authors = ["AWS Crypto Tools "] [tool.poetry.dependencies] python = "^3.11.0" -aws-dbesdk-dynamodb = { path = "../../../DynamoDbEncryption/runtimes/python", develop = false} +aws-dbesdk-dynamodb = { path = "../../../DynamoDbEncryption/runtimes/python", develop = false, extras = ["legacy-ddbec"]} [tool.poetry.group.test.dependencies] pytest = "^7.4.0" diff --git a/Examples/runtimes/python/tox.ini b/Examples/runtimes/python/tox.ini index b3edda147..525f3596c 100644 --- a/Examples/runtimes/python/tox.ini +++ b/Examples/runtimes/python/tox.ini @@ -15,4 +15,4 @@ commands_pre = poetry install --with test --no-root commands = dynamodbencryption: {[testenv:base-command]commands} DynamoDBEncryption/test/ - migration: {[testenv:base-command]commands} Migration/PlaintextToAWSDBE/test/ \ No newline at end of file + migration: {[testenv:base-command]commands} Migration/test/ \ No newline at end of file diff --git a/Makefile b/Makefile index c1806faab..7bede0e44 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ format_java_misc-check: setup_prettier npx prettier --plugin=prettier-plugin-java . --check setup_prettier: - npm i --no-save prettier@3 prettier-plugin-java@2.5 + npm i --no-save prettier@3.5.3 prettier-plugin-java@2.5 # Generate the top-level project.properties file using smithy-dafny. # This is for the benefit of the nightly Dafny CI,