From 7808e6410b2be77d9f6b3a4b9a781687f390d75e Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 May 2026 14:14:29 -0700 Subject: [PATCH 1/4] chore: apply user agent string with version --- src/s3_encryption/__init__.py | 4 +++- src/s3_encryption/_utils.py | 12 ++++++++++++ src/s3_encryption/materials/kms_keyring.py | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index ad07fe43..3f188510 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -9,7 +9,7 @@ from botocore.exceptions import ClientError from botocore.response import StreamingBody -from ._utils import safe_get_dict +from ._utils import _USER_AGENT_SUFFIX, append_user_agent, safe_get_dict from .exceptions import S3EncryptionClientError from .instruction_file import parse_instruction_file from .instruction_file_config import InstructionFileConfig @@ -350,6 +350,8 @@ def __attrs_post_init__(self): # Expose plugin context on wrapped client for instruction file fetching self.wrapped_s3_client._s3ec_plugin_context = self._plugin._context + append_user_agent(self.wrapped_s3_client, _USER_AGENT_SUFFIX) + # Register event handlers using boto3's event system event_system = self.wrapped_s3_client.meta.events event_system.register("before-call.s3.PutObject", self._plugin.on_put_object_before_call) diff --git a/src/s3_encryption/_utils.py b/src/s3_encryption/_utils.py index 4997b973..7cab9e27 100644 --- a/src/s3_encryption/_utils.py +++ b/src/s3_encryption/_utils.py @@ -2,6 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 """Internal utility helpers for the S3 Encryption Client.""" +import importlib.metadata + +_PACKAGE_VERSION = importlib.metadata.version("amazon-s3-encryption-client-python") +_USER_AGENT_SUFFIX = f"S3ECPy/{_PACKAGE_VERSION}" + def safe_get_dict(source: dict, key: str) -> dict: """Get a dict value from *source*, defaulting to {} if missing or None. @@ -10,3 +15,10 @@ def safe_get_dict(source: dict, key: str) -> dict: when the key exists but its value is explicitly None. """ return source.get(key, {}) or {} + + +def append_user_agent(client, suffix: str): + """Append a suffix to the User-Agent header of a boto3 client.""" + existing = client.meta.config.user_agent_extra or "" + sep = " " if existing else "" + client.meta.config.user_agent_extra = f"{existing}{sep}{suffix}" diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index abd6fad4..6550a38b 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -53,6 +53,11 @@ class KmsKeyring(S3Keyring): ##% The KmsV1 mode MUST be only enabled when legacy wrapping algorithms are enabled. enable_legacy_wrapping_algorithms: bool = field(default=False) + def __attrs_post_init__(self): # noqa: D105 + from .._utils import _USER_AGENT_SUFFIX, append_user_agent + + append_user_agent(self.kms_client, _USER_AGENT_SUFFIX) + def on_encrypt(self, enc_materials): """Process encryption materials using KMS. From fed6ed8d07fcb7516ca1212cff2116b5156f7de8 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 May 2026 15:42:51 -0700 Subject: [PATCH 2/4] add test --- test/test_user_agent.py | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 test/test_user_agent.py diff --git a/test/test_user_agent.py b/test/test_user_agent.py new file mode 100644 index 00000000..53d18a4d --- /dev/null +++ b/test/test_user_agent.py @@ -0,0 +1,44 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for user agent string injection.""" +from unittest.mock import MagicMock, patch + +import boto3 + +from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig +from s3_encryption._utils import _PACKAGE_VERSION, _USER_AGENT_SUFFIX +from s3_encryption.materials.kms_keyring import KmsKeyring + + +class TestUserAgent: + def test_user_agent_suffix_format(self): + assert _USER_AGENT_SUFFIX == f"S3ECPy/{_PACKAGE_VERSION}" + + def test_s3_client_gets_user_agent(self): + s3 = boto3.client("s3", region_name="us-east-1") + kms = boto3.client("kms", region_name="us-east-1") + keyring = KmsKeyring(kms, "arn:aws:kms:us-east-1:000000000000:key/fake") + config = S3EncryptionClientConfig(keyring=keyring) + + S3EncryptionClient(s3, config) + + assert _USER_AGENT_SUFFIX in s3.meta.config.user_agent_extra + + def test_kms_client_gets_user_agent(self): + kms = boto3.client("kms", region_name="us-east-1") + KmsKeyring(kms, "arn:aws:kms:us-east-1:000000000000:key/fake") + + assert _USER_AGENT_SUFFIX in kms.meta.config.user_agent_extra + + def test_existing_user_agent_extra_preserved(self): + s3 = boto3.client("s3", region_name="us-east-1") + s3.meta.config.user_agent_extra = "existing-agent/1.0" + + kms = boto3.client("kms", region_name="us-east-1") + keyring = KmsKeyring(kms, "arn:aws:kms:us-east-1:000000000000:key/fake") + config = S3EncryptionClientConfig(keyring=keyring) + + S3EncryptionClient(s3, config) + + assert "existing-agent/1.0" in s3.meta.config.user_agent_extra + assert _USER_AGENT_SUFFIX in s3.meta.config.user_agent_extra From 48df237770514abd34ae59e6f628e94dd4cc543b Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 May 2026 15:50:53 -0700 Subject: [PATCH 3/4] lint --- test/test_user_agent.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_user_agent.py b/test/test_user_agent.py index 53d18a4d..d46d13fc 100644 --- a/test/test_user_agent.py +++ b/test/test_user_agent.py @@ -1,8 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """Unit tests for user agent string injection.""" -from unittest.mock import MagicMock, patch - import boto3 from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig @@ -12,7 +10,7 @@ class TestUserAgent: def test_user_agent_suffix_format(self): - assert _USER_AGENT_SUFFIX == f"S3ECPy/{_PACKAGE_VERSION}" + assert f"S3ECPy/{_PACKAGE_VERSION}" == _USER_AGENT_SUFFIX def test_s3_client_gets_user_agent(self): s3 = boto3.client("s3", region_name="us-east-1") From 89940c9e2cec9e6f64153b5884aa65d0535450f0 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 May 2026 16:06:45 -0700 Subject: [PATCH 4/4] linter again ole ole --- test/test_user_agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_user_agent.py b/test/test_user_agent.py index d46d13fc..ad7f6a30 100644 --- a/test/test_user_agent.py +++ b/test/test_user_agent.py @@ -1,6 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """Unit tests for user agent string injection.""" + import boto3 from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig