From 2cd56254ac50bdd814d0a8cb135e59052b77d039 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 18 Mar 2025 14:12:58 -0700 Subject: [PATCH 01/10] Implement a configurable credentials resolver chain --- .../aws/codegen/AwsAuthIntegration.java | 4 ++ .../credentials_resolvers/__init__.py | 8 ++- .../credentials_resolver_chain.py | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java index 1a54d2727..381273c58 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java @@ -57,6 +57,10 @@ public List getClientPlugins(GenerationContext context) { .build()) // TODO: Initialize with the provider chain? .nullable(true) + .initialize(writer -> { + writer.addImport("smithy_aws_core.credentials_resolvers", "CredentialsResolverChain"); + writer.write("self.aws_credentials_identity_resolver = aws_credentials_identity_resolver or CredentialsResolverChain()"); + }) .build()) .addConfigProperty(REGION) .authScheme(new Sigv4AuthScheme()) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py index 3aead11b3..14bf7b543 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py @@ -1,6 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 + from .environment import EnvironmentCredentialsResolver from .static import StaticCredentialsResolver +from .credentials_resolver_chain import CredentialsResolverChain -__all__ = ("EnvironmentCredentialsResolver", "StaticCredentialsResolver") +__all__ = ( + "EnvironmentCredentialsResolver", + "StaticCredentialsResolver", + "CredentialsResolverChain", +) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py new file mode 100644 index 000000000..da03892c5 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py @@ -0,0 +1,54 @@ +from typing import Callable, List + +from smithy_aws_core.credentials_resolvers import EnvironmentCredentialsResolver +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSCredentialsResolver +from smithy_core.aio.interfaces.identity import IdentityResolver +from smithy_core.exceptions import SmithyIdentityException +from smithy_core.interfaces.identity import IdentityProperties + +import os + + +def _env_creds_available() -> bool: + return bool(os.getenv("AWS_ACCESS_KEY_ID")) and bool( + os.getenv("AWS_SECRET_ACCESS_KEY") + ) + + +def _build_env_creds() -> AWSCredentialsResolver: + return EnvironmentCredentialsResolver() + + +type CredentialSource = tuple[Callable[[], bool], Callable[[], AWSCredentialsResolver]] +_DEFAULT_SOURCES: list[CredentialSource] = [(_env_creds_available, _build_env_creds)] + + +class CredentialsResolverChain( + IdentityResolver[AWSCredentialsIdentity, IdentityProperties] +): + """Resolves AWS Credentials from system environment variables.""" + + def __init__(self, *, sources: List[CredentialSource] | None = None): + if sources is None: + sources = _DEFAULT_SOURCES + self._sources: List[CredentialSource] = sources + self._credentials_resolver: AWSCredentialsResolver | None = None + + async def get_identity( + self, *, identity_properties: IdentityProperties + ) -> AWSCredentialsIdentity: + if self._credentials_resolver is not None: + return await self._credentials_resolver.get_identity( + identity_properties=identity_properties + ) + + for source in self._sources: + if source[0](): + self._credentials_resolver = source[1]() + return await self._credentials_resolver.get_identity( + identity_properties=identity_properties + ) + + raise SmithyIdentityException( + "None of the configured credentials sources were able to resolve credentials." + ) From 368fd21786b5603f98ce9e07522a03cee67bfc7a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 20 Mar 2025 09:29:20 -0700 Subject: [PATCH 02/10] Add test cases --- .../test_credentials_resolver_chain.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py diff --git a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py new file mode 100644 index 000000000..a26bfd172 --- /dev/null +++ b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py @@ -0,0 +1,56 @@ +import pytest +import os + +from smithy_aws_core.credentials_resolvers import CredentialsResolverChain, StaticCredentialsResolver +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_core.exceptions import SmithyIdentityException +from smithy_core.interfaces.identity import IdentityProperties + + +async def test_no_sources_resolve(): + resolver_chain = CredentialsResolverChain(sources=[]) + with pytest.raises(SmithyIdentityException): + await resolver_chain.get_identity(identity_properties=IdentityProperties()) + + +async def test_env_credentials_resolver_not_set(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("AWS_ACCESS_KEY_ID", raising=False) + monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) + resolver_chain = CredentialsResolverChain() + + with pytest.raises(SmithyIdentityException): + await resolver_chain.get_identity(identity_properties=IdentityProperties()) + + +async def test_env_credentials_resolver_partial(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "akid") + monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) + resolver_chain = CredentialsResolverChain() + + with pytest.raises(SmithyIdentityException): + await resolver_chain.get_identity(identity_properties=IdentityProperties()) + + +async def test_env_credentials_resolver_success(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "akid") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "secret") + resolver_chain = CredentialsResolverChain() + + credentials = await resolver_chain.get_identity(identity_properties=IdentityProperties()) + assert credentials.access_key_id == "akid" + assert credentials.secret_access_key == "secret" + + +async def test_custom_sources_with_static_credentials(): + static_credentials = AWSCredentialsIdentity( + access_key_id="static_akid", + secret_access_key="static_secret", + ) + static_resolver = StaticCredentialsResolver(credentials=static_credentials) + resolver_chain = CredentialsResolverChain( + sources=[(lambda: False, lambda: None), (lambda: True, lambda: static_resolver)]) + + credentials = await resolver_chain.get_identity(identity_properties=IdentityProperties()) + assert credentials.access_key_id == "static_akid" + assert credentials.secret_access_key == "static_secret" + From 87f73bab089c0c9ee735e444d70613273863c8d6 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 20 Mar 2025 09:30:39 -0700 Subject: [PATCH 03/10] Fix lint --- .../test_credentials_resolver_chain.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py index a26bfd172..f7bdd1536 100644 --- a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py +++ b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py @@ -1,7 +1,9 @@ import pytest -import os -from smithy_aws_core.credentials_resolvers import CredentialsResolverChain, StaticCredentialsResolver +from smithy_aws_core.credentials_resolvers import ( + CredentialsResolverChain, + StaticCredentialsResolver, +) from smithy_aws_core.identity import AWSCredentialsIdentity from smithy_core.exceptions import SmithyIdentityException from smithy_core.interfaces.identity import IdentityProperties @@ -36,7 +38,9 @@ async def test_env_credentials_resolver_success(monkeypatch: pytest.MonkeyPatch) monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "secret") resolver_chain = CredentialsResolverChain() - credentials = await resolver_chain.get_identity(identity_properties=IdentityProperties()) + credentials = await resolver_chain.get_identity( + identity_properties=IdentityProperties() + ) assert credentials.access_key_id == "akid" assert credentials.secret_access_key == "secret" @@ -48,9 +52,11 @@ async def test_custom_sources_with_static_credentials(): ) static_resolver = StaticCredentialsResolver(credentials=static_credentials) resolver_chain = CredentialsResolverChain( - sources=[(lambda: False, lambda: None), (lambda: True, lambda: static_resolver)]) + sources=[(lambda: False, lambda: None), (lambda: True, lambda: static_resolver)] # type: ignore + ) - credentials = await resolver_chain.get_identity(identity_properties=IdentityProperties()) + credentials = await resolver_chain.get_identity( + identity_properties=IdentityProperties() + ) assert credentials.access_key_id == "static_akid" assert credentials.secret_access_key == "static_secret" - From c33518b46ec2d025701d3daf65735cd43b9e5921 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 24 Mar 2025 12:10:01 -0700 Subject: [PATCH 04/10] Update packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py Co-authored-by: Nate Prewitt --- .../credentials_resolvers/credentials_resolver_chain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py index da03892c5..031b9f660 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py @@ -10,8 +10,9 @@ def _env_creds_available() -> bool: - return bool(os.getenv("AWS_ACCESS_KEY_ID")) and bool( - os.getenv("AWS_SECRET_ACCESS_KEY") + return ( + "AWS_ACCESS_KEY_ID" in os.environ + and "AWS_SECRET_ACCESS_KEY" in os.environ ) From 4351de178653d787aa51a37cdd1b10524bbc235c Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 24 Mar 2025 12:38:11 -0700 Subject: [PATCH 05/10] Updates to typing --- .../credentials_resolvers/__init__.py | 2 +- ...credentials_resolver_chain.py => chain.py} | 25 +++++++++---------- .../test_credentials_resolver_chain.py | 1 - uv.lock | 4 ++- 4 files changed, 16 insertions(+), 16 deletions(-) rename packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/{credentials_resolver_chain.py => chain.py} (76%) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py index ffcd922e7..f2b7ffd1b 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/__init__.py @@ -1,9 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from .chain import CredentialsResolverChain from .environment import EnvironmentCredentialsResolver from .imds import IMDSCredentialsResolver from .static import StaticCredentialsResolver -from .credentials_resolver_chain import CredentialsResolverChain __all__ = ( "CredentialsResolverChain", diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py similarity index 76% rename from packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py rename to packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py index 031b9f660..614c3d3f1 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/credentials_resolver_chain.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py @@ -1,19 +1,18 @@ -from typing import Callable, List +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +from collections.abc import Callable, Sequence -from smithy_aws_core.credentials_resolvers import EnvironmentCredentialsResolver -from smithy_aws_core.identity import AWSCredentialsIdentity, AWSCredentialsResolver from smithy_core.aio.interfaces.identity import IdentityResolver from smithy_core.exceptions import SmithyIdentityException from smithy_core.interfaces.identity import IdentityProperties -import os +from smithy_aws_core.credentials_resolvers import EnvironmentCredentialsResolver +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSCredentialsResolver def _env_creds_available() -> bool: - return ( - "AWS_ACCESS_KEY_ID" in os.environ - and "AWS_SECRET_ACCESS_KEY" in os.environ - ) + return "AWS_ACCESS_KEY_ID" in os.environ and "AWS_SECRET_ACCESS_KEY" in os.environ def _build_env_creds() -> AWSCredentialsResolver: @@ -21,7 +20,9 @@ def _build_env_creds() -> AWSCredentialsResolver: type CredentialSource = tuple[Callable[[], bool], Callable[[], AWSCredentialsResolver]] -_DEFAULT_SOURCES: list[CredentialSource] = [(_env_creds_available, _build_env_creds)] +_DEFAULT_SOURCES: Sequence[CredentialSource] = ( + (_env_creds_available, _build_env_creds), +) class CredentialsResolverChain( @@ -29,10 +30,8 @@ class CredentialsResolverChain( ): """Resolves AWS Credentials from system environment variables.""" - def __init__(self, *, sources: List[CredentialSource] | None = None): - if sources is None: - sources = _DEFAULT_SOURCES - self._sources: List[CredentialSource] = sources + def __init__(self, *, sources: Sequence[CredentialSource] = _DEFAULT_SOURCES): + self._sources: Sequence[CredentialSource] = sources self._credentials_resolver: AWSCredentialsResolver | None = None async def get_identity( diff --git a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py index f7bdd1536..db5bbb91c 100644 --- a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py +++ b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py @@ -1,5 +1,4 @@ import pytest - from smithy_aws_core.credentials_resolvers import ( CredentialsResolverChain, StaticCredentialsResolver, diff --git a/uv.lock b/uv.lock index d716ebc20..6856c99ed 100644 --- a/uv.lock +++ b/uv.lock @@ -686,6 +686,7 @@ dependencies = [ [package.optional-dependencies] aiohttp = [ { name = "aiohttp" }, + { name = "yarl" }, ] awscrt = [ { name = "awscrt" }, @@ -693,9 +694,10 @@ awscrt = [ [package.metadata] requires-dist = [ - { name = "aiohttp", marker = "extra == 'aiohttp'", specifier = ">=3.11.12" }, + { name = "aiohttp", marker = "extra == 'aiohttp'", specifier = ">=3.11.12,<4.0" }, { name = "awscrt", marker = "extra == 'awscrt'", specifier = ">=0.23.10" }, { name = "smithy-core", editable = "packages/smithy-core" }, + { name = "yarl", marker = "extra == 'aiohttp'" }, ] provides-extras = ["awscrt", "aiohttp"] From b325e70b09cb3a950dd42db75d3c2e84e9147cf8 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 24 Mar 2025 13:33:15 -0700 Subject: [PATCH 06/10] Refactor Sources into a class/Protocol --- .../aws/codegen/AwsAuthIntegration.java | 2 +- .../credentials_resolvers/chain.py | 41 ++++++++-------- .../credentials_resolvers/environment.py | 17 ++++++- .../credentials_resolvers/imds.py | 17 ++++++- .../credentials_resolvers/interfaces.py | 24 ++++++++++ .../test_credentials_resolver_chain.py | 48 ++++++++++++++++--- 6 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java index 381273c58..9c829316c 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java @@ -59,7 +59,7 @@ public List getClientPlugins(GenerationContext context) { .nullable(true) .initialize(writer -> { writer.addImport("smithy_aws_core.credentials_resolvers", "CredentialsResolverChain"); - writer.write("self.aws_credentials_identity_resolver = aws_credentials_identity_resolver or CredentialsResolverChain()"); + writer.write("self.aws_credentials_identity_resolver = aws_credentials_identity_resolver or CredentialsResolverChain(config=self)"); }) .build()) .addConfigProperty(REGION) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py index 614c3d3f1..81192ea21 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py @@ -1,27 +1,24 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import os -from collections.abc import Callable, Sequence +from collections.abc import Sequence from smithy_core.aio.interfaces.identity import IdentityResolver from smithy_core.exceptions import SmithyIdentityException from smithy_core.interfaces.identity import IdentityProperties -from smithy_aws_core.credentials_resolvers import EnvironmentCredentialsResolver +from smithy_aws_core.credentials_resolvers.environment import ( + EnvironmentCredentialsSource, +) +from smithy_aws_core.credentials_resolvers.imds import IMDSCredentialsSource +from smithy_aws_core.credentials_resolvers.interfaces import ( + AwsCredentialsConfig, + CredentialsSource, +) from smithy_aws_core.identity import AWSCredentialsIdentity, AWSCredentialsResolver - -def _env_creds_available() -> bool: - return "AWS_ACCESS_KEY_ID" in os.environ and "AWS_SECRET_ACCESS_KEY" in os.environ - - -def _build_env_creds() -> AWSCredentialsResolver: - return EnvironmentCredentialsResolver() - - -type CredentialSource = tuple[Callable[[], bool], Callable[[], AWSCredentialsResolver]] -_DEFAULT_SOURCES: Sequence[CredentialSource] = ( - (_env_creds_available, _build_env_creds), +_DEFAULT_SOURCES: Sequence[CredentialsSource] = ( + EnvironmentCredentialsSource(), + IMDSCredentialsSource(), ) @@ -30,8 +27,14 @@ class CredentialsResolverChain( ): """Resolves AWS Credentials from system environment variables.""" - def __init__(self, *, sources: Sequence[CredentialSource] = _DEFAULT_SOURCES): - self._sources: Sequence[CredentialSource] = sources + def __init__( + self, + *, + config: AwsCredentialsConfig, + sources: Sequence[CredentialsSource] = _DEFAULT_SOURCES, + ): + self._config = config + self._sources: Sequence[CredentialsSource] = sources self._credentials_resolver: AWSCredentialsResolver | None = None async def get_identity( @@ -43,8 +46,8 @@ async def get_identity( ) for source in self._sources: - if source[0](): - self._credentials_resolver = source[1]() + if source.is_available(config=self._config): + self._credentials_resolver = source.build_resolver(config=self._config) return await self._credentials_resolver.get_identity( identity_properties=identity_properties ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/environment.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/environment.py index 34cea57a4..27e0f8032 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/environment.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/environment.py @@ -6,7 +6,12 @@ from smithy_core.exceptions import SmithyIdentityException from smithy_core.interfaces.identity import IdentityProperties -from ..identity import AWSCredentialsIdentity +from smithy_aws_core.credentials_resolvers.interfaces import ( + AwsCredentialsConfig, + CredentialsSource, +) + +from ..identity import AWSCredentialsIdentity, AWSCredentialsResolver class EnvironmentCredentialsResolver( @@ -41,3 +46,13 @@ async def get_identity( ) return self._credentials + + +class EnvironmentCredentialsSource(CredentialsSource): + def is_available(self, config: AwsCredentialsConfig) -> bool: + return ( + "AWS_ACCESS_KEY_ID" in os.environ and "AWS_SECRET_ACCESS_KEY" in os.environ + ) + + def build_resolver(self, config: AwsCredentialsConfig) -> AWSCredentialsResolver: + return EnvironmentCredentialsResolver() diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py index 6ae6fee09..12706f01e 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py @@ -17,8 +17,13 @@ from smithy_http.aio import HTTPRequest from smithy_http.aio.interfaces import HTTPClient +from smithy_aws_core.credentials_resolvers.interfaces import ( + AwsCredentialsConfig, + CredentialsSource, +) + from .. import __version__ -from ..identity import AWSCredentialsIdentity +from ..identity import AWSCredentialsIdentity, AWSCredentialsResolver _USER_AGENT_FIELD = Field( name="User-Agent", @@ -235,3 +240,13 @@ async def get_identity( account_id=account_id, ) return self._credentials + + +class IMDSCredentialsSource(CredentialsSource): + def is_available(self, config: AwsCredentialsConfig) -> bool: + # IMDS credentials should always be the last in the chain + # We cannot check if they available without actually making a call + return True + + def build_resolver(self, config: AwsCredentialsConfig) -> AWSCredentialsResolver: + return IMDSCredentialsResolver(http_client=config.http_client) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py new file mode 100644 index 000000000..40e910596 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py @@ -0,0 +1,24 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Protocol + +from smithy_http.aio.interfaces import HTTPClient + +from smithy_aws_core.identity import AWSCredentialsResolver + + +class AwsCredentialsConfig(Protocol): + """Configuration required for resolving credentials.""" + + http_client: HTTPClient + """A static endpoint to use for the request.""" + + +class CredentialsSource(Protocol): + def is_available(self, config: AwsCredentialsConfig) -> bool: + """Returns True if credentials are available from this source.""" + ... + + def build_resolver(self, config: AwsCredentialsConfig) -> AWSCredentialsResolver: + """Builds a credentials resolver for the given configuration.""" + ... diff --git a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py index db5bbb91c..cf0d73b1c 100644 --- a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py +++ b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py @@ -1,15 +1,34 @@ +from dataclasses import dataclass +from unittest.mock import Mock + import pytest from smithy_aws_core.credentials_resolvers import ( CredentialsResolverChain, StaticCredentialsResolver, ) -from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.credentials_resolvers.environment import ( + EnvironmentCredentialsSource, +) +from smithy_aws_core.credentials_resolvers.interfaces import ( + AwsCredentialsConfig, + CredentialsSource, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSCredentialsResolver from smithy_core.exceptions import SmithyIdentityException from smithy_core.interfaces.identity import IdentityProperties +from smithy_http.aio.interfaces import HTTPClient + + +@dataclass +class Config: + http_client: HTTPClient + + def __init__(self): + self.http_client = Mock(spec=HTTPClient) # type: ignore async def test_no_sources_resolve(): - resolver_chain = CredentialsResolverChain(sources=[]) + resolver_chain = CredentialsResolverChain(sources=[], config=Config()) with pytest.raises(SmithyIdentityException): await resolver_chain.get_identity(identity_properties=IdentityProperties()) @@ -17,7 +36,9 @@ async def test_no_sources_resolve(): async def test_env_credentials_resolver_not_set(monkeypatch: pytest.MonkeyPatch): monkeypatch.delenv("AWS_ACCESS_KEY_ID", raising=False) monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) - resolver_chain = CredentialsResolverChain() + resolver_chain = CredentialsResolverChain( + sources=[EnvironmentCredentialsSource()], config=Config() + ) with pytest.raises(SmithyIdentityException): await resolver_chain.get_identity(identity_properties=IdentityProperties()) @@ -26,7 +47,9 @@ async def test_env_credentials_resolver_not_set(monkeypatch: pytest.MonkeyPatch) async def test_env_credentials_resolver_partial(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("AWS_ACCESS_KEY_ID", "akid") monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) - resolver_chain = CredentialsResolverChain() + resolver_chain = CredentialsResolverChain( + sources=[EnvironmentCredentialsSource()], config=Config() + ) with pytest.raises(SmithyIdentityException): await resolver_chain.get_identity(identity_properties=IdentityProperties()) @@ -35,7 +58,9 @@ async def test_env_credentials_resolver_partial(monkeypatch: pytest.MonkeyPatch) async def test_env_credentials_resolver_success(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("AWS_ACCESS_KEY_ID", "akid") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "secret") - resolver_chain = CredentialsResolverChain() + resolver_chain = CredentialsResolverChain( + sources=[EnvironmentCredentialsSource()], config=Config() + ) credentials = await resolver_chain.get_identity( identity_properties=IdentityProperties() @@ -50,8 +75,19 @@ async def test_custom_sources_with_static_credentials(): secret_access_key="static_secret", ) static_resolver = StaticCredentialsResolver(credentials=static_credentials) + + class TestStaticSource(CredentialsSource): + def is_available(self, config: AwsCredentialsConfig) -> bool: + return True + + def build_resolver( + self, config: AwsCredentialsConfig + ) -> AWSCredentialsResolver: + return static_resolver + resolver_chain = CredentialsResolverChain( - sources=[(lambda: False, lambda: None), (lambda: True, lambda: static_resolver)] # type: ignore + sources=[TestStaticSource()], + config=Config(), # type: ignore ) credentials = await resolver_chain.get_identity( From d14e85e0b1158b6eb38fa31f949895f43c6614e3 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Mar 2025 10:17:22 -0700 Subject: [PATCH 07/10] Doc string update --- .../src/smithy_aws_core/credentials_resolvers/chain.py | 2 +- .../src/smithy_aws_core/credentials_resolvers/imds.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py index 81192ea21..fc9b8ab77 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/chain.py @@ -25,7 +25,7 @@ class CredentialsResolverChain( IdentityResolver[AWSCredentialsIdentity, IdentityProperties] ): - """Resolves AWS Credentials from system environment variables.""" + """Resolves AWS Credentials from an ordered list of credentials sources.""" def __init__( self, diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py index 12706f01e..26288b753 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py @@ -249,4 +249,5 @@ def is_available(self, config: AwsCredentialsConfig) -> bool: return True def build_resolver(self, config: AwsCredentialsConfig) -> AWSCredentialsResolver: + # TODO: Configure lower number of retries/lower timeout return IMDSCredentialsResolver(http_client=config.http_client) From 086d9cec7fd6d53cff8ebb31b17857011db209a8 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Mar 2025 09:31:31 -0700 Subject: [PATCH 08/10] Improve tests --- .../test_credentials_resolver_chain.py | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py index cf0d73b1c..e6adbee24 100644 --- a/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py +++ b/packages/smithy-aws-core/tests/unit/credentials_resolvers/test_credentials_resolver_chain.py @@ -4,6 +4,7 @@ import pytest from smithy_aws_core.credentials_resolvers import ( CredentialsResolverChain, + IMDSCredentialsResolver, StaticCredentialsResolver, ) from smithy_aws_core.credentials_resolvers.environment import ( @@ -55,13 +56,39 @@ async def test_env_credentials_resolver_partial(monkeypatch: pytest.MonkeyPatch) await resolver_chain.get_identity(identity_properties=IdentityProperties()) -async def test_env_credentials_resolver_success(monkeypatch: pytest.MonkeyPatch): +async def test_default_sources_env_credentials_resolver_success( + monkeypatch: pytest.MonkeyPatch, +): monkeypatch.setenv("AWS_ACCESS_KEY_ID", "akid") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "secret") - resolver_chain = CredentialsResolverChain( - sources=[EnvironmentCredentialsSource()], config=Config() + resolver_chain = CredentialsResolverChain(config=Config()) + + credentials = await resolver_chain.get_identity( + identity_properties=IdentityProperties() + ) + assert credentials.access_key_id == "akid" + assert credentials.secret_access_key == "secret" + + +async def test_default_sources_imds_resolver_success(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("AWS_ACCESS_KEY_ID", raising=False) + monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) + + async def mock_imds_get_identity( + self: IMDSCredentialsResolver, *, identity_properties: IdentityProperties + ) -> AWSCredentialsIdentity: + return AWSCredentialsIdentity( + access_key_id="akid", + secret_access_key="secret", + ) + + monkeypatch.setattr( + "smithy_aws_core.credentials_resolvers.IMDSCredentialsResolver.get_identity", + mock_imds_get_identity, ) + resolver_chain = CredentialsResolverChain(config=Config()) + credentials = await resolver_chain.get_identity( identity_properties=IdentityProperties() ) @@ -69,6 +96,63 @@ async def test_env_credentials_resolver_success(monkeypatch: pytest.MonkeyPatch) assert credentials.secret_access_key == "secret" +async def test_multiple_sources_one_valid(): + class FailingSource(CredentialsSource): + def is_available(self, config: AwsCredentialsConfig) -> bool: + return False + + def build_resolver( + self, config: AwsCredentialsConfig + ) -> AWSCredentialsResolver: + raise RuntimeError("Should not be called") + + static_credentials = AWSCredentialsIdentity( + access_key_id="valid_akid", secret_access_key="valid_secret" + ) + static_resolver = StaticCredentialsResolver(credentials=static_credentials) + + class ValidSource(CredentialsSource): + def is_available(self, config: AwsCredentialsConfig) -> bool: + return True + + def build_resolver( + self, config: AwsCredentialsConfig + ) -> AWSCredentialsResolver: + return static_resolver + + resolver_chain = CredentialsResolverChain( + sources=[FailingSource(), ValidSource()], config=Config() + ) + + credentials = await resolver_chain.get_identity( + identity_properties=IdentityProperties() + ) + assert credentials.access_key_id == "valid_akid" + assert credentials.secret_access_key == "valid_secret" + + +async def test_cached_resolver_used(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "cached_akid") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "cached_secret") + resolver_chain = CredentialsResolverChain( + sources=[EnvironmentCredentialsSource()], config=Config() + ) + + credentials1 = await resolver_chain.get_identity( + identity_properties=IdentityProperties() + ) + credentials2 = await resolver_chain.get_identity( + identity_properties=IdentityProperties() + ) + + assert credentials1.access_key_id == credentials2.access_key_id == "cached_akid" + assert ( + credentials1.secret_access_key + == credentials2.secret_access_key + == "cached_secret" + ) + + async def test_custom_sources_with_static_credentials(): static_credentials = AWSCredentialsIdentity( access_key_id="static_akid", From ef5e16978418778921e50898fc4fe4230ec96ae4 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 27 Mar 2025 09:20:44 -0700 Subject: [PATCH 09/10] Fix doc --- .../src/smithy_aws_core/credentials_resolvers/interfaces.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py index 40e910596..315050728 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/interfaces.py @@ -11,7 +11,6 @@ class AwsCredentialsConfig(Protocol): """Configuration required for resolving credentials.""" http_client: HTTPClient - """A static endpoint to use for the request.""" class CredentialsSource(Protocol): From b7412fbf6f1af69c6d79e6546336158df90cb652 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 27 Mar 2025 09:21:07 -0700 Subject: [PATCH 10/10] Update packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py Co-authored-by: Nate Prewitt --- .../src/smithy_aws_core/credentials_resolvers/imds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py index 26288b753..619cca472 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/credentials_resolvers/imds.py @@ -245,7 +245,7 @@ async def get_identity( class IMDSCredentialsSource(CredentialsSource): def is_available(self, config: AwsCredentialsConfig) -> bool: # IMDS credentials should always be the last in the chain - # We cannot check if they available without actually making a call + # We cannot check if they're available without actually making a call return True def build_resolver(self, config: AwsCredentialsConfig) -> AWSCredentialsResolver: