main/migration/plaintext_to_awsdbe/awsdbe/
migration_step_3.rs

1// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::migration::plaintext_to_awsdbe::awsdbe::common::create_table_configs;
5use crate::migration::plaintext_to_awsdbe::migration_utils::{
6    verify_returned_item, DO_NOTHING_VALUE, ENCRYPTED_AND_SIGNED_VALUE, SIGN_ONLY_VALUE,
7};
8use aws_db_esdk::dynamodb::types::PlaintextOverride;
9use aws_db_esdk::intercept::DbEsdkInterceptor;
10use aws_sdk_dynamodb::types::AttributeValue;
11use std::collections::HashMap;
12
13/*
14Migration Step 3: This is the final step in the migration process from
15plaintext to encrypted DynamoDB using the AWS Database Encryption SDK.
16
17In this example, we configure a DynamoDB Encryption client to do the following:
181. Write items with encryption (no longer writing plaintext)
192. Read only encrypted items (no longer reading plaintext)
20
21Once you complete Step 3, all items being read by your system are encrypted.
22
23Before you move onto this step, you will need to encrypt all plaintext items in your dataset.
24How you will want to do this depends on your system.
25
26Running this example requires access to the DDB Table whose name
27is provided in the function parameter.
28This table must be configured with the following
29primary key configuration:
30  - Partition key is named "partition_key" with type (S)
31  - Sort key is named "sort_key" with type (N)
32*/
33pub async fn migration_step_3_example(
34    kms_key_id: &str,
35    ddb_table_name: &str,
36    partition_key_value: &str,
37    sort_key_write_value: &str,
38    sort_key_read_value: &str,
39) -> Result<bool, Box<dyn std::error::Error>> {
40    // 1. Create table configurations
41    // In this step of migration we will use PlaintextOverride::ForbidPlaintextWriteForbidPlaintextRead
42    // which means:
43    //     - Write: Items are forbidden to be written as plaintext.
44    //              Items will be written as encrypted items.
45    //     - Read: Items are forbidden to be read as plaintext.
46    //             Items will be read as encrypted items.
47    // Note: If you do not specify a PlaintextOverride, it defaults to
48    //       ForbidPlaintextWriteForbidPlaintextRead, which is the desired
49    //       behavior for a client interacting with a fully encrypted database.
50    let table_configs = create_table_configs(
51        kms_key_id,
52        ddb_table_name,
53        PlaintextOverride::ForbidPlaintextWriteForbidPlaintextRead,
54    )
55    .await?;
56
57    // 2. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs
58    let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
59    let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
60        .interceptor(DbEsdkInterceptor::new(table_configs)?)
61        .build();
62    let ddb = aws_sdk_dynamodb::Client::from_conf(dynamo_config);
63
64    // 3. Put an item into our table using the above client.
65    //    This item will be encrypted due to our PlaintextOverride configuration.
66    let partition_key_name = "partition_key";
67    let sort_key_name = "sort_key";
68    let encrypted_and_signed_value = ENCRYPTED_AND_SIGNED_VALUE;
69    let sign_only_value = SIGN_ONLY_VALUE;
70    let do_nothing_value = DO_NOTHING_VALUE;
71    let item = HashMap::from([
72        (
73            partition_key_name.to_string(),
74            AttributeValue::S(partition_key_value.to_string()),
75        ),
76        (
77            sort_key_name.to_string(),
78            AttributeValue::N(sort_key_write_value.to_string()),
79        ),
80        (
81            "attribute1".to_string(),
82            AttributeValue::S(encrypted_and_signed_value.to_string()),
83        ),
84        (
85            "attribute2".to_string(),
86            AttributeValue::S(sign_only_value.to_string()),
87        ),
88        (
89            "attribute3".to_string(),
90            AttributeValue::S(do_nothing_value.to_string()),
91        ),
92    ]);
93
94    ddb.put_item()
95        .table_name(ddb_table_name)
96        .set_item(Some(item))
97        .send()
98        .await?;
99
100    // 4. Get an item back from the table using the same client.
101    //    If this is an item written in plaintext (i.e. any item written
102    //    during Step 0 or 1), then the read will fail, as we have
103    //    configured our client to forbid reading plaintext items.
104    //    If this is an item that was encrypted client-side (i.e. any item written
105    //    during Step 2 or after), then the item will be decrypted client-side
106    //    and surfaced as a plaintext item.
107    let key = HashMap::from([
108        (
109            partition_key_name.to_string(),
110            AttributeValue::S(partition_key_value.to_string()),
111        ),
112        (
113            sort_key_name.to_string(),
114            AttributeValue::N(sort_key_read_value.to_string()),
115        ),
116    ]);
117
118    let response = ddb
119        .get_item()
120        .table_name(ddb_table_name)
121        .set_key(Some(key))
122        // In this example we configure a strongly consistent read
123        // because we perform a read immediately after a write (for demonstrative purposes).
124        // By default, reads are only eventually consistent.
125        .consistent_read(true)
126        .send()
127        .await?;
128
129    // Verify we get the expected item back
130    if let Some(item) = response.item {
131        let success = verify_returned_item(&item, partition_key_value, sort_key_read_value)?;
132        if success {
133            println!("MigrationStep3 completed successfully");
134        }
135        Ok(success)
136    } else {
137        Err("No item found".into())
138    }
139}
140
141#[tokio::test(flavor = "multi_thread")]
142async fn test_migration_step_3() -> Result<(), Box<dyn std::error::Error>> {
143    use crate::migration::plaintext_to_awsdbe::awsdbe::migration_step_1::migration_step_1_example;
144    use crate::migration::plaintext_to_awsdbe::awsdbe::migration_step_2::migration_step_2_example;
145    use crate::migration::plaintext_to_awsdbe::plaintext::migration_step_0::migration_step_0_example;
146    use crate::test_utils;
147    use uuid::Uuid;
148
149    let kms_key_id = test_utils::TEST_KMS_KEY_ID;
150    let table_name = test_utils::TEST_DDB_TABLE_NAME;
151    let partition_key = Uuid::new_v4().to_string();
152    let sort_keys = ["0", "1", "2", "3"];
153
154    // Successfully executes step 3
155    let success = migration_step_3_example(
156        kms_key_id,
157        table_name,
158        &partition_key,
159        sort_keys[3],
160        sort_keys[3],
161    )
162    .await?;
163    assert!(success, "MigrationStep3 should complete successfully");
164
165    // Given: Step 0 has succeeded
166    let success =
167        migration_step_0_example(table_name, &partition_key, sort_keys[0], sort_keys[0]).await?;
168    assert!(success, "MigrationStep0 should complete successfully");
169
170    // When: Execute Step 3 with sortReadValue=0, Then: should error out when reading plaintext items from Step 0
171    let result = migration_step_3_example(
172        kms_key_id,
173        table_name,
174        &partition_key,
175        sort_keys[3],
176        sort_keys[0],
177    )
178    .await;
179    assert!(
180        result.is_err(),
181        "MigrationStep3 should fail when reading plaintext items"
182    );
183
184    // Given: Step 1 has succeeded
185    let success = migration_step_1_example(
186        kms_key_id,
187        table_name,
188        &partition_key,
189        sort_keys[1],
190        sort_keys[1],
191    )
192    .await?;
193    assert!(success, "MigrationStep1 should complete successfully");
194
195    // When: Execute Step 3 with sortReadValue=1, Then: should error out when reading plaintext items from Step 1
196    let result = migration_step_3_example(
197        kms_key_id,
198        table_name,
199        &partition_key,
200        sort_keys[3],
201        sort_keys[1],
202    )
203    .await;
204    assert!(
205        result.is_err(),
206        "MigrationStep3 should fail when reading plaintext items"
207    );
208
209    // Given: Step 2 has succeeded
210    let success = migration_step_2_example(
211        kms_key_id,
212        table_name,
213        &partition_key,
214        sort_keys[2],
215        sort_keys[2],
216    )
217    .await?;
218    assert!(success, "MigrationStep2 should complete successfully");
219
220    // When: Execute Step 3 with sortReadValue=2, Then: Success (i.e. can read encrypted values from Step 2)
221    let success = migration_step_3_example(
222        kms_key_id,
223        table_name,
224        &partition_key,
225        sort_keys[3],
226        sort_keys[2],
227    )
228    .await?;
229    assert!(
230        success,
231        "MigrationStep3 should be able to read items written by Step 2"
232    );
233
234    // Cleanup
235    for sort_key in &sort_keys {
236        test_utils::cleanup_items(table_name, &partition_key, sort_key).await?;
237    }
238
239    Ok(())
240}