-
Notifications
You must be signed in to change notification settings - Fork 18
Add sts, imds, and http credential provider packages #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5fd775d
d8f06b7
6a89682
a93d349
6988cd5
c1dfead
ae1f0b3
e0b561f
4b417dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "type": "feature", | ||
| "description": "Add container HTTP credentials resolver and `EcsContainer` chain provider." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # aws-credentials-http | ||
|
|
||
| This package provides a container HTTP credential resolver and chain provider. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```shell | ||
| uv pip install aws-credentials-http | ||
| ``` | ||
|
|
||
| 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 this source when the container credential environment variables | ||
| (`AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` or `AWS_CONTAINER_CREDENTIALS_FULL_URI`) | ||
| are set, unless a higher-precedence source resolves credentials first. | ||
|
|
||
| ## Client Configuration | ||
|
|
||
| To use this resolver explicitly, set the `aws_credentials_identity_resolver` | ||
| property on a service client's config to a `ContainerCredentialsResolver` | ||
| instance: | ||
|
|
||
| ```python | ||
| from aws_credentials_http import ContainerCredentialsResolver | ||
|
|
||
| service_client = ServiceClient( | ||
| config=ServiceClientConfig( | ||
| aws_credentials_identity_resolver=ContainerCredentialsResolver(), | ||
| ) | ||
| ) | ||
| ``` | ||
|
|
||
| ## Standalone | ||
|
|
||
| The resolver can also be used on its own to fetch credentials directly: | ||
|
|
||
| ```python | ||
| import asyncio | ||
|
|
||
| from aws_credentials_http import ContainerCredentialsResolver | ||
|
|
||
| async def main() -> None: | ||
| resolver = ContainerCredentialsResolver() | ||
| identity = await resolver.get_identity(properties={}) | ||
|
|
||
| asyncio.run(main()) | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| [project] | ||
| name = "aws-credentials-http" | ||
| dynamic = ["version"] | ||
| requires-python = ">=3.12" | ||
| authors = [ | ||
| {name = "Amazon Web Services"}, | ||
| ] | ||
| description = "HTTP endpoint credentials support for the AWS SDK for Python." | ||
| readme = "README.md" | ||
| license = {text = "Apache License 2.0"} | ||
| keywords = ["aws", "credentials", "http", "ecs", "eks", "sdk", "smithy"] | ||
| classifiers = [ | ||
| "Development Status :: 2 - Pre-Alpha", | ||
| "Intended Audience :: Developers", | ||
| "Intended Audience :: System Administrators", | ||
| "Natural Language :: English", | ||
| "License :: OSI Approved :: Apache Software License", | ||
| "Operating System :: OS Independent", | ||
| "Programming Language :: Python", | ||
| "Programming Language :: Python :: 3 :: Only", | ||
| "Programming Language :: Python :: 3", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Programming Language :: Python :: 3.13", | ||
| "Programming Language :: Python :: 3.14", | ||
| "Programming Language :: Python :: Implementation :: CPython", | ||
| "Programming Language :: Python :: Free Threading :: 2 - Beta", | ||
| "Topic :: Software Development :: Libraries", | ||
| ] | ||
| dependencies = [ | ||
| "smithy-aws-core~=0.8.0", | ||
| "smithy-core~=0.7.0", | ||
| "smithy-http[aiohttp]~=0.4.0", | ||
| ] | ||
|
|
||
| [project.urls] | ||
| "Code" = "https://github.com/aws/aws-sdk-python/tree/develop/packages/aws-credentials-http/" | ||
| "Issue tracker" = "https://github.com/aws/aws-sdk-python/issues" | ||
|
|
||
| [project.entry-points."smithy_aws_core.identity.chain_providers"] | ||
| EcsContainer = "aws_credentials_http.providers:EcsContainerProvider" | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.version] | ||
| path = "src/aws_credentials_http/__init__.py" | ||
|
|
||
| [tool.hatch.build] | ||
| exclude = [ | ||
| "tests", | ||
| ] | ||
|
|
||
| [tool.ruff] | ||
| src = ["src"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| __version__ = "0.0.0" | ||
|
|
||
| from .providers import EcsContainerProvider | ||
| from .resolvers import ContainerCredentialsResolver | ||
|
|
||
| __all__ = ( | ||
| "ContainerCredentialsResolver", | ||
| "EcsContainerProvider", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| import asyncio | ||
| import ipaddress | ||
| import json | ||
|
|
||
| from smithy_core import URI | ||
| from smithy_core.exceptions import SmithyIdentityError | ||
| from smithy_http import Field, Fields | ||
| from smithy_http.aio import HTTPRequest | ||
| from smithy_http.aio.interfaces import HTTPClient, HTTPResponse | ||
| from smithy_http.interfaces import HTTPRequestConfiguration | ||
|
|
||
| _CONTAINER_METADATA_IP = "169.254.170.2" | ||
| _CONTAINER_METADATA_ALLOWED_HOSTS = { | ||
| _CONTAINER_METADATA_IP, | ||
| "169.254.170.23", | ||
| "fd00:ec2::23", | ||
| "localhost", | ||
| } | ||
| _DEFAULT_TIMEOUT = 2 | ||
| _DEFAULT_RETRIES = 3 | ||
| _SLEEP_SECONDS = 1 | ||
|
|
||
|
|
||
| class HttpCredentialsClient: | ||
| """Retrieves AWS credentials from an HTTP credentials endpoint.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| http_client: HTTPClient, | ||
| *, | ||
| timeout: int = _DEFAULT_TIMEOUT, | ||
| retries: int = _DEFAULT_RETRIES, | ||
| ): | ||
| self._http_client = http_client | ||
| # TODO: Also apply this value as the connect timeout once smithy_http's | ||
| # HTTPRequestConfiguration supports it. | ||
| self._timeout = timeout | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why this wasn't applied to the request in the first place. I confirmed we should apply the default timeout by looking at botocore. However, we need to apply the value to both the read timeout and connect timeout. Looks like I applied the value as the |
||
| self._retries = retries | ||
|
|
||
| async def get_credentials(self, uri: URI, fields: Fields) -> dict[str, str]: | ||
| self._validate_allowed_url(uri) | ||
| fields.set_field(Field(name="Accept", values=["application/json"])) | ||
|
|
||
| attempts = 0 | ||
| last_exc = None | ||
| while attempts < self._retries: | ||
| try: | ||
| request = HTTPRequest( | ||
| method="GET", | ||
| destination=uri, | ||
| fields=fields, | ||
| ) | ||
| response: HTTPResponse = await self._http_client.send( | ||
| request, | ||
| request_config=HTTPRequestConfiguration(read_timeout=self._timeout), | ||
| ) | ||
| body = await response.consume_body_async() | ||
| if response.status != 200: | ||
| raise SmithyIdentityError( | ||
| f"Container metadata service returned {response.status}: " | ||
| f"{body.decode('utf-8')}" | ||
| ) | ||
| try: | ||
| return json.loads(body.decode("utf-8")) | ||
| except Exception as error: | ||
| raise SmithyIdentityError( | ||
| "Unable to parse JSON from container metadata: " | ||
| f"{body.decode('utf-8')}" | ||
| ) from error | ||
| except Exception as error: | ||
| last_exc = error | ||
| await asyncio.sleep(_SLEEP_SECONDS) | ||
| attempts += 1 | ||
|
|
||
| raise SmithyIdentityError( | ||
| f"Failed to retrieve container metadata after {self._retries} attempt(s)" | ||
| ) from last_exc | ||
|
|
||
| def _validate_allowed_url(self, uri: URI) -> None: | ||
|
alexgromero marked this conversation as resolved.
|
||
| if uri.scheme == "https": | ||
| return | ||
|
|
||
| if self._is_loopback(uri.host): | ||
| return | ||
|
|
||
| if not self._is_allowed_container_metadata_host(uri.host): | ||
| raise SmithyIdentityError( | ||
| f"Unsupported host '{uri.host}'. " | ||
| f"Can only retrieve metadata from an HTTPS endpoint, a loopback " | ||
| f"address, or one of: {', '.join(_CONTAINER_METADATA_ALLOWED_HOSTS)}" | ||
| ) | ||
|
|
||
| def _is_loopback(self, hostname: str) -> bool: | ||
| try: | ||
| return ipaddress.ip_address(hostname).is_loopback | ||
| except ValueError: | ||
| return False | ||
|
|
||
| def _is_allowed_container_metadata_host(self, hostname: str) -> bool: | ||
| return hostname in _CONTAINER_METADATA_ALLOWED_HOSTS | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| import os | ||
|
|
||
| from smithy_aws_core.identity import AWSCredentialsIdentity | ||
| from smithy_aws_core.identity.chain import Standard, StandardProvider | ||
| from smithy_aws_core.identity.chain.provider import ChainSetup | ||
| from smithy_core.interfaces.identity import Identity | ||
|
|
||
| from .resolvers import ContainerCredentialsResolver | ||
|
|
||
| _RELATIVE_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" | ||
| _FULL_URI = "AWS_CONTAINER_CREDENTIALS_FULL_URI" | ||
|
|
||
|
|
||
| class EcsContainerProvider: | ||
| """Adds a container credential resolver to the credential chain.""" | ||
|
|
||
| @property | ||
| def name(self) -> str: | ||
| """Return the canonical provider name.""" | ||
| return StandardProvider.ECS_CONTAINER.canonical_name | ||
|
|
||
| @property | ||
| def ordering(self) -> Standard: | ||
| """Return the provider's standard chain position.""" | ||
| return Standard(slot=StandardProvider.ECS_CONTAINER) | ||
|
|
||
| async def setup( | ||
| self, | ||
| identity_type: type[Identity], | ||
| setup: ChainSetup, | ||
| ) -> None: | ||
| """Add a terminal resolver when a container endpoint is configured.""" | ||
| if identity_type is not AWSCredentialsIdentity: | ||
| return | ||
| if not os.getenv(_RELATIVE_URI) and not os.getenv(_FULL_URI): | ||
| return | ||
| setup.add_terminal_resolver( | ||
| ContainerCredentialsResolver(http_client=setup.http_client) | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.