Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "bugfix",
"description": "The AssumeRole chain provider no longer claims a profile that sets `role_arn` and `web_identity_token_file` without `source_profile` or `credential_source`, so the profile resolves through web identity instead of failing."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "feature",
"description": "Added `WebIdentityCredentialsResolver` and `AsyncWebIdentityCredentialsResolver`, which resolve credentials with STS `AssumeRoleWithWebIdentity` from an OIDC token file, and the `WebIdentityTokenEnvProvider` and `ProfileWebIdentityProvider` chain providers (with `Async*` equivalents), so the default credential chain uses web identity configured by the `AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN` environment variables or a profile's `web_identity_token_file` and `role_arn`."
}
46 changes: 37 additions & 9 deletions packages/aws-credentials-sts/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# aws-credentials-sts

This package provides STS-based credential resolvers and a chain provider:
This package provides STS-based credential resolvers and chain providers:

- `AssumeRoleCredentialsResolver` - assumes an explicit `role_arn` using
credentials from a `source_resolver`.
- `ProfileAssumeRoleCredentialsResolver` - assumes a role configured in a
named profile in the shared config/credentials files.
- `WebIdentityCredentialsResolver` - assumes `role_arn` with an OIDC token read
from `web_identity_token_file`, using STS `AssumeRoleWithWebIdentity`.

Each resolver and provider has an async equivalent in `aws_credentials_sts.aio`.

## Installation

```shell
uv pip install aws-credentials-sts
```

Once installed, the provider registers itself with the SDK's modular credential
chain. When a client resolves credentials through the default chain, it
will attempt to assume the role configured in the active profile
(`role_arn`/`source_profile`/`credential_source`), unless a higher-precedence
source resolves credentials first.
Once installed, the providers register themselves with the SDK's modular
credential chain. When a client resolves credentials through the default chain,
unless a higher-precedence source resolves credentials first, it will:

- assume a role with web identity when the `AWS_WEB_IDENTITY_TOKEN_FILE` and
`AWS_ROLE_ARN` environment variables are set, and `AWS_ROLE_SESSION_NAME`
optionally names the session. This is skipped when a profile is selected
explicitly.
- assume the role configured in the active profile
(`role_arn`/`source_profile`/`credential_source`).
- assume a role with web identity configured in the active profile
(`web_identity_token_file`/`role_arn`/`role_session_name`).

## Client Configuration

Expand All @@ -30,7 +41,7 @@ from aws_credentials_sts import AssumeRoleCredentialsResolver
from smithy_aws_core.identity import EnvironmentCredentialsResolver

service_client = ServiceClient(
config=ServiceClientConfig(
config=ServiceClientConfig.resolve(
aws_credentials_identity_resolver=AssumeRoleCredentialsResolver(
source_resolver=EnvironmentCredentialsResolver(),
role_arn="arn:aws:iam::123456789012:role/example-role",
Expand All @@ -50,7 +61,7 @@ from smithy_aws_core.config import load_config

def build_client() -> ServiceClient:
return ServiceClient(
config=ServiceClientConfig(
config=ServiceClientConfig.resolve(
aws_credentials_identity_resolver=ProfileAssumeRoleCredentialsResolver(
profile_name="my-profile",
config_file=load_config(),
Expand All @@ -59,9 +70,26 @@ def build_client() -> ServiceClient:
)
```

To assume a role with web identity explicitly, use
`WebIdentityCredentialsResolver`. The token file is read each time the
credentials are refreshed, so a token rotated on disk is picked up:

```python
from aws_credentials_sts import WebIdentityCredentialsResolver

service_client = ServiceClient(
config=ServiceClientConfig.resolve(
aws_credentials_identity_resolver=WebIdentityCredentialsResolver(
role_arn="arn:aws:iam::123456789012:role/example-role",
web_identity_token_file="/var/run/secrets/token",
),
)
)
```

## Standalone

Either resolver can also be used on its own to fetch credentials directly:
Any resolver can also be used on its own to fetch credentials directly:

```python
import asyncio
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-credentials-sts/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ dependencies = [

[project.entry-points."smithy_aws_core.identity.chain_providers"]
ProfileAssumeRole = "aws_credentials_sts.providers:ProfileAssumeRoleProvider"
ProfileWebIdentity = "aws_credentials_sts.providers:ProfileWebIdentityProvider"
WebIdentityTokenEnv = "aws_credentials_sts.providers:WebIdentityTokenEnvProvider"

[project.entry-points."smithy_aws_core.aio.identity.chain_providers"]
ProfileAssumeRole = "aws_credentials_sts.aio.providers:AsyncProfileAssumeRoleProvider"
ProfileWebIdentity = "aws_credentials_sts.aio.providers:AsyncProfileWebIdentityProvider"
WebIdentityTokenEnv = "aws_credentials_sts.aio.providers:AsyncWebIdentityTokenEnvProvider"

[dependency-groups]
test = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@
# SPDX-License-Identifier: Apache-2.0
__version__ = "0.4.0"

from .providers import ProfileAssumeRoleProvider
from .providers import (
ProfileAssumeRoleProvider,
ProfileWebIdentityProvider,
WebIdentityTokenEnvProvider,
)
from .resolvers import (
AssumeRoleConfigurationError,
AssumeRoleCredentialsResolver,
ProfileAssumeRoleCredentialsResolver,
WebIdentityConfigurationError,
WebIdentityCredentialsResolver,
)

__all__ = (
"AssumeRoleConfigurationError",
"AssumeRoleCredentialsResolver",
"ProfileAssumeRoleCredentialsResolver",
"ProfileAssumeRoleProvider",
"ProfileWebIdentityProvider",
"WebIdentityConfigurationError",
"WebIdentityCredentialsResolver",
"WebIdentityTokenEnvProvider",
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
_DURATION_SECONDS = "duration_seconds"
_SOURCE_PROFILE = "source_profile"
_CREDENTIAL_SOURCE = "credential_source"
_WEB_IDENTITY_TOKEN_FILE = "web_identity_token_file"
_REGION = "region"

_CREDENTIAL_SOURCE_SLOTS = {
Expand Down Expand Up @@ -64,11 +65,22 @@ class AssumeRolePlan:
source: StaticSource | CredentialSource | AssumeRolePlan


def has_role_arn(config_file: MergedConfig | None, profile_name: str | None) -> bool:
def is_assume_role_profile(
config_file: MergedConfig | None, profile_name: str | None
) -> bool:
"""Return whether the profile is configured for AssumeRole.

A profile with a role ARN and a web identity token file, but no source, is
configured for web identity instead.
"""
if profile_name is None or config_file is None:
return False
if config_file.get(profile_name, _ROLE_ARN) is None:
return False
return (
profile_name is not None
and config_file is not None
and config_file.get(profile_name, _ROLE_ARN) is not None
config_file.get(profile_name, _SOURCE_PROFILE) is not None
or config_file.get(profile_name, _CREDENTIAL_SOURCE) is not None
or not config_file.get(profile_name, _WEB_IDENTITY_TOKEN_FILE)
)


Expand Down Expand Up @@ -226,7 +238,7 @@ def _plan_source_profile(
"complete static credentials."
)

if config_file.get(source_profile_name, _ROLE_ARN) is not None:
if is_assume_role_profile(config_file, source_profile_name):
return _plan_assume_role(
config_file, source_profile_name, (*visited, source_profile_name)
)
Expand Down
Loading
Loading