-
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
Open
arandito
wants to merge
4
commits into
aws:develop
Choose a base branch
from
arandito:network-credential-providers
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
.../.changes/next-release/aws-credentials-http-feature-cb78a08a2d6149668222fc4ee762070a.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # aws-credentials-http | ||
|
|
||
| This package provides a container HTTP credential resolver and chain provider. | ||
| Installing it automatically adds the `ECS_CONTAINER` source to the SDK's modular | ||
| AWS credential chain. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] |
11 changes: 11 additions & 0 deletions
11
packages/aws-credentials-http/src/aws_credentials_http/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ) |
102 changes: 102 additions & 0 deletions
102
packages/aws-credentials-http/src/aws_credentials_http/client.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| 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 | ||
41 changes: 41 additions & 0 deletions
41
packages/aws-credentials-http/src/aws_credentials_http/providers.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| ) |
1 change: 1 addition & 0 deletions
1
packages/aws-credentials-http/src/aws_credentials_http/py.typed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
138 changes: 138 additions & 0 deletions
138
packages/aws-credentials-http/src/aws_credentials_http/resolvers.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| import asyncio | ||
| import os | ||
| from datetime import UTC, datetime | ||
| from urllib.parse import urlparse | ||
|
|
||
| from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties | ||
| from smithy_core import URI | ||
| from smithy_core.aio.interfaces.identity import IdentityResolver | ||
| from smithy_core.exceptions import SmithyIdentityError | ||
| from smithy_http import Field, Fields | ||
| from smithy_http.aio.aiohttp import AIOHTTPClient | ||
| from smithy_http.aio.interfaces import HTTPClient | ||
|
|
||
| from .client import HttpCredentialsClient | ||
|
|
||
| _CONTAINER_METADATA_IP = "169.254.170.2" | ||
| _DEFAULT_TIMEOUT = 2 | ||
| _DEFAULT_RETRIES = 3 | ||
|
|
||
|
|
||
| class ContainerCredentialsResolver( | ||
| IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | ||
| ): | ||
| """Resolves AWS credentials from container HTTP endpoints.""" | ||
|
|
||
| ENV_VAR = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" | ||
| ENV_VAR_FULL = "AWS_CONTAINER_CREDENTIALS_FULL_URI" | ||
| ENV_VAR_AUTH_TOKEN = "AWS_CONTAINER_AUTHORIZATION_TOKEN" # noqa: S105 | ||
| ENV_VAR_AUTH_TOKEN_FILE = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE" # noqa: S105 | ||
|
|
||
| def __init__( | ||
| self, | ||
| http_client: HTTPClient | None = None, | ||
| *, | ||
| timeout: int = _DEFAULT_TIMEOUT, | ||
| retries: int = _DEFAULT_RETRIES, | ||
| ): | ||
| self._http_client = http_client or AIOHTTPClient() | ||
| self._client = HttpCredentialsClient( | ||
| self._http_client, timeout=timeout, retries=retries | ||
| ) | ||
| self._credentials = None | ||
|
|
||
| async def get_identity( | ||
| self, *, properties: AWSIdentityProperties | ||
| ) -> AWSCredentialsIdentity: | ||
| """Return cached credentials if valid, otherwise fetch from container endpoint.""" | ||
| if ( | ||
| self._credentials is not None | ||
| and self._credentials.expiration | ||
| and datetime.now(UTC) < self._credentials.expiration | ||
| ): | ||
| return self._credentials | ||
|
|
||
| uri = await self._resolve_uri_from_env() | ||
| fields = await self._resolve_fields_from_env() | ||
| creds = await self._client.get_credentials(uri, fields) | ||
|
|
||
| access_key_id = creds.get("AccessKeyId") | ||
| secret_access_key = creds.get("SecretAccessKey") | ||
| session_token = creds.get("Token") | ||
| expiration = creds.get("Expiration") | ||
| account_id = creds.get("AccountId") | ||
|
|
||
| if isinstance(expiration, str): | ||
| expiration = datetime.fromisoformat(expiration).replace(tzinfo=UTC) | ||
|
|
||
| if access_key_id is None or secret_access_key is None: | ||
| raise SmithyIdentityError( | ||
| "AccessKeyId and SecretAccessKey are required for container credentials" | ||
| ) | ||
|
|
||
| self._credentials = AWSCredentialsIdentity( | ||
| access_key_id=access_key_id, | ||
| secret_access_key=secret_access_key, | ||
| session_token=session_token, | ||
| expiration=expiration, | ||
| account_id=account_id, | ||
| ) | ||
| return self._credentials | ||
|
|
||
| async def invalidate(self) -> None: | ||
| """No-op until the rejected identity is passed in to expire selectively.""" | ||
|
|
||
| async def _resolve_uri_from_env(self) -> URI: | ||
| if self.ENV_VAR in os.environ: | ||
| return URI( | ||
| scheme="http", | ||
| host=_CONTAINER_METADATA_IP, | ||
| path=os.environ[self.ENV_VAR], | ||
| ) | ||
| elif self.ENV_VAR_FULL in os.environ: | ||
| parsed = urlparse(os.environ[self.ENV_VAR_FULL]) | ||
| return URI( | ||
| scheme=parsed.scheme, | ||
| host=parsed.hostname or "", | ||
| port=parsed.port, | ||
| path=parsed.path, | ||
| ) | ||
| else: | ||
| raise SmithyIdentityError( | ||
| f"Neither {self.ENV_VAR} or {self.ENV_VAR_FULL} environment " | ||
| "variables are set. Unable to resolve credentials." | ||
| ) | ||
|
|
||
| async def _resolve_fields_from_env(self) -> Fields: | ||
| fields = Fields() | ||
| if self.ENV_VAR_AUTH_TOKEN_FILE in os.environ: | ||
| try: | ||
| filename = os.environ[self.ENV_VAR_AUTH_TOKEN_FILE] | ||
| auth_token = await asyncio.to_thread(self._read_file, filename) | ||
| except (FileNotFoundError, PermissionError) as error: | ||
| raise SmithyIdentityError( | ||
| f"Unable to open {os.environ[self.ENV_VAR_AUTH_TOKEN_FILE]}." | ||
| ) from error | ||
|
|
||
| fields.set_field(self._build_auth_field(auth_token)) | ||
| elif self.ENV_VAR_AUTH_TOKEN in os.environ: | ||
| auth_token = os.environ[self.ENV_VAR_AUTH_TOKEN] | ||
| fields.set_field(self._build_auth_field(auth_token)) | ||
|
|
||
| return fields | ||
|
|
||
| def _build_auth_field(self, auth_token: str) -> Field: | ||
| if "\r" in auth_token or "\n" in auth_token: | ||
| raise SmithyIdentityError("Auth token value is not a legal header value.") | ||
| return Field(name="Authorization", values=[auth_token]) | ||
|
|
||
| def _read_file(self, filename: str) -> str: | ||
| with open(filename) as token_file: | ||
| try: | ||
| return token_file.read().strip() | ||
| except UnicodeDecodeError as error: | ||
| raise SmithyIdentityError( | ||
| f"Unable to read valid utf-8 bytes from {filename}." | ||
| ) from error |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self._timeoutseems never used. Should it be applied to the request, or should the parameter be dropped? Or do you want to to add a TODO?There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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
HTTPRequestConfigurationonly supportsread_timeoutfor now which may be why it wasn't used.I applied the value as the
read_timeoutin the request configuration and added a TODO to apply the value toconnect_timeoutonce its supported.