From 2d05ce7e66b86d3128120a72871bf7373558d8a2 Mon Sep 17 00:00:00 2001 From: davidlm Date: Fri, 11 Aug 2023 17:52:07 -0400 Subject: [PATCH] add account id to credentials where possible --- botocore/credentials.py | 68 +++++++++++-- tests/__init__.py | 3 +- tests/unit/test_credentials.py | 177 ++++++++++++++++++++++++++++----- 3 files changed, 213 insertions(+), 35 deletions(-) diff --git a/botocore/credentials.py b/botocore/credentials.py index b16f308c44..e736fb8a56 100644 --- a/botocore/credentials.py +++ b/botocore/credentials.py @@ -49,13 +49,14 @@ InstanceMetadataFetcher, JSONFileCache, SSOTokenLoader, + ArnParser, parse_key_val_file, resolve_imds_endpoint_mode, ) logger = logging.getLogger(__name__) ReadOnlyCredentials = namedtuple( - 'ReadOnlyCredentials', ['access_key', 'secret_key', 'token'] + 'ReadOnlyCredentials', ['access_key', 'secret_key', 'token', 'account_id'] ) _DEFAULT_MANDATORY_REFRESH_TIMEOUT = 10 * 60 # 10 min @@ -305,14 +306,16 @@ class Credentials: :param str access_key: The access key part of the credentials. :param str secret_key: The secret key part of the credentials. :param str token: The security token, valid only for session credentials. + :param str account_id: The account ID associated with the credentials. :param str method: A string which identifies where the credentials were found. """ - def __init__(self, access_key, secret_key, token=None, method=None): + def __init__(self, access_key, secret_key, token=None, account_id=None, method=None): self.access_key = access_key self.secret_key = secret_key self.token = token + self.account_id = account_id if method is None: method = 'explicit' @@ -332,7 +335,7 @@ def _normalize(self): def get_frozen_credentials(self): return ReadOnlyCredentials( - self.access_key, self.secret_key, self.token + self.access_key, self.secret_key, self.token, self.account_id ) @@ -344,6 +347,7 @@ class RefreshableCredentials(Credentials): :param str access_key: The access key part of the credentials. :param str secret_key: The secret key part of the credentials. :param str token: The security token, valid only for session credentials. + :param str account_id: The account ID associated with the credentials. :param function refresh_using: Callback function to refresh the credentials. :param str method: A string which identifies where the credentials were found. @@ -362,6 +366,7 @@ def __init__( access_key, secret_key, token, + account_id, expiry_time, refresh_using, method, @@ -371,12 +376,13 @@ def __init__( self._access_key = access_key self._secret_key = secret_key self._token = token + self._account_id = account_id self._expiry_time = expiry_time self._time_fetcher = time_fetcher self._refresh_lock = threading.Lock() self.method = method self._frozen_credentials = ReadOnlyCredentials( - access_key, secret_key, token + access_key, secret_key, token, account_id ) self._normalize() @@ -390,6 +396,7 @@ def create_from_metadata(cls, metadata, refresh_using, method): access_key=metadata['access_key'], secret_key=metadata['secret_key'], token=metadata['token'], + account_id=metadata.get('account_id'), expiry_time=cls._expiry_datetime(metadata['expiry_time']), method=method, refresh_using=refresh_using, @@ -435,6 +442,19 @@ def token(self): def token(self, value): self._token = value + @property + def account_id(self): + """Warning: Using this property can lead to race conditions if you + access another property subsequently along the refresh boundary. + Please use get_frozen_credentials instead. + """ + self._refresh() + return self._account_id + + @account_id.setter + def account_id(self, value): + self._account_id = value + def _seconds_remaining(self): delta = self._expiry_time - self._time_fetcher() return total_seconds(delta) @@ -443,7 +463,7 @@ def refresh_needed(self, refresh_in=None): """Check if a refresh is needed. A refresh is needed if the expiry time associated - with the temporary credentials is less than the + with the temporary credentials is dfless than the provided ``refresh_in``. If ``time_delta`` is not provided, ``self.advisory_refresh_needed`` will be used. @@ -531,7 +551,7 @@ def _protected_refresh(self, is_mandatory): return self._set_from_data(metadata) self._frozen_credentials = ReadOnlyCredentials( - self._access_key, self._secret_key, self._token + self._access_key, self._secret_key, self._token, self._account_id ) if self._is_expired(): # We successfully refreshed credentials but for whatever @@ -571,6 +591,7 @@ def _set_from_data(self, data): logger.debug( "Retrieved credentials will expire at: %s", self._expiry_time ) + self._account_id = data.get('account_id') self._normalize() def get_frozen_credentials(self): @@ -627,6 +648,7 @@ def __init__(self, refresh_using, method, time_fetcher=_local_now): self._refresh_lock = threading.Lock() self.method = method self._frozen_credentials = None + self._account_id = None def refresh_needed(self, refresh_in=None): if self._frozen_credentials is None: @@ -680,6 +702,7 @@ def _get_cached_credentials(self): 'secret_key': creds['SecretAccessKey'], 'token': creds['SessionToken'], 'expiry_time': expiration, + 'account_id': response['AccountId'], } def _load_from_cache(self): @@ -754,6 +777,10 @@ def _create_cache_key(self): args = json.dumps(args, sort_keys=True) argument_hash = sha1(args.encode('utf-8')).hexdigest() return self._make_file_safe(argument_hash) + + def _generate_account_id(self, resp): + user_arn = resp['AssumedRoleUser']['Arn'] + return ArnParser().parse_arn(user_arn)['account'] class AssumeRoleCredentialFetcher(BaseAssumeRoleCredentialFetcher): @@ -815,7 +842,9 @@ def _get_credentials(self): """Get credentials by calling assume role.""" kwargs = self._assume_role_kwargs() client = self._create_client() - return client.assume_role(**kwargs) + resp = client.assume_role(**kwargs) + resp['AccountId'] = self._generate_account_id(resp) + return resp def _assume_role_kwargs(self): """Get the arguments for assume role based on current configuration.""" @@ -902,7 +931,9 @@ def _get_credentials(self): # the token, explicitly configure the client to not sign requests. config = Config(signature_version=UNSIGNED) client = self._client_creator('sts', config=config) - return client.assume_role_with_web_identity(**kwargs) + resp = client.assume_role_with_web_identity(**kwargs) + resp['AccountId'] = self._generate_account_id(resp) + return resp def _assume_role_kwargs(self): """Get the arguments for assume role based on current configuration.""" @@ -985,6 +1016,7 @@ def load(self): access_key=creds_dict['access_key'], secret_key=creds_dict['secret_key'], token=creds_dict.get('token'), + account_id=creds_dict.get('account_id'), method=self.METHOD, ) @@ -1016,6 +1048,7 @@ def _retrieve_credentials_using(self, credential_process): 'secret_key': parsed['SecretAccessKey'], 'token': parsed.get('SessionToken'), 'expiry_time': parsed.get('Expiration'), + 'account_id': parsed.get('AccountId'), } except KeyError as e: raise CredentialRetrievalError( @@ -1071,6 +1104,7 @@ class EnvProvider(CredentialProvider): # AWS_SESSION_TOKEN is what other AWS SDKs have standardized on. TOKENS = ['AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN'] EXPIRY_TIME = 'AWS_CREDENTIAL_EXPIRATION' + ACCOUNT_ID = 'AWS_ACCOUNT_ID' def __init__(self, environ=None, mapping=None): """ @@ -1097,6 +1131,7 @@ def _build_mapping(self, mapping): var_mapping['secret_key'] = self.SECRET_KEY var_mapping['token'] = self.TOKENS var_mapping['expiry_time'] = self.EXPIRY_TIME + var_mapping['account_id'] = self.ACCOUNT_ID else: var_mapping['access_key'] = mapping.get( 'access_key', self.ACCESS_KEY @@ -1110,6 +1145,9 @@ def _build_mapping(self, mapping): var_mapping['expiry_time'] = mapping.get( 'expiry_time', self.EXPIRY_TIME ) + var_mapping['account_id'] = mapping.get( + 'account_id', self.ACCOUNT_ID + ) return var_mapping def load(self): @@ -1123,7 +1161,6 @@ def load(self): logger.info('Found credentials in environment variables.') fetcher = self._create_credentials_fetcher() credentials = fetcher(require_expiry=False) - expiry_time = credentials['expiry_time'] if expiry_time is not None: expiry_time = parse(expiry_time) @@ -1131,6 +1168,7 @@ def load(self): credentials['access_key'], credentials['secret_key'], credentials['token'], + credentials['account_id'], expiry_time, refresh_using=fetcher, method=self.METHOD, @@ -1140,6 +1178,7 @@ def load(self): credentials['access_key'], credentials['secret_key'], credentials['token'], + credentials['account_id'], method=self.METHOD, ) else: @@ -1182,6 +1221,10 @@ def fetch_credentials(require_expiry=True): raise PartialCredentialsError( provider=method, cred_var=mapping['expiry_time'] ) + credentials['account_id'] = None + account_id = environ.get(mapping['account_id'], '') + if account_id: + credentials['account_id'] = account_id return credentials @@ -1281,6 +1324,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): """ @@ -1316,9 +1360,10 @@ def load(self): access_key, secret_key = self._extract_creds_from_mapping( profile_config, self.ACCESS_KEY, self.SECRET_KEY ) + account_id = profile_config.get(self.ACCOUNT_ID) token = self._get_session_token(profile_config) return Credentials( - access_key, secret_key, token, method=self.METHOD + access_key, secret_key, token, account_id, method=self.METHOD ) else: return None @@ -1679,6 +1724,7 @@ def _resolve_static_credentials_from_profile(self, profile): access_key=profile['aws_access_key_id'], secret_key=profile['aws_secret_access_key'], token=profile.get('aws_session_token'), + account_id=profile.get('aws_account_id'), ) except KeyError as e: raise PartialCredentialsError( @@ -1912,6 +1958,7 @@ def _retrieve_or_fail(self): access_key=creds['access_key'], secret_key=creds['secret_key'], token=creds['token'], + account_id=None, method=self.METHOD, expiry_time=_parse_if_needed(creds['expiry_time']), refresh_using=fetcher, @@ -2128,6 +2175,7 @@ def _get_credentials(self): credentials = { 'ProviderType': 'sso', + 'AccountId': self._account_id, 'Credentials': { 'AccessKeyId': credentials['accessKeyId'], 'SecretAccessKey': credentials['secretAccessKey'], diff --git a/tests/__init__.py b/tests/__init__.py index 33307c0081..b098f705f3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -320,7 +320,7 @@ def __init__( if refresh_function is None: refresh_function = self._do_refresh super().__init__( - '0', '0', '0', expires_in, refresh_function, 'INTREFRESH' + '0', '0', '0', '0', expires_in, refresh_function, 'INTREFRESH' ) self.creds_last_for = creds_last_for self.refresh_counter = 0 @@ -337,6 +337,7 @@ def _do_refresh(self): 'secret_key': next_id, 'token': next_id, 'expiry_time': self._seconds_later(self.creds_last_for), + 'account_id': next_id, } def _seconds_later(self, num_seconds): diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py index 4301b6446f..5c101a2487 100644 --- a/tests/unit/test_credentials.py +++ b/tests/unit/test_credentials.py @@ -46,6 +46,7 @@ ContainerMetadataFetcher, FileWebIdentityTokenLoader, SSOTokenLoader, + ArnParser, datetime2timestamp, ) from tests import BaseEnvVar, IntegerRefresher, mock, skip_if_windows, unittest @@ -105,6 +106,7 @@ def setUp(self): 'token': 'NEW-TOKEN', 'expiry_time': self.future_time.isoformat(), 'role_name': 'rolename', + 'account_id': '123456789012', } self.refresher.return_value = self.metadata self.mock_time = mock.Mock() @@ -112,6 +114,7 @@ def setUp(self): 'ORIGINAL-ACCESS', 'ORIGINAL-SECRET', 'ORIGINAL-TOKEN', + '123456789012', self.expiry_time, self.refresher, 'iam-role', @@ -129,12 +132,14 @@ def test_refresh_needed(self): self.assertEqual(self.creds.access_key, 'NEW-ACCESS') self.assertEqual(self.creds.secret_key, 'NEW-SECRET') self.assertEqual(self.creds.token, 'NEW-TOKEN') + self.assertEqual(self.creds.account_id, '123456789012') def test_no_expiration(self): creds = credentials.RefreshableCredentials( 'ORIGINAL-ACCESS', 'ORIGINAL-SECRET', 'ORIGINAL-TOKEN', + '123456789012', None, self.refresher, 'iam-role', @@ -153,6 +158,7 @@ def test_no_refresh_needed(self): self.assertEqual(self.creds.access_key, 'ORIGINAL-ACCESS') self.assertEqual(self.creds.secret_key, 'ORIGINAL-SECRET') self.assertEqual(self.creds.token, 'ORIGINAL-TOKEN') + self.assertEqual(self.creds.account_id, '123456789012') def test_get_credentials_set(self): # We need to return a consistent set of credentials to use during the @@ -165,6 +171,7 @@ def test_get_credentials_set(self): self.assertEqual(credential_set.access_key, 'ORIGINAL-ACCESS') self.assertEqual(credential_set.secret_key, 'ORIGINAL-SECRET') self.assertEqual(credential_set.token, 'ORIGINAL-TOKEN') + self.assertEqual(self.creds.account_id, '123456789012') def test_refresh_returns_empty_dict(self): self.refresher.return_value = {} @@ -201,6 +208,7 @@ def setUp(self): 'token': 'NEW-TOKEN', 'expiry_time': self.future_time.isoformat(), 'role_name': 'rolename', + 'account_id': '123456789012', } self.refresher.return_value = self.metadata self.mock_time = mock.Mock() @@ -257,6 +265,7 @@ def get_expected_creds_from_response(self, response): 'secret_key': response['Credentials']['SecretAccessKey'], 'token': response['Credentials']['SessionToken'], 'expiry_time': expiration, + 'account_id': ArnParser().parse_arn(response['AssumedRoleUser']['Arn'])['account'], } def some_future_time(self): @@ -271,6 +280,7 @@ def test_no_cache(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) refresher = credentials.AssumeRoleCredentialFetcher( @@ -295,6 +305,7 @@ def test_expiration_in_datetime_format(self): # are immediately expired. 'Expiration': self.some_future_time(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) refresher = credentials.AssumeRoleCredentialFetcher( @@ -317,7 +328,9 @@ def test_retrieves_from_cache(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': utc_timestamp, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012', } } client_creator = mock.Mock() @@ -341,6 +354,7 @@ def test_cache_key_is_windows_safe(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } cache = {} client_creator = self.create_client_creator(with_response=response) @@ -351,7 +365,6 @@ def test_cache_key_is_windows_safe(self): ) refresher.fetch_credentials() - # On windows, you cannot use a a ':' in the filename, so # we need to make sure that it doesn't make it into the cache key. cache_key = '75c539f0711ba78c5b9e488d0add95f178a54d74' @@ -366,6 +379,7 @@ def test_cache_key_with_role_session_name(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } cache = {} client_creator = self.create_client_creator(with_response=response) @@ -379,7 +393,6 @@ def test_cache_key_with_role_session_name(self): extra_args={'RoleSessionName': role_session_name}, ) refresher.fetch_credentials() - # This is the sha256 hex digest of the expected assume role args. cache_key = '2964201f5648c8be5b9460a9cf842d73a266daf2' self.assertIn(cache_key, cache) @@ -393,6 +406,7 @@ def test_cache_key_with_policy(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } cache = {} client_creator = self.create_client_creator(with_response=response) @@ -413,7 +427,6 @@ def test_cache_key_with_policy(self): extra_args={'Policy': policy}, ) refresher.fetch_credentials() - # This is the sha256 hex digest of the expected assume role args. cache_key = '176f223d915e82456c253545e192aa21d68f5ab8' self.assertIn(cache_key, cache) @@ -427,6 +440,7 @@ def test_assume_role_in_cache_but_expired(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) cache = { @@ -436,7 +450,9 @@ def test_assume_role_in_cache_but_expired(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': datetime.now(tzlocal()), - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo-cached'}, + 'AccountId': '123456789012', } } @@ -456,6 +472,7 @@ def test_role_session_name_can_be_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) role_session_name = 'myname' @@ -481,6 +498,7 @@ def test_external_id_can_be_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) external_id = 'my_external_id' @@ -508,6 +526,7 @@ def test_policy_can_be_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) policy = json.dumps( @@ -540,6 +559,7 @@ def test_duration_seconds_can_be_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) duration = 1234 @@ -567,6 +587,7 @@ def test_mfa(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) prompter = mock.Mock(return_value='token-code') @@ -607,6 +628,7 @@ def test_refreshes(self): datetime.now(tzlocal()) - timedelta(seconds=100) ).isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, }, { 'Credentials': { @@ -614,7 +636,8 @@ def test_refreshes(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, }, ] client_creator = self.create_client_creator(with_response=responses) @@ -647,6 +670,7 @@ def test_mfa_refresh_enabled(self): datetime.now(tzlocal()) - timedelta(seconds=100) ).isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, }, { 'Credentials': { @@ -654,7 +678,8 @@ def test_mfa_refresh_enabled(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, }, ] client_creator = self.create_client_creator(with_response=responses) @@ -720,6 +745,7 @@ def get_expected_creds_from_response(self, response): 'secret_key': response['Credentials']['SecretAccessKey'], 'token': response['Credentials']['SessionToken'], 'expiry_time': expiration, + 'account_id': ArnParser().parse_arn(response['AssumedRoleUser']['Arn'])['account'], } def test_no_cache(self): @@ -730,6 +756,7 @@ def test_no_cache(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) refresher = credentials.AssumeRoleWithWebIdentityCredentialFetcher( @@ -751,7 +778,9 @@ def test_retrieves_from_cache(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': utc_timestamp, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012' } } client_creator = mock.Mock() @@ -774,6 +803,7 @@ def test_assume_role_in_cache_but_expired(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) cache = { @@ -783,7 +813,9 @@ def test_assume_role_in_cache_but_expired(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': datetime.now(tzlocal()), - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012' } } @@ -840,6 +872,7 @@ def test_assume_role_with_no_cache(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) mock_loader_cls = self._mock_loader_cls('totally.a.token') @@ -870,7 +903,9 @@ def test_assume_role_retrieves_from_cache(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': utc_timestamp, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012' } } mock_loader_cls = self._mock_loader_cls('totally.a.token') @@ -900,6 +935,7 @@ def test_assume_role_in_cache_but_expired(self): 'SessionToken': 'baz', 'Expiration': valid_creds, }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } cache = { 'development--myrole': { @@ -908,7 +944,9 @@ def test_assume_role_in_cache_but_expired(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': expired_creds, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012', } } client_creator = self.create_client_creator(with_response=response) @@ -937,6 +975,7 @@ def test_role_session_name_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) mock_loader_cls = self._mock_loader_cls('totally.a.token') @@ -1012,6 +1051,20 @@ def test_envvars_found_with_session_token(self): self.assertEqual(creds.token, 'baz') self.assertEqual(creds.method, 'env') + def test_envvars_found_with_account_id(self): + environ = { + 'AWS_ACCESS_KEY_ID': 'foo', + 'AWS_SECRET_ACCESS_KEY': 'bar', + 'AWS_ACCOUNT_ID': '1234567890', + } + provider = credentials.EnvProvider(environ) + creds = provider.load() + self.assertIsNotNone(creds) + self.assertEqual(creds.access_key, 'foo') + self.assertEqual(creds.secret_key, 'bar') + self.assertEqual(creds.account_id, '1234567890') + self.assertEqual(creds.method, 'env') + def test_envvars_not_found(self): provider = credentials.EnvProvider(environ={}) creds = provider.load() @@ -1022,6 +1075,7 @@ def test_envvars_empty_string(self): 'AWS_ACCESS_KEY_ID': '', 'AWS_SECRET_ACCESS_KEY': '', 'AWS_SECURITY_TOKEN': '', + 'AWS_ACCOUNT_ID': '', } provider = credentials.EnvProvider(environ) creds = provider.load() @@ -1967,6 +2021,7 @@ def test_assume_role_with_no_cache(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -1981,6 +2036,7 @@ def test_assume_role_with_no_cache(self): self.assertEqual(creds.access_key, 'foo') self.assertEqual(creds.secret_key, 'bar') self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.account_id, '123456789012') def test_assume_role_with_datetime(self): response = { @@ -1995,6 +2051,7 @@ def test_assume_role_with_datetime(self): # are immediately expired. 'Expiration': datetime.now(tzlocal()) + timedelta(hours=20), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -2009,6 +2066,7 @@ def test_assume_role_with_datetime(self): self.assertEqual(creds.access_key, 'foo') self.assertEqual(creds.secret_key, 'bar') self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.account_id, '123456789012') def test_assume_role_refresher_serializes_datetime(self): client = mock.Mock() @@ -2022,7 +2080,8 @@ def test_assume_role_refresher_serializes_datetime(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': expiration, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } refresh = create_assume_role_refresher(client, {}) expiry_time = refresh()['expiry_time'] @@ -2041,7 +2100,9 @@ def test_assume_role_retrieves_from_cache(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': utc_timestamp, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012', } } provider = credentials.AssumeRoleProvider( @@ -2056,6 +2117,7 @@ def test_assume_role_retrieves_from_cache(self): self.assertEqual(creds.access_key, 'foo-cached') self.assertEqual(creds.secret_key, 'bar-cached') self.assertEqual(creds.token, 'baz-cached') + self.assertEqual(creds.account_id, '123456789012') def test_chain_prefers_cache(self): date_in_future = datetime.utcnow() + timedelta(seconds=1000) @@ -2072,7 +2134,9 @@ def test_chain_prefers_cache(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': utc_timestamp, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012', } } @@ -2092,6 +2156,7 @@ def test_chain_prefers_cache(self): self.assertEqual(creds.access_key, 'foo-cached') self.assertEqual(creds.secret_key, 'bar-cached') self.assertEqual(creds.token, 'baz-cached') + self.assertEqual(creds.account_id, '123456789012') def test_cache_key_is_windows_safe(self): response = { @@ -2101,6 +2166,7 @@ def test_cache_key_is_windows_safe(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } cache = {} self.fake_config['profiles']['development'][ @@ -2130,6 +2196,7 @@ def test_cache_key_with_role_session_name(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } cache = {} self.fake_config['profiles']['development'][ @@ -2164,6 +2231,7 @@ def test_assume_role_in_cache_but_expired(self): 'SessionToken': 'baz', 'Expiration': valid_creds, }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) cache = { @@ -2173,7 +2241,9 @@ def test_assume_role_in_cache_but_expired(self): 'SecretAccessKey': 'bar-cached', 'SessionToken': 'baz-cached', 'Expiration': expired_creds, - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, + 'AccountId': '123456789012', } } provider = credentials.AssumeRoleProvider( @@ -2187,7 +2257,8 @@ def test_assume_role_in_cache_but_expired(self): self.assertEqual(creds.access_key, 'foo') self.assertEqual(creds.secret_key, 'bar') - self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.token, 'baz'), + self.assertEqual(creds.account_id, '123456789012') def test_role_session_name_provided(self): dev_profile = self.fake_config['profiles']['development'] @@ -2199,6 +2270,7 @@ def test_role_session_name_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -2225,6 +2297,7 @@ def test_external_id_provided(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -2251,6 +2324,7 @@ def test_assume_role_with_duration(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -2279,6 +2353,7 @@ def test_assume_role_with_bad_duration(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -2305,6 +2380,7 @@ def test_assume_role_with_mfa(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) prompter = mock.Mock(return_value='token-code') @@ -2345,6 +2421,7 @@ def test_assume_role_populates_session_name_on_refresh(self): # refresh behavior will be triggered. 'Expiration': expiration_time.isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, }, { 'Credentials': { @@ -2352,7 +2429,8 @@ def test_assume_role_populates_session_name_on_refresh(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': next_expiration_time.isoformat(), - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, }, ] client_creator = self.create_client_creator(with_response=responses) @@ -2401,6 +2479,7 @@ def test_assume_role_mfa_cannot_refresh_credentials(self): # refresh behavior will be triggered. 'Expiration': expiration_time.isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) provider = credentials.AssumeRoleProvider( @@ -2548,6 +2627,7 @@ def test_assume_role_with_credential_source(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) @@ -2580,6 +2660,7 @@ def test_assume_role_with_credential_source(self): self.assertEqual(creds.access_key, 'foo') self.assertEqual(creds.secret_key, 'bar') self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.account_id, '123456789012') client_creator.assert_called_with( 'sts', aws_access_key_id=fake_creds.access_key, @@ -2623,6 +2704,7 @@ def test_source_profile_can_reference_self(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) @@ -2648,6 +2730,7 @@ def test_source_profile_can_reference_self(self): self.assertEqual(creds.access_key, 'foo') self.assertEqual(creds.secret_key, 'bar') self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.account_id, '123456789012') def test_infinite_looping_profiles_raises_error(self): config = { @@ -2669,8 +2752,8 @@ def test_infinite_looping_profiles_raises_error(self): def test_recursive_assume_role(self): assume_responses = [ - Credentials('foo', 'bar', 'baz'), - Credentials('spam', 'eggs', 'spamandegss'), + Credentials('foo', 'bar', 'baz', '123456789012'), + Credentials('spam', 'eggs', 'spamandegss', '123456789012'), ] responses = [] for credential_set in assume_responses: @@ -2681,7 +2764,8 @@ def test_recursive_assume_role(self): 'SecretAccessKey': credential_set.secret_key, 'SessionToken': credential_set.token, 'Expiration': self.some_future_time().isoformat(), - } + }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } ) client_creator = self.create_client_creator(with_response=responses) @@ -2710,6 +2794,7 @@ def test_recursive_assume_role(self): self.assertEqual(creds.access_key, expected_creds.access_key) self.assertEqual(creds.secret_key, expected_creds.secret_key) self.assertEqual(creds.token, expected_creds.token) + self.assertEqual(creds.account_id, expected_creds.account_id) client_creator.assert_has_calls( [ @@ -2736,6 +2821,7 @@ def test_assume_role_with_profile_provider(self): 'SessionToken': 'baz', 'Expiration': self.some_future_time().isoformat(), }, + 'AssumedRoleUser': {'AssumedRoleId': 'foo', 'Arn': 'arn:aws:iam::123456789012:assumed-role/foo'}, } client_creator = self.create_client_creator(with_response=response) mock_builder = mock.Mock(spec=ProfileProviderBuilder) @@ -2762,6 +2848,7 @@ def test_assume_role_with_profile_provider(self): self.assertEqual(creds.access_key, 'foo') self.assertEqual(creds.secret_key, 'bar') self.assertEqual(creds.token, 'baz') + self.assertEqual(creds.account_id, '123456789012') class ProfileProvider: @@ -2888,7 +2975,7 @@ def test_mandatory_refresh_needed(self): advisory_refresh=3, ) temp = creds.get_frozen_credentials() - self.assertEqual(temp, credentials.ReadOnlyCredentials('1', '1', '1')) + self.assertEqual(temp, credentials.ReadOnlyCredentials('1', '1', '1', '1')) def test_advisory_refresh_needed(self): creds = IntegerRefresher( @@ -2899,7 +2986,7 @@ def test_advisory_refresh_needed(self): advisory_refresh=5, ) temp = creds.get_frozen_credentials() - self.assertEqual(temp, credentials.ReadOnlyCredentials('1', '1', '1')) + self.assertEqual(temp, credentials.ReadOnlyCredentials('1', '1', '1', '1')) def test_refresh_fails_is_not_an_error_during_advisory_period(self): fail_refresh = mock.Mock(side_effect=Exception("refresh failed")) @@ -2916,7 +3003,7 @@ def test_refresh_fails_is_not_an_error_during_advisory_period(self): # Because we're in the advisory period we'll not propogate # the exception and return the current set of credentials # (generation '1'). - self.assertEqual(temp, credentials.ReadOnlyCredentials('0', '0', '0')) + self.assertEqual(temp, credentials.ReadOnlyCredentials('0', '0', '0', '0')) def test_exception_propogated_on_error_during_mandatory_period(self): fail_refresh = mock.Mock(side_effect=Exception("refresh failed")) @@ -3176,6 +3263,7 @@ def test_can_retrieve_via_process(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3186,6 +3274,7 @@ def test_can_retrieve_via_process(self): self.assertEqual(creds.secret_key, 'bar') self.assertEqual(creds.token, 'baz') self.assertEqual(creds.method, 'custom-process') + self.assertEqual(creds.account_id, '123456789012') self.popen_mock.assert_called_with( ['my-process'], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -3203,6 +3292,7 @@ def test_can_pass_arguments_through(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3232,6 +3322,7 @@ def test_can_refresh_credentials(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': expired_date, + 'AccountId': '123456789012', } ) new_creds = self._get_output( @@ -3241,6 +3332,7 @@ def test_can_refresh_credentials(self): 'SecretAccessKey': 'bar2', 'SessionToken': 'baz2', 'Expiration': future_date, + 'AccountId': '123456789012', } ) self.invoked_process.communicate.side_effect = [old_creds, new_creds] @@ -3253,6 +3345,7 @@ def test_can_refresh_credentials(self): self.assertEqual(creds.secret_key, 'bar2') self.assertEqual(creds.token, 'baz2') self.assertEqual(creds.method, 'custom-process') + self.assertEqual(creds.account_id, '123456789012') def test_non_zero_rc_raises_exception(self): self.loaded_config['profiles'] = { @@ -3277,6 +3370,7 @@ def test_unsupported_version_raises_mismatch(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3296,6 +3390,7 @@ def test_missing_version_in_payload_returned_raises_exception(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3315,6 +3410,7 @@ def test_missing_access_key_raises_exception(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3334,6 +3430,7 @@ def test_missing_secret_key_raises_exception(self): # Missing secret key. 'SessionToken': 'baz', 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3353,6 +3450,7 @@ def test_missing_session_token(self): 'SecretAccessKey': 'bar', # Missing session token. 'Expiration': '2999-01-01T00:00:00Z', + 'AccountId': '123456789012', } ) @@ -3363,6 +3461,7 @@ def test_missing_session_token(self): self.assertEqual(creds.secret_key, 'bar') self.assertIsNone(creds.token) self.assertEqual(creds.method, 'custom-process') + self.assertEqual(creds.account_id, '123456789012') def test_missing_expiration(self): self.loaded_config['profiles'] = { @@ -3375,6 +3474,31 @@ def test_missing_expiration(self): 'SecretAccessKey': 'bar', 'SessionToken': 'baz', # Missing expiration. + 'AccountId': '123456789012', + } + ) + + provider = self.create_process_provider() + 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, 'custom-process') + self.assertEqual(creds.account_id, '123456789012') + + def test_missing_account_id(self): + self.loaded_config['profiles'] = { + 'default': {'credential_process': 'my-process'} + } + self._set_process_return_value( + { + 'Version': 1, + 'AccessKeyId': 'foo', + 'SecretAccessKey': 'bar', + 'SessionToken': 'baz', + 'Expiration': '2999-01-01T00:00:00Z', + # Missing account id. } ) @@ -3396,6 +3520,7 @@ def test_missing_expiration_and_session_token(self): 'AccessKeyId': 'foo', 'SecretAccessKey': 'bar', # Missing session token and expiration + 'AccountId': '123456789012', } ) @@ -3486,9 +3611,11 @@ def test_can_fetch_credentials(self): self.assertEqual(credentials['secret_key'], 'bar') self.assertEqual(credentials['token'], 'baz') self.assertEqual(credentials['expiry_time'], '2008-09-23T12:43:20Z') + self.assertEqual(credentials['account_id'], self.account_id) cache_key = '048db75bbe50955c16af7aba6ff9c41a3131bb7e' expected_cached_credentials = { 'ProviderType': 'sso', + 'AccountId': self.account_id, 'Credentials': { 'AccessKeyId': 'foo', 'SecretAccessKey': 'bar', @@ -3587,6 +3714,7 @@ def test_load_sso_credentials_without_cache(self): self.assertEqual(credentials.access_key, 'foo') self.assertEqual(credentials.secret_key, 'bar') self.assertEqual(credentials.token, 'baz') + self.assertEqual(credentials.account_id, self.account_id) def test_load_sso_credentials_with_cache(self): cached_creds = { @@ -3595,7 +3723,8 @@ def test_load_sso_credentials_with_cache(self): 'SecretAccessKey': 'cached-sak', 'SessionToken': 'cached-st', 'Expiration': self.expires_at.strftime('%Y-%m-%dT%H:%M:%S%Z'), - } + }, + 'AccountId': self.account_id, } self.cache[self.cached_creds_key] = cached_creds credentials = self.provider.load()