diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py index 0436492e1..4ee48e70d 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py @@ -175,18 +175,18 @@ def identity_type(self) -> type[I] | None: async def create[ChainIdentity: Identity]( identity_type: type[ChainIdentity], *, - profile_file: MergedConfig | None = None, - profile_name_override: str | None = None, + config_file: MergedConfig | None = None, + profile_name: str | None = None, region_override: str | None = None, http_client: HTTPClient | None = None, ) -> "IdentityChain[ChainIdentity]": """Create an identity chain from discovered providers. :param identity_type: The identity type to resolve. - :param profile_file: Parsed config/credentials file. Loaded from disk + :param config_file: Parsed config/credentials file. Loaded from disk when not set. - :param profile_name_override: Profile name to use, taking precedence over - ``AWS_PROFILE``. + :param profile_name: Profile name to use. If omitted, the shared config + provider uses ``AWS_PROFILE`` when set, otherwise ``default``. :param region_override: Region to use for providers whose resolvers fetch credentials through a service call. :param http_client: HTTP client to use for providers whose resolvers make @@ -196,8 +196,8 @@ async def create[ChainIdentity: Identity]( _validate_providers(discovered_providers) providers = _sort_by_ordering(discovered_providers) setup = ChainSetup( - profile_file=profile_file, - profile_name_override=profile_name_override, + config_file=config_file, + profile_name=profile_name, region_override=region_override, http_client=http_client, ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/ordering.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/ordering.py index ecd9d34cd..4fa5cbf2e 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/ordering.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/ordering.py @@ -20,7 +20,7 @@ class StandardProvider(Enum): PROFILE_SSO_SESSION = "ProfileSsoSession", "aws-credentials-sso" PROFILE_LOGIN = "Login", "aws-credentials-login" PROFILE_CREDENTIAL_PROCESS = "ProfileCredentialProcess", None - ECS_CONTAINER = "EcsContainer", "aws-credentials-ecs" + ECS_CONTAINER = "EcsContainer", "aws-credentials-http" EC2_INSTANCE_METADATA = "Ec2InstanceMetadata", "aws-credentials-imds" def __init__( diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py index 90f8b8cd2..31a95a515 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py @@ -10,7 +10,6 @@ from smithy_core.interfaces.identity import Identity from smithy_http.aio.interfaces import HTTPClient -from ...config.file_parser import Section from ...config.merged_config import MergedConfig from .ordering import OrderingConstraint @@ -37,17 +36,16 @@ class ChainSetup: def __init__( self, *, - profile_file: MergedConfig | None = None, - profile_name_override: str | None = None, + config_file: MergedConfig | None = None, + profile_name: str | None = None, region_override: str | None = None, http_client: HTTPClient | None = None, properties: MutableMapping[str, Any] | None = None, ) -> None: - self._profile_file = profile_file - self._profile_name_override = profile_name_override + self._config_file = config_file + self._profile_name = profile_name self._region_override = region_override self._http_client = http_client - self._profile: Section | None = None self._properties: MutableMapping[str, Any] = ( {} if properties is None else properties ) @@ -56,19 +54,14 @@ def __init__( self._terminal = False @property - def profile_file(self) -> MergedConfig | None: - """Return the parsed config and credentials files, if loaded.""" - return self._profile_file + def config_file(self) -> MergedConfig | None: + """Return the parsed config/credentials file, if loaded.""" + return self._config_file @property - def profile(self) -> Section | None: - """Return the active profile, if selected.""" - return self._profile - - @property - def profile_name_override(self) -> str | None: - """Return the client-specified profile name, if provided.""" - return self._profile_name_override + def profile_name(self) -> str | None: + """Return the profile name, if selected.""" + return self._profile_name @property def region_override(self) -> str | None: @@ -101,15 +94,15 @@ def set_current_provider(self, provider: ChainIdentityProvider) -> None: raise RuntimeError("Cannot change provider after a terminal resolver.") self._current_provider = provider - def set_profile_file(self, profile_file: MergedConfig) -> None: - """Set the parsed profile file without overwriting an existing value.""" - if self._profile_file is not None: - raise RuntimeError("Cannot overwrite a profile file already present.") - self._profile_file = profile_file + def set_config_file(self, config_file: MergedConfig) -> None: + """Set the parsed config file without overwriting an existing value.""" + if self._config_file is not None: + raise RuntimeError("Cannot overwrite a config file already present.") + self._config_file = config_file - def set_profile(self, profile: Section) -> None: - """Set the active profile.""" - self._profile = profile + def set_profile_name(self, profile_name: str) -> None: + """Set the resolved name of the active profile.""" + self._profile_name = profile_name def add_resolver(self, resolver: IdentityResolver[Any, Any]) -> None: """Add a named resolver and continue assembly.""" diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py index a3ccc133c..fca712ec1 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 from smithy_core.interfaces.identity import Identity -from ....config.file_parser import Section from ...components import AWSCredentialsIdentity from ...static import StaticCredentialsResolver from ..ordering import Standard, StandardProvider @@ -14,11 +13,6 @@ _ACCOUNT_ID = "aws_account_id" -def _get_string(profile: Section, key: str) -> str | None: - value = profile.properties.get(key) - return value if isinstance(value, str) else None - - class ProfileSessionCredentialsProvider: """Adds a resolver for session credentials from the active profile.""" @@ -37,13 +31,14 @@ async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: if identity_type is not AWSCredentialsIdentity: return - profile = setup.profile - if profile is None: + config_file = setup.config_file + profile_name = setup.profile_name + if config_file is None or profile_name is None: return - access_key_id = _get_string(profile, _ACCESS_KEY_ID) - secret_access_key = _get_string(profile, _SECRET_ACCESS_KEY) - session_token = _get_string(profile, _SESSION_TOKEN) + access_key_id = config_file.get(profile_name, _ACCESS_KEY_ID) + secret_access_key = config_file.get(profile_name, _SECRET_ACCESS_KEY) + session_token = config_file.get(profile_name, _SESSION_TOKEN) if access_key_id is None or secret_access_key is None or session_token is None: return @@ -51,7 +46,7 @@ async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: access_key_id=access_key_id, secret_access_key=secret_access_key, session_token=session_token, - account_id=_get_string(profile, _ACCOUNT_ID), + account_id=config_file.get(profile_name, _ACCOUNT_ID), ) setup.add_terminal_resolver(StaticCredentialsResolver(identity)) @@ -74,18 +69,19 @@ async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: if identity_type is not AWSCredentialsIdentity: return - profile = setup.profile - if profile is None: + config_file = setup.config_file + profile_name = setup.profile_name + if config_file is None or profile_name is None: return - access_key_id = _get_string(profile, _ACCESS_KEY_ID) - secret_access_key = _get_string(profile, _SECRET_ACCESS_KEY) + access_key_id = config_file.get(profile_name, _ACCESS_KEY_ID) + secret_access_key = config_file.get(profile_name, _SECRET_ACCESS_KEY) if access_key_id is None or secret_access_key is None: return identity = AWSCredentialsIdentity( access_key_id=access_key_id, secret_access_key=secret_access_key, - account_id=_get_string(profile, _ACCOUNT_ID), + account_id=config_file.get(profile_name, _ACCOUNT_ID), ) setup.add_terminal_resolver(StaticCredentialsResolver(identity)) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py index 64dbc211d..161cdb0dc 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py @@ -28,14 +28,10 @@ async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: if identity_type is not AWSCredentialsIdentity: return - profile_file = setup.profile_file - if profile_file is None: - profile_file = await load_config() - setup.set_profile_file(profile_file) - - profile_name = ( - setup.profile_name_override or os.getenv("AWS_PROFILE") or "default" - ) - profile = profile_file.get_profile(profile_name) - if profile is not None: - setup.set_profile(profile) + config_file = setup.config_file + if config_file is None: + config_file = await load_config() + setup.set_config_file(config_file) + + profile_name = setup.profile_name or os.getenv("AWS_PROFILE") or "default" + setup.set_profile_name(profile_name) diff --git a/packages/smithy-aws-core/tests/unit/identity/chain/providers/conftest.py b/packages/smithy-aws-core/tests/unit/identity/chain/providers/conftest.py index 5a9bae4c6..71882b5a0 100644 --- a/packages/smithy-aws-core/tests/unit/identity/chain/providers/conftest.py +++ b/packages/smithy-aws-core/tests/unit/identity/chain/providers/conftest.py @@ -17,8 +17,10 @@ class OtherIdentity(Identity): @pytest.fixture def merged_config() -> Callable[..., MergedConfig]: + """Build merged config for provider tests.""" + def _build( - profiles: Mapping[str, Mapping[str, str]] | None = None, + profiles: Mapping[str, Mapping[str, str | dict[str, str]]] | None = None, ) -> MergedConfig: sections = { name: Section(properties=dict(properties)) @@ -31,21 +33,20 @@ def _build( @pytest.fixture def setup_provider() -> Callable[..., Awaitable[ChainSetup]]: + """Setup a provider with a configured ChainSetup.""" + async def _setup( provider: Any, *, identity_type: type[Identity] = AWSCredentialsIdentity, - profile: Section | None = None, - profile_file: MergedConfig | None = None, - profile_name_override: str | None = None, + config_file: MergedConfig | None = None, + profile_name: str | None = None, ) -> ChainSetup: setup = ChainSetup( - profile_file=profile_file, - profile_name_override=profile_name_override, + config_file=config_file, + profile_name=profile_name, ) setup.set_current_provider(provider) - if profile is not None: - setup.set_profile(profile) await provider.setup(identity_type, setup) return setup diff --git a/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_profile.py b/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_profile.py index bf3c507f3..2e37c4749 100644 --- a/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_profile.py +++ b/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_profile.py @@ -4,7 +4,7 @@ from typing import Any import pytest -from smithy_aws_core.config.file_parser import Section +from smithy_aws_core.config.merged_config import MergedConfig from smithy_aws_core.identity import AWSCredentialsIdentity from smithy_aws_core.identity.chain.provider import ChainSetup from smithy_aws_core.identity.chain.providers.profile import ( @@ -43,6 +43,25 @@ async def test_requires_active_profile( assert not setup.terminal +@pytest.mark.parametrize( + "provider", + [ProfileSessionCredentialsProvider(), ProfileStaticCredentialsProvider()], +) +async def test_missing_profile_does_not_register( + provider: Any, + setup_provider: Callable[..., Awaitable[ChainSetup]], + merged_config: Callable[..., MergedConfig], +) -> None: + setup = await setup_provider( + provider, + config_file=merged_config({"default": {"aws_access_key_id": "akid"}}), + profile_name="missing", + ) + + assert setup.resolvers == () + assert not setup.terminal + + @pytest.mark.parametrize( "provider, profile, expected", [ @@ -81,8 +100,13 @@ async def test_registers_terminal_resolver_for_complete_profile( profile: dict[str, str | dict[str, str]], expected: AWSCredentialsIdentity, setup_provider: Callable[..., Awaitable[ChainSetup]], + merged_config: Callable[..., MergedConfig], ) -> None: - setup = await setup_provider(provider, profile=Section(properties=profile)) + setup = await setup_provider( + provider, + config_file=merged_config({"default": profile}), + profile_name="default", + ) assert setup.terminal assert len(setup.resolvers) == 1 @@ -111,8 +135,13 @@ async def test_rejects_incomplete_or_non_string_keys( provider: Any, properties: dict[str, Any], setup_provider: Callable[..., Awaitable[ChainSetup]], + merged_config: Callable[..., MergedConfig], ) -> None: - setup = await setup_provider(provider, profile=Section(properties=properties)) + setup = await setup_provider( + provider, + config_file=merged_config({"default": properties}), + profile_name="default", + ) assert setup.resolvers == () assert not setup.terminal diff --git a/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_shared_config.py b/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_shared_config.py index 1b85b22f9..88a150d46 100644 --- a/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_shared_config.py +++ b/packages/smithy-aws-core/tests/unit/identity/chain/providers/test_shared_config.py @@ -28,10 +28,10 @@ async def test_ignores_non_aws_identity_type( load_config.assert_not_awaited() -# profile selection precedence: profile_name_override wins over the AWS_PROFILE +# Profile selection precedence: profile_name wins over the AWS_PROFILE # env var (environment_profile), which wins over the "default" fallback. @pytest.mark.parametrize( - "profile_name_override, environment_profile, expected", + "profile_name, environment_profile, expected", [ ("override", "environment", "override"), (None, "environment", "environment"), @@ -39,20 +39,14 @@ async def test_ignores_non_aws_identity_type( ], ) async def test_selects_profile_without_reloading( - profile_name_override: str | None, + profile_name: str | None, environment_profile: str | None, expected: str, setup_provider: Callable[..., Awaitable[ChainSetup]], merged_config: Callable[..., MergedConfig], monkeypatch: pytest.MonkeyPatch, ) -> None: - profile_file = merged_config( - { - "override": {"name": "override"}, - "environment": {"name": "environment"}, - "default": {"name": "default"}, - } - ) + config_file = merged_config() if environment_profile is None: monkeypatch.delenv("AWS_PROFILE", raising=False) else: @@ -62,12 +56,12 @@ async def test_selects_profile_without_reloading( setup = await setup_provider( SharedConfigProvider(), - profile_file=profile_file, - profile_name_override=profile_name_override, + config_file=config_file, + profile_name=profile_name, ) - assert setup.profile_file is profile_file - assert setup.profile is profile_file.get_profile(expected) + assert setup.config_file is config_file + assert setup.profile_name == expected assert setup.resolvers == () load_config.assert_not_awaited() @@ -84,25 +78,7 @@ async def test_loads_when_not_preloaded( setup = await setup_provider(SharedConfigProvider()) - assert setup.profile_file is loaded - assert setup.profile is loaded.get_profile("default") + assert setup.config_file is loaded + assert setup.profile_name == "default" assert setup.resolvers == () load_config.assert_awaited_once_with() - - -async def test_leaves_missing_profile_unset( - setup_provider: Callable[..., Awaitable[ChainSetup]], - merged_config: Callable[..., MergedConfig], - monkeypatch: pytest.MonkeyPatch, -) -> None: - load_config = AsyncMock() - monkeypatch.setattr(shared_config_module, "load_config", load_config) - - setup = await setup_provider( - SharedConfigProvider(), - profile_file=merged_config(), - profile_name_override="missing", - ) - - assert setup.profile is None - load_config.assert_not_awaited() diff --git a/packages/smithy-aws-core/tests/unit/identity/chain/test_ordering.py b/packages/smithy-aws-core/tests/unit/identity/chain/test_ordering.py index f0542f83b..ffbd3a93e 100644 --- a/packages/smithy-aws-core/tests/unit/identity/chain/test_ordering.py +++ b/packages/smithy-aws-core/tests/unit/identity/chain/test_ordering.py @@ -121,7 +121,7 @@ def test_shared_config_detection( "ProfileCredentialProcess", None, ), - (StandardProvider.ECS_CONTAINER, "EcsContainer", "aws-credentials-ecs"), + (StandardProvider.ECS_CONTAINER, "EcsContainer", "aws-credentials-http"), ( StandardProvider.EC2_INSTANCE_METADATA, "Ec2InstanceMetadata", diff --git a/packages/smithy-aws-core/tests/unit/identity/chain/test_provider.py b/packages/smithy-aws-core/tests/unit/identity/chain/test_provider.py index bade5e675..63e2f62eb 100644 --- a/packages/smithy-aws-core/tests/unit/identity/chain/test_provider.py +++ b/packages/smithy-aws-core/tests/unit/identity/chain/test_provider.py @@ -81,13 +81,13 @@ def test_cannot_add_without_current_provider() -> None: setup.add_resolver(AsyncMock()) -def test_set_profile_file_cannot_overwrite() -> None: - setup = ChainSetup(profile_file=_empty_config()) +def test_set_config_file_cannot_overwrite() -> None: + setup = ChainSetup(config_file=_empty_config()) with pytest.raises( - RuntimeError, match="Cannot overwrite a profile file already present" + RuntimeError, match="Cannot overwrite a config file already present" ): - setup.set_profile_file(_empty_config()) + setup.set_config_file(_empty_config()) def test_properties_bag_is_shared_and_mutable() -> None: