Skip to content

Commit 112798d

Browse files
authored
chore(python): examples for client_supplier
2 parents e9d473b + b55c13f commit 112798d

File tree

6 files changed

+345
-0
lines changed

6 files changed

+345
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Stub to allow relative imports of examples from tests."""
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example demonstrating DynamoDB Encryption using a custom client supplier.
5+
6+
A custom client supplier grants users access to more granular configuration aspects
7+
of their authentication details and KMS client. The example creates a simple custom
8+
client supplier that authenticates with a different IAM role based on the region
9+
of the KMS key.
10+
11+
Creates a MRK multi-keyring configured with a custom client supplier using a single
12+
MRK and puts an encrypted item to the table. Then, creates a MRK discovery
13+
multi-keyring to decrypt the item and retrieves the item from the table.
14+
15+
Running this example requires access to the DDB Table whose name is provided in
16+
CLI arguments. This table must be configured with the following primary key
17+
configuration:
18+
- Partition key is named "partition_key" with type (S)
19+
- Sort key is named "sort_key" with type (N)
20+
"""
21+
22+
from typing import List
23+
24+
import boto3
25+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
26+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
27+
from aws_cryptographic_material_providers.mpl.models import (
28+
CreateAwsKmsMrkDiscoveryMultiKeyringInput,
29+
CreateAwsKmsMrkMultiKeyringInput,
30+
DiscoveryFilter,
31+
)
32+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
33+
from aws_dbesdk_dynamodb.structures.dynamodb import (
34+
DynamoDbTableEncryptionConfig,
35+
DynamoDbTablesEncryptionConfig,
36+
)
37+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
38+
CryptoAction,
39+
)
40+
41+
from .regional_role_client_supplier import RegionalRoleClientSupplier
42+
43+
44+
def client_supplier_example(ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str]) -> None:
45+
"""
46+
Demonstrate using custom client supplier with AWS KMS MRK keyrings.
47+
48+
Shows how to use a custom client supplier with AWS KMS MRK multi-keyring and AWS
49+
KMS MRK discovery multi-keyring.
50+
51+
:param ddb_table_name: The name of the DynamoDB table
52+
:param key_arn: The ARN of the AWS KMS key
53+
:param account_ids: List of AWS account IDs
54+
:param regions: List of AWS regions
55+
"""
56+
# 1. Create a single MRK multi-keyring.
57+
# This can be either a single-region KMS key or an MRK.
58+
# For this example to succeed, the key's region must either
59+
# 1) be in the regions list, or
60+
# 2) the key must be an MRK with a replica defined
61+
# in a region in the regions list, and the client
62+
# must have the correct permissions to access the replica.
63+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
64+
65+
# Create the multi-keyring using our custom client supplier
66+
# defined in the RegionalRoleClientSupplier class in this directory.
67+
create_aws_kms_mrk_multi_keyring_input = CreateAwsKmsMrkMultiKeyringInput(
68+
# Note: RegionalRoleClientSupplier will internally use the keyArn's region
69+
# to retrieve the correct IAM role.
70+
client_supplier=RegionalRoleClientSupplier(),
71+
generator=key_arn,
72+
)
73+
mrk_keyring_with_client_supplier = mat_prov.create_aws_kms_mrk_multi_keyring(
74+
input=create_aws_kms_mrk_multi_keyring_input
75+
)
76+
77+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
78+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
79+
# we must explicitly configure how they should be treated during item encryption:
80+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
81+
# - SIGN_ONLY: The attribute is not encrypted, but is still included in the signature
82+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
83+
attribute_actions_on_encrypt = {
84+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
85+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
86+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
87+
}
88+
89+
# 3. Configure which attributes we expect to be included in the signature
90+
# when reading items. There are two options for configuring this:
91+
#
92+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
93+
# When defining your DynamoDb schema and deciding on attribute names,
94+
# choose a distinguishing prefix (such as ":") for all attributes that
95+
# you do not want to include in the signature.
96+
# This has two main benefits:
97+
# - It is easier to reason about the security and authenticity of data within your item
98+
# when all unauthenticated data is easily distinguishable by their attribute name.
99+
# - If you need to add new unauthenticated attributes in the future,
100+
# you can easily make the corresponding update to your `attribute_actions_on_encrypt`
101+
# and immediately start writing to that new attribute, without
102+
# any other configuration update needed.
103+
# Once you configure this field, it is not safe to update it.
104+
#
105+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
106+
# a set of attributes that should be considered unauthenticated when encountered
107+
# on read. Be careful if you use this configuration. Do not remove an attribute
108+
# name from this configuration, even if you are no longer writing with that attribute,
109+
# as old items may still include this attribute, and our configuration needs to know
110+
# to continue to exclude this attribute from the signature scope.
111+
# If you add new attribute names to this field, you must first deploy the update to this
112+
# field to all readers in your host fleet before deploying the update to start writing
113+
# with that new attribute.
114+
#
115+
# For this example, we currently authenticate all attributes. To make it easier to
116+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
117+
unsign_attr_prefix = ":"
118+
119+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
120+
table_config = DynamoDbTableEncryptionConfig(
121+
logical_table_name=ddb_table_name,
122+
partition_key_name="partition_key",
123+
sort_key_name="sort_key",
124+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
125+
keyring=mrk_keyring_with_client_supplier,
126+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
127+
)
128+
129+
table_configs = {ddb_table_name: table_config}
130+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
131+
132+
# 5. Create the EncryptedClient
133+
ddb_client = boto3.client("dynamodb")
134+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
135+
136+
# 6. Put an item into our table using the above client.
137+
# Before the item gets sent to DynamoDb, it will be encrypted
138+
# client-side using the MRK multi-keyring.
139+
# The data key protecting this item will be encrypted
140+
# with all the KMS Keys in this keyring, so that it can be
141+
# decrypted with any one of those KMS Keys.
142+
item = {
143+
"partition_key": {"S": "clientSupplierItem"},
144+
"sort_key": {"N": "0"},
145+
"sensitive_data": {"S": "encrypt and sign me!"},
146+
}
147+
148+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
149+
150+
# Demonstrate that PutItem succeeded
151+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
152+
153+
# 7. Get the item back from our table using the same keyring.
154+
# The client will decrypt the item client-side using the MRK
155+
# and return the original item.
156+
key_to_get = {"partition_key": {"S": "clientSupplierItem"}, "sort_key": {"N": "0"}}
157+
158+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
159+
160+
# Demonstrate that GetItem succeeded and returned the decrypted item
161+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
162+
returned_item = get_response["Item"]
163+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
164+
165+
# 8. Create a MRK discovery multi-keyring with a custom client supplier.
166+
# A discovery MRK multi-keyring will be composed of
167+
# multiple discovery MRK keyrings, one for each region.
168+
# Each component keyring has its own KMS client in a particular region.
169+
# When we provide a client supplier to the multi-keyring, all component
170+
# keyrings will use that client supplier configuration.
171+
# In our tests, we make `key_arn` an MRK with a replica, and
172+
# provide only the replica region in our discovery filter.
173+
discovery_filter = DiscoveryFilter(partition="aws", account_ids=account_ids)
174+
175+
mrk_discovery_client_supplier_input = CreateAwsKmsMrkDiscoveryMultiKeyringInput(
176+
client_supplier=RegionalRoleClientSupplier(), discovery_filter=discovery_filter, regions=regions
177+
)
178+
179+
mrk_discovery_client_supplier_keyring = mat_prov.create_aws_kms_mrk_discovery_multi_keyring(
180+
input=mrk_discovery_client_supplier_input
181+
)
182+
183+
# 9. Create a new config and client using the discovery keyring.
184+
# This is the same setup as above, except we provide the discovery keyring to the config.
185+
replica_key_table_config = DynamoDbTableEncryptionConfig(
186+
logical_table_name=ddb_table_name,
187+
partition_key_name="partition_key",
188+
sort_key_name="sort_key",
189+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
190+
# Provide discovery keyring here
191+
keyring=mrk_discovery_client_supplier_keyring,
192+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
193+
)
194+
195+
replica_key_tables_config = {ddb_table_name: replica_key_table_config}
196+
replica_key_tables_encryption_config = DynamoDbTablesEncryptionConfig(
197+
table_encryption_configs=replica_key_tables_config
198+
)
199+
200+
replica_key_encrypted_client = EncryptedClient(
201+
client=ddb_client, encryption_config=replica_key_tables_encryption_config
202+
)
203+
204+
# 10. Get the item back from our table using the discovery keyring client.
205+
# The client will decrypt the item client-side using the keyring,
206+
# and return the original item.
207+
# The discovery keyring will only use KMS keys in the provided regions and
208+
# AWS accounts. Since we have provided it with a custom client supplier
209+
# which uses different IAM roles based on the key region,
210+
# the discovery keyring will use a particular IAM role to decrypt
211+
# based on the region of the KMS key it uses to decrypt.
212+
replica_key_key_to_get = {"partition_key": {"S": "awsKmsMrkMultiKeyringItem"}, "sort_key": {"N": "0"}}
213+
214+
replica_key_get_response = replica_key_encrypted_client.get_item(
215+
TableName=ddb_table_name, Key=replica_key_key_to_get
216+
)
217+
218+
# Demonstrate that GetItem succeeded and returned the decrypted item
219+
assert replica_key_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
220+
replica_key_returned_item = replica_key_get_response["Item"]
221+
assert replica_key_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Demonstrates implementing a custom client supplier.
5+
6+
Creates KMS clients with different IAM roles depending on the region passed.
7+
"""
8+
9+
import logging
10+
11+
import boto3
12+
from aws_cryptographic_material_providers.mpl.models import GetClientInput
13+
from aws_cryptographic_material_providers.mpl.references import ClientSupplier
14+
from botocore.exceptions import ClientError
15+
16+
from .regional_role_client_supplier_config import RegionalRoleClientSupplierConfig
17+
18+
19+
class RegionalRoleClientSupplier(ClientSupplier):
20+
"""
21+
Custom client supplier for region-specific IAM roles.
22+
23+
Creates KMS clients with different IAM roles depending on the region passed.
24+
"""
25+
26+
def __init__(self):
27+
"""Initialize the client supplier with STS client and configuration."""
28+
self._sts_client = boto3.client("sts")
29+
self._config = RegionalRoleClientSupplierConfig()
30+
self._logger = logging.getLogger(__name__)
31+
32+
def get_client(self, input_params: GetClientInput) -> boto3.client:
33+
"""
34+
Get a KMS client for the specified region using the configured IAM role.
35+
36+
In test environments where assuming the role might fail, we fall back to
37+
creating a standard KMS client for the region without assuming a role.
38+
This ensures examples can run in test environments without proper IAM permissions.
39+
40+
:param input_params: Input parameters containing the region
41+
:return: A boto3 KMS client for the specified region with the appropriate credentials
42+
"""
43+
region = input_params.region
44+
if region not in self._config.region_iam_role_map:
45+
self._logger.warning(f"Missing region in config: {region}. Using default client.")
46+
return boto3.client("kms", region_name=region)
47+
48+
role_arn = self._config.region_iam_role_map[region]
49+
50+
try:
51+
# Assume the IAM role for the region
52+
response = self._sts_client.assume_role(
53+
RoleArn=role_arn,
54+
DurationSeconds=900, # 15 minutes is the minimum value
55+
RoleSessionName="Python-Client-Supplier-Example-Session",
56+
)
57+
58+
# Create a KMS client with the temporary credentials
59+
return boto3.client(
60+
"kms",
61+
region_name=region,
62+
aws_access_key_id=response["Credentials"]["AccessKeyId"],
63+
aws_secret_access_key=response["Credentials"]["SecretAccessKey"],
64+
aws_session_token=response["Credentials"]["SessionToken"],
65+
)
66+
except ClientError as e:
67+
# In test environments, fall back to a standard client
68+
self._logger.warning(f"Failed to assume role: {str(e)}. Falling back to default client.")
69+
return boto3.client("kms", region_name=region)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Configuration for the RegionalRoleClientSupplier.
5+
6+
Contains hardcoded configuration values for demonstration purposes. In production
7+
code, these values might be loaded from environment variables, AWS AppConfig, or
8+
other external sources.
9+
"""
10+
11+
12+
class RegionalRoleClientSupplierConfig:
13+
"""
14+
Configuration class mapping AWS regions to IAM roles.
15+
16+
Provides a mapping between AWS regions and their corresponding IAM roles for
17+
use in the RegionalRoleClientSupplier. For demonstration purposes, this uses
18+
hardcoded values.
19+
"""
20+
21+
US_EAST_1_IAM_ROLE = "arn:aws:iam::370957321024:role/GitHub-CI-DDBEC-Dafny-Role-only-us-east-1-KMS-keys"
22+
EU_WEST_1_IAM_ROLE = "arn:aws:iam::370957321024:role/GitHub-CI-DDBEC-Dafny-Role-only-eu-west-1-KMS-keys"
23+
24+
def __init__(self):
25+
"""Initialize the configuration with region to IAM role mapping."""
26+
self.region_iam_role_map = {"us-east-1": self.US_EAST_1_IAM_ROLE, "eu-west-1": self.EU_WEST_1_IAM_ROLE}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test suite for the client_supplier examples."""
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test suite for the client supplier example."""
4+
import pytest
5+
6+
from ...src.client_supplier.client_supplier_example import client_supplier_example
7+
from .. import test_utils
8+
9+
pytestmark = [pytest.mark.examples]
10+
11+
12+
def test_client_supplier_example():
13+
"""Test function for client supplier example."""
14+
accounts = [test_utils.TEST_AWS_ACCOUNT_ID]
15+
regions = ["eu-west-1"] # Using eu-west-1
16+
17+
# Call the client_supplier_example with the test parameters
18+
client_supplier_example(
19+
ddb_table_name=test_utils.TEST_DDB_TABLE_NAME,
20+
key_arn=test_utils.TEST_MRK_REPLICA_KEY_ID_US_EAST_1,
21+
account_ids=accounts,
22+
regions=regions,
23+
)

0 commit comments

Comments
 (0)