Add modular credential chain for AWS identity resolution#749
Conversation
|
|
||
|
|
||
| @dataclass(frozen=True, kw_only=True) | ||
| class NamedResolver: |
There was a problem hiding this comment.
This will also eventually associate an identity resolver with other provider metadata like a feature id set, not just its name. I'm open to renaming this resolver wrapper.
| self._terminal = True | ||
|
|
||
|
|
||
| class ChainIdentityProvider(Protocol): |
There was a problem hiding this comment.
I just realized that this class will eventually need to expose a feature_ids property and may need to expose new things in the future. This would break anybody that initializes their provider without creating a subclass from this protocol, even if we provide a default.
We can probably switch this to an ABC to allow us to add these properties with defaults that automatically propagate to third-party implementations. We could also implement a more specific protocol like AwsChainIdentityProvider that we control while directing third party implementations to use the base ChainIdentityProvider. Curious what reviewers think.
jonathan343
left a comment
There was a problem hiding this comment.
Thanks @arandito!
This PR is looking pretty good. I left a few comments, let me know if you have any questions!
| self.canonical_name = canonical_name | ||
| self.module_suggestion = module_suggestion | ||
|
|
||
| def is_detected(self) -> bool: |
There was a problem hiding this comment.
nit - could we use match / case here instead of multiple if's?
def is_detected(self) -> bool:
"""Return whether this slot has a cheap environment detection signal."""
match self:
case StandardProvider.ENVIRONMENT:
return bool(os.getenv("AWS_ACCESS_KEY_ID"))
case StandardProvider.WEB_IDENTITY_TOKEN_ENV:
return bool(os.getenv("AWS_WEB_IDENTITY_TOKEN_FILE")) and bool(
os.getenv("AWS_ROLE_ARN")
)
case StandardProvider.ECS_CONTAINER:
return bool(os.getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")) or bool(
os.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
)
case StandardProvider.SHARED_CONFIG:
return shared_config_files_exist()
case _:
return False| unclaimed_sources=unclaimed_sources, | ||
| ) | ||
|
|
||
| async def get_identity(self, *, properties: Mapping[str, Any]) -> I: |
There was a problem hiding this comment.
nit - Can we make properties optional and default to {}? I was testing this locally and if felt weird that I was forced to pass in properties={}:
(smithy-python) $ python -m asyncio
asyncio REPL 3.12.6 (main, Sep 24 2024, 21:09:37) [Clang 16.0.0 (clang-1600.0.26.3)] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from smithy_aws_core.identity.chain import IdentityChain
>>> from smithy_aws_core.identity.components import AWSCredentialsIdentity
>>> chain = await IdentityChain.create(AWSCredentialsIdentity)
>>> await chain.get_identity(properties={})
AWSCredentialsIdentity(access_key_id='...', secret_access_key='.', session_token=None, expiration=None, account_id=None)| account_id = os.getenv("AWS_ACCOUNT_ID") | ||
|
|
||
| if access_key_id is None or secret_access_key is None: | ||
| if not access_key_id or not secret_access_key: |
There was a problem hiding this comment.
Why this change? Is this to catch empty strings?
| return tuple(unclaimed_sources) | ||
|
|
||
|
|
||
| class IdentityChain[I: Identity](IdentityResolver[I, Mapping[str, Any]]): |
There was a problem hiding this comment.
Don't we need to add an invalidate method here? Something like:
async def invalidate(self) -> None:
for resolver in self._resolvers:
await resolver.invalidate()This will also require the IdentityResolver interface to be updated. I think we can implement a no-op as default. Then resolvers that need custom behavior can implement what they need. This does force custom resolvers to implement as well, but I think that's fine.
| return await self.resolver.get_identity(properties=properties) | ||
|
|
||
|
|
||
| class ChainSetup: |
There was a problem hiding this comment.
Why no region override?
Description
This PR adds modular credential resolution to Smithy Python via a new
IdentityChain. Today users must set a singleaws_credentials_identity_resolveron the client config; there is no fallback chain to automatically detect and resolve credentials from the environment, shared config files, etc. The newIdentityChainfills that gap.IdentityChainimplements the existingIdentityResolverinterface, so it drops into the current config slot unchanged. A client cannot tell whether it holds a single resolver or a chain.The chain follows a two-phase model:
Assembly (once, at construction).
IdentityChain.create():smithy_aws_core.identity.chain_providersentry point group. Installing a package that exposes a provider to that group is all that is needed to add a provider to the chain.Standard/Before/Afterordering constraints against a fixed set ofStandardProviderslots.setup()with a sharedChainSetupcontext. A provider inspects the environment and active profile, and if it detects its source, registers a resolver. Registering a terminal resolver (a source that is authoritative once detected, e.g. complete environment credentials) stops further assembly. When assembly finishes,ChainSetupis discarded and only the ordered resolvers survive.Resolution (per request).
IdentityChain.get_identity()walks the assembled resolvers in order, returning the first identity that resolves. A resolver "misses" by raisingSmithyIdentityError, and the chain moves on. If every resolver misses, it raisesIdentityChainErrorwith per-provider diagnostics. When a known credential source is detected on the system but no provider claims it, the error also suggests the package to install.This PR initially ships four standard providers (
Environment,SharedConfig,ProfileSessionKeys, andProfileStaticKeys) all of which detect their source locally with no network calls. Network-backed providers (STS, SSO, ECS, IMDS) are designed to ship as separate optional packages that register under the same entry point group.Note
Network providers and integration into SDK client construction will be added in follow-up PRs. This PR adds standalone support for now.
Usage
Create a chain from installed providers:
Construct a chain with explicit resolvers (useful for tests or an explicit, non-discovered chain). Here
StaticCredentialsResolveris seeded with a fixedAWSCredentialsIdentity, so the chain falls back to those credentials if the environment resolver misses:Custom providers implement
ChainIdentityProviderand position themselves relative to a standard slot:Register a custom provider so
IdentityChain.create()can discover it:Testing
make test-pyBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.