From 167fc0cb76c29bb3f42f168fa3803afdb8673d86 Mon Sep 17 00:00:00 2001 From: Alessandra Romero Date: Wed, 22 Jan 2025 14:18:52 -0500 Subject: [PATCH 1/5] Add account ID to SharedCredentialProvider --- botocore/credentials.py | 11 ++++++++++- botocore/session.py | 18 ++++++++++++++++-- tests/integration/test_credentials.py | 10 ++++++++-- tests/unit/test_credentials.py | 22 ++++++++++++++++++++++ tests/unit/test_session.py | 20 ++++++++++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/botocore/credentials.py b/botocore/credentials.py index 00b9f057fb..f566843f3e 100644 --- a/botocore/credentials.py +++ b/botocore/credentials.py @@ -1293,6 +1293,7 @@ class SharedCredentialProvider(CredentialProvider): ACCESS_KEY = 'aws_access_key_id' SECRET_KEY = 'aws_secret_access_key' + ACCOUNT_ID = 'aws_account_id' # Same deal as the EnvProvider above. Botocore originally supported # aws_security_token, but the SDKs are standardizing on aws_session_token # so we support both. @@ -1323,8 +1324,13 @@ def load(self): config, self.ACCESS_KEY, self.SECRET_KEY ) token = self._get_session_token(config) + account_id = self._get_account_id(config) return Credentials( - access_key, secret_key, token, method=self.METHOD + access_key, + secret_key, + token, + method=self.METHOD, + account_id=account_id, ) def _get_session_token(self, config): @@ -1332,6 +1338,9 @@ def _get_session_token(self, config): if token_envvar in config: return config[token_envvar] + def _get_account_id(self, config): + return config.get(self.ACCOUNT_ID) + class ConfigProvider(CredentialProvider): """INI based config provider with profile sections.""" diff --git a/botocore/session.py b/botocore/session.py index 93d020757a..d078a550f6 100644 --- a/botocore/session.py +++ b/botocore/session.py @@ -479,7 +479,9 @@ def set_default_client_config(self, client_config): """ self._client_config = client_config - def set_credentials(self, access_key, secret_key, token=None): + def set_credentials( + self, access_key, secret_key, token=None, account_id=None + ): """ Manually create credentials for this session. If you would prefer to use botocore without a config file, environment variables, @@ -495,9 +497,12 @@ def set_credentials(self, access_key, secret_key, token=None): :type token: str :param token: An option session token used by STS session credentials. + + :type account_id: str + :param account_id: An optional account ID part of the credentials. """ self._credentials = botocore.credentials.Credentials( - access_key, secret_key, token + access_key, secret_key, token, account_id=account_id ) def get_credentials(self): @@ -840,6 +845,7 @@ def create_client( aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, + aws_account_id=None, config=None, ): """Create a botocore client. @@ -898,6 +904,13 @@ def create_client( :param aws_session_token: The session token to use when creating the client. Same semantics as aws_access_key_id above. + :type aws_account_id: string + :param aws_account_id: The account id to use when creating + the client. This is entirely optional, and if not provided, + the credentials configured for the session will automatically + be used. You only need to provide this argument if you want + to override the credentials used for this specific client. + :type config: botocore.client.Config :param config: Advanced client configuration options. If a value is specified in the client config, its value will take precedence @@ -945,6 +958,7 @@ def create_client( access_key=aws_access_key_id, secret_key=aws_secret_access_key, token=aws_session_token, + account_id=aws_account_id, ) elif self._missing_cred_vars(aws_access_key_id, aws_secret_access_key): raise PartialCredentialsError( diff --git a/tests/integration/test_credentials.py b/tests/integration/test_credentials.py index d49c266e2e..87da445eac 100644 --- a/tests/integration/test_credentials.py +++ b/tests/integration/test_credentials.py @@ -72,7 +72,10 @@ def test_access_secret_vs_profile_code(self, credentials_cls): ) credentials_cls.assert_called_with( - access_key='code', secret_key='code-secret', token=mock.ANY + access_key='code', + secret_key='code-secret', + token=mock.ANY, + account_id=mock.ANY, ) def test_profile_env_vs_code(self): @@ -97,7 +100,10 @@ def test_access_secret_env_vs_code(self, credentials_cls): ) credentials_cls.assert_called_with( - access_key='code', secret_key='code-secret', token=mock.ANY + access_key='code', + secret_key='code-secret', + token=mock.ANY, + account_id=mock.ANY, ) def test_access_secret_env_vs_profile_code(self): diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py index 28608eb5cf..b93d9542c6 100644 --- a/tests/unit/test_credentials.py +++ b/tests/unit/test_credentials.py @@ -1428,6 +1428,28 @@ def test_credentials_file_does_not_exist_returns_none(self): creds = provider.load() self.assertIsNone(creds) + def test_credentials_file_exists_with_account_id(self): + self.ini_parser.return_value = { + 'default': { + 'aws_access_key_id': 'foo', + 'aws_secret_access_key': 'bar', + 'aws_session_token': 'baz', + 'aws_account_id': 'bin', + } + } + provider = credentials.SharedCredentialProvider( + creds_filename='~/.aws/creds', + profile_name='default', + ini_parser=self.ini_parser, + ) + creds = provider.load() + self.assertIsNotNone(creds) + self.assertEqual(creds.access_key, 'foo') + self.assertEqual(creds.secret_key, 'bar') + self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.method, 'shared-credentials-file') + self.assertEqual(creds.account_id, 'bin') + class TestConfigFileProvider(BaseEnvVar): def setUp(self): diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index d3eabbdf08..6d5ca2924c 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -781,6 +781,26 @@ def test_param_api_version_overrides_config_value(self, client_creator): ] self.assertEqual(call_kwargs['api_version'], override_api_version) + @mock.patch('botocore.client.ClientCreator') + def test_create_client_with_credentials(self, client_creator): + self.session.create_client( + 'sts', + 'us-west-2', + aws_access_key_id='foo', + aws_secret_access_key='bar', + aws_session_token='baz', + aws_account_id='bin', + ) + credentials = ( + client_creator.return_value.create_client.call_args.kwargs[ + 'credentials' + ] + ) + self.assertEqual(credentials.access_key, 'foo') + self.assertEqual(credentials.secret_key, 'bar') + self.assertEqual(credentials.token, 'baz') + self.assertEqual(credentials.account_id, 'bin') + class TestSessionComponent(BaseSessionTest): def test_internal_component(self): From 1f72e6bba2f344328f5d60728e0e1456edf81950 Mon Sep 17 00:00:00 2001 From: Alessandra Romero Date: Thu, 23 Jan 2025 10:28:37 -0500 Subject: [PATCH 2/5] Add account ID to ConfigProvider --- botocore/credentials.py | 13 +++++++++++-- tests/unit/test_credentials.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/botocore/credentials.py b/botocore/credentials.py index f566843f3e..0648261c37 100644 --- a/botocore/credentials.py +++ b/botocore/credentials.py @@ -1293,11 +1293,11 @@ class SharedCredentialProvider(CredentialProvider): ACCESS_KEY = 'aws_access_key_id' SECRET_KEY = 'aws_secret_access_key' - ACCOUNT_ID = 'aws_account_id' # Same deal as the EnvProvider above. Botocore originally supported # aws_security_token, but the SDKs are standardizing on aws_session_token # so we support both. TOKENS = ['aws_security_token', 'aws_session_token'] + ACCOUNT_ID = 'aws_account_id' def __init__(self, creds_filename, profile_name=None, ini_parser=None): self._creds_filename = creds_filename @@ -1354,6 +1354,7 @@ class ConfigProvider(CredentialProvider): # aws_security_token, but the SDKs are standardizing on aws_session_token # so we support both. TOKENS = ['aws_security_token', 'aws_session_token'] + ACCOUNT_ID = 'aws_account_id' def __init__(self, config_filename, profile_name, config_parser=None): """ @@ -1390,8 +1391,13 @@ def load(self): profile_config, self.ACCESS_KEY, self.SECRET_KEY ) token = self._get_session_token(profile_config) + account_id = self._get_account_id(profile_config) return Credentials( - access_key, secret_key, token, method=self.METHOD + access_key, + secret_key, + token, + method=self.METHOD, + account_id=account_id, ) else: return None @@ -1401,6 +1407,9 @@ def _get_session_token(self, profile_config): if token_name in profile_config: return profile_config[token_name] + def _get_account_id(self, config): + return config.get(self.ACCOUNT_ID) + class BotoProvider(CredentialProvider): METHOD = 'boto-config' diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py index b93d9542c6..c6256c0fa7 100644 --- a/tests/unit/test_credentials.py +++ b/tests/unit/test_credentials.py @@ -1512,6 +1512,25 @@ def test_partial_creds_is_error(self): with self.assertRaises(botocore.exceptions.PartialCredentialsError): provider.load() + def test_config_file_with_account_id(self): + profile_config = { + 'aws_access_key_id': 'foo', + 'aws_secret_access_key': 'bar', + 'aws_session_token': 'baz', + 'aws_account_id': 'bin', + } + parsed = {'profiles': {'default': profile_config}} + parser = mock.Mock() + parser.return_value = parsed + provider = credentials.ConfigProvider('cli.cfg', 'default', parser) + creds = provider.load() + self.assertIsNotNone(creds) + self.assertEqual(creds.access_key, 'foo') + self.assertEqual(creds.secret_key, 'bar') + self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.method, 'config-file') + self.assertEqual(creds.account_id, 'bin') + class TestBotoProvider(BaseEnvVar): def setUp(self): From 1235cd3a76e4505f450a28e31304052821dac08d Mon Sep 17 00:00:00 2001 From: Alessandra Romero Date: Fri, 24 Jan 2025 10:09:56 -0500 Subject: [PATCH 3/5] update param order in create_client() --- botocore/session.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/botocore/session.py b/botocore/session.py index d078a550f6..ab4d8661cc 100644 --- a/botocore/session.py +++ b/botocore/session.py @@ -845,8 +845,8 @@ def create_client( aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, - aws_account_id=None, config=None, + aws_account_id=None, ): """Create a botocore client. @@ -904,13 +904,6 @@ def create_client( :param aws_session_token: The session token to use when creating the client. Same semantics as aws_access_key_id above. - :type aws_account_id: string - :param aws_account_id: The account id to use when creating - the client. This is entirely optional, and if not provided, - the credentials configured for the session will automatically - be used. You only need to provide this argument if you want - to override the credentials used for this specific client. - :type config: botocore.client.Config :param config: Advanced client configuration options. If a value is specified in the client config, its value will take precedence @@ -920,6 +913,13 @@ def create_client( the client will be the result of calling ``merge()`` on the default config with the config provided to this call. + :type aws_account_id: string + :param aws_account_id: The account id to use when creating + the client. This is entirely optional, and if not provided, + the credentials configured for the session will automatically + be used. You only need to provide this argument if you want + to override the credentials used for this specific client. + :rtype: botocore.client.BaseClient :return: A botocore client instance From ac4bd55f123803a9811ada84666c7cbc1e570ee7 Mon Sep 17 00:00:00 2001 From: Alessandra Romero Date: Thu, 6 Feb 2025 12:56:23 -0500 Subject: [PATCH 4/5] add debug log for set credentials that get ignored --- botocore/session.py | 20 ++++++++++++++++---- tests/unit/test_session.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/botocore/session.py b/botocore/session.py index ab4d8661cc..71a7d0af4d 100644 --- a/botocore/session.py +++ b/botocore/session.py @@ -915,10 +915,7 @@ def create_client( :type aws_account_id: string :param aws_account_id: The account id to use when creating - the client. This is entirely optional, and if not provided, - the credentials configured for the session will automatically - be used. You only need to provide this argument if you want - to override the credentials used for this specific client. + the client. Same semantics as aws_access_key_id above. :rtype: botocore.client.BaseClient :return: A botocore client instance @@ -968,6 +965,13 @@ def create_client( ), ) else: + if ignored_credentials := self._get_ignored_credentials( + aws_session_token, aws_account_id + ): + logger.debug( + f"Ignoring the following credential-related values which were set without " + f"an access key id and secret key on the client: {ignored_credentials}" + ) credentials = self.get_credentials() auth_token = self.get_auth_token() endpoint_resolver = self._get_internal_component('endpoint_resolver') @@ -1140,6 +1144,14 @@ def get_available_regions( pass return results + def _get_ignored_credentials(self, aws_session_token, aws_account_id): + credential_inputs = [] + if aws_session_token: + credential_inputs.append('aws_session_token') + if aws_account_id: + credential_inputs.append('aws_account_id') + return ', '.join(credential_inputs) if credential_inputs else None + class ComponentLocator: """Service locator for session components.""" diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 6d5ca2924c..c1603519de 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -801,6 +801,26 @@ def test_create_client_with_credentials(self, client_creator): self.assertEqual(credentials.token, 'baz') self.assertEqual(credentials.account_id, 'bin') + @mock.patch('botocore.client.ClientCreator') + def test_create_client_with_ignored_credentials(self, client_creator): + with self.assertLogs('botocore.session', level='DEBUG') as log: + self.session.create_client( + 'sts', + 'us-west-2', + aws_account_id='foo', + ) + credentials = ( + client_creator.return_value.create_client.call_args.kwargs[ + 'credentials' + ] + ) + self.assertIn( + 'Ignoring the following credential-related values', + log.output[0], + ) + self.assertIn('aws_account_id', log.output[0]) + self.assertEqual(credentials.account_id, None) + class TestSessionComponent(BaseSessionTest): def test_internal_component(self): From 34a0020ff88e6e4acbe4f35270c42997de114ef5 Mon Sep 17 00:00:00 2001 From: Alessandra Romero <24320222+alexgromero@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:59:13 -0500 Subject: [PATCH 5/5] Update botocore/session.py Co-authored-by: SamRemis --- botocore/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/botocore/session.py b/botocore/session.py index 71a7d0af4d..250db2d2ca 100644 --- a/botocore/session.py +++ b/botocore/session.py @@ -970,7 +970,7 @@ def create_client( ): logger.debug( f"Ignoring the following credential-related values which were set without " - f"an access key id and secret key on the client: {ignored_credentials}" + f"an access key id and secret key on the session or client: {ignored_credentials}" ) credentials = self.get_credentials() auth_token = self.get_auth_token()