From c6ee6c66c57a58362923a1bc82f406c31866aeea Mon Sep 17 00:00:00 2001 From: Peter Dolberg Date: Mon, 15 Jun 2015 14:04:45 -0700 Subject: [PATCH 1/4] Adding support for a custom RoleSessionName when assuming a role --- awscli/customizations/assumerole.py | 11 ++++++---- awscli/topics/config-vars.rst | 5 +++++ tests/unit/customizations/test_assumerole.py | 21 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/awscli/customizations/assumerole.py b/awscli/customizations/assumerole.py index 46e34611f173..70b727fbd3dc 100644 --- a/awscli/customizations/assumerole.py +++ b/awscli/customizations/assumerole.py @@ -60,8 +60,6 @@ def create_assume_role_provider(session, provider_cls): def create_refresher_function(client, params): def refresh(): - role_session_name = 'AWS-CLI-session-%s' % (int(time.time())) - params['RoleSessionName'] = role_session_name response = client.assume_role(**params) credentials = response['Credentials'] # We need to normalize the credential names to @@ -252,6 +250,7 @@ def _get_role_config_values(self): raise PartialCredentialsError(provider=self.METHOD, cred_var=str(e)) external_id = profiles[self._profile_name].get('external_id') + role_session_name = profiles[self._profile_name].get('role_session_name') if source_profile not in profiles: raise InvalidConfigError( 'The source_profile "%s" referenced in ' @@ -264,6 +263,7 @@ def _get_role_config_values(self): 'source_profile': source_profile, 'mfa_serial': mfa_serial, 'source_cred_values': source_cred_values, + 'role_session_name': role_session_name } def _create_creds_from_response(self, response): @@ -300,8 +300,9 @@ def _retrieve_temp_credentials(self): client = self._create_client_from_config(config) assume_role_kwargs = self._assume_role_base_kwargs(config) - role_session_name = 'AWS-CLI-session-%s' % (int(time.time())) - assume_role_kwargs['RoleSessionName'] = role_session_name + if assume_role_kwargs.get('RoleSessionName') is None: + role_session_name = 'AWS-CLI-session-%s' % (int(time.time())) + assume_role_kwargs['RoleSessionName'] = role_session_name response = client.assume_role(**assume_role_kwargs) creds = self._create_creds_from_response(response) @@ -315,4 +316,6 @@ def _assume_role_base_kwargs(self, config): token_code = self._prompter("Enter MFA code: ") assume_role_kwargs['SerialNumber'] = config['mfa_serial'] assume_role_kwargs['TokenCode'] = token_code + if config['role_session_name'] is not None: + assume_role_kwargs['RoleSessionName'] = config['role_session_name'] return assume_role_kwargs diff --git a/awscli/topics/config-vars.rst b/awscli/topics/config-vars.rst index 3dfd0681157b..4a8a13f6bfbe 100644 --- a/awscli/topics/config-vars.rst +++ b/awscli/topics/config-vars.rst @@ -182,6 +182,11 @@ in the AWS CLI config file: authentication. The value is either the serial number for a hardware device (such as GAHT12345678) or an Amazon Resource Name (ARN) for a virtual device (such as arn:aws:iam::123456789012:mfa/user). +* ``role_session_name`` - The name applied to this assume-role session. This + value affects the assumed role user ARN (such as + arn:aws:sts::123456789012:assumed-role/role_name/role_session_name). This + maps to the ``RoleSessionName`` parameter in the ``AssumeRole`` operation. + This is an optional parameter. If you do not have MFA authentication required, then you only need to specify a ``role_arn`` and a ``source_profile``. diff --git a/tests/unit/customizations/test_assumerole.py b/tests/unit/customizations/test_assumerole.py index 8d8b8675afba..c8f2cd4d8747 100644 --- a/tests/unit/customizations/test_assumerole.py +++ b/tests/unit/customizations/test_assumerole.py @@ -189,6 +189,27 @@ def test_assume_role_in_cache_but_expired(self): self.assertEqual(credentials.access_key, 'foo') self.assertEqual(credentials.secret_key, 'bar') self.assertEqual(credentials.token, 'baz') + + def test_role_session_name_provided(self): + self.fake_config['profiles']['development']['role_session_name'] = 'myname' + response = { + 'Credentials': { + 'AccessKeyId': 'foo', + 'SecretAccessKey': 'bar', + 'SessionToken': 'baz', + 'Expiration': datetime.now(tzlocal()).isoformat(), + }, + } + client_creator = self.create_client_creator(with_response=response) + provider = assumerole.AssumeRoleProvider( + self.create_config_loader(), + client_creator, cache={}, profile_name='development') + + provider.load() + + client = client_creator.return_value + client.assume_role.assert_called_with( + RoleArn='myrole', RoleSessionName='myname') def test_external_id_provided(self): self.fake_config['profiles']['development']['external_id'] = 'myid' From 3bacb039712bb0e80aa13acf553a3ca8d4933358 Mon Sep 17 00:00:00 2001 From: Peter Dolberg Date: Thu, 18 Jun 2015 10:49:16 -0700 Subject: [PATCH 2/4] Can now assume a role from an instance profile (EC2 role) --- awscli/customizations/assumerole.py | 78 ++++++++++++-------- tests/unit/customizations/test_assumerole.py | 43 +++++++++-- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/awscli/customizations/assumerole.py b/awscli/customizations/assumerole.py index 70b727fbd3dc..345009014cf4 100644 --- a/awscli/customizations/assumerole.py +++ b/awscli/customizations/assumerole.py @@ -1,16 +1,16 @@ -import os -import time +from datetime import datetime +import getpass import json import logging -import getpass - -from dateutil.parser import parse -from datetime import datetime -from dateutil.tz import tzlocal +import os +import time from botocore import credentials from botocore.compat import total_seconds -from botocore.exceptions import PartialCredentialsError +from botocore.credentials import InstanceMetadataProvider, Credentials +from botocore.utils import InstanceMetadataFetcher +from dateutil.parser import parse +from dateutil.tz import tzlocal LOG = logging.getLogger(__name__) @@ -138,7 +138,8 @@ class AssumeRoleProvider(credentials.CredentialProvider): EXPIRY_WINDOW_SECONDS = 60 * 15 def __init__(self, load_config, client_creator, cache, profile_name, - prompter=getpass.getpass): + prompter=getpass.getpass, + fallback_cred_provider=None): """ :type load_config: callable @@ -171,6 +172,12 @@ def __init__(self, load_config, client_creator, cache, profile_name, self._profile_name = profile_name self._cache = cache self._prompter = prompter + if fallback_cred_provider==None: + self._fallback_cred_provider=InstanceMetadataProvider( + iam_role_fetcher=InstanceMetadataFetcher()) + else: + self._fallback_cred_provider = fallback_cred_provider + # The _loaded_config attribute will be populated from the # load_config() function once the configuration is actually # loaded. The reason we go through all this instead of just @@ -181,6 +188,7 @@ def __init__(self, load_config, client_creator, cache, profile_name, self._loaded_config = {} def load(self): + LOG.debug("Attempting load from assume role provider") self._loaded_config = self._load_config() if self._has_assume_role_config_vars(): return self._load_creds_via_assume_role() @@ -242,29 +250,40 @@ def _write_cached_credentials(self, creds, cache_key): def _get_role_config_values(self): # This returns the role related configuration. profiles = self._loaded_config.get('profiles', {}) - try: - source_profile = profiles[self._profile_name]['source_profile'] - role_arn = profiles[self._profile_name]['role_arn'] - mfa_serial = profiles[self._profile_name].get('mfa_serial') - except KeyError as e: - raise PartialCredentialsError(provider=self.METHOD, - cred_var=str(e)) - external_id = profiles[self._profile_name].get('external_id') - role_session_name = profiles[self._profile_name].get('role_session_name') - if source_profile not in profiles: - raise InvalidConfigError( - 'The source_profile "%s" referenced in ' - 'the profile "%s" does not exist.' % ( - source_profile, self._profile_name)) - source_cred_values = profiles[source_profile] + role_profile=profiles[self._profile_name]; + + source_profile = role_profile.get('source_profile') + role_arn = role_profile['role_arn'] + mfa_serial = role_profile.get('mfa_serial') + external_id = role_profile.get('external_id') + role_session_name = role_profile.get('role_session_name') + return { 'role_arn': role_arn, 'external_id': external_id, 'source_profile': source_profile, 'mfa_serial': mfa_serial, - 'source_cred_values': source_cred_values, 'role_session_name': role_session_name } + + def _get_source_profile_credentials(self,source_profile): + profiles = self._loaded_config.get('profiles', {}) + if source_profile == None : + return self._fallback_cred_provider.load() + + if source_profile not in profiles: + raise InvalidConfigError( + 'The source_profile "%s" referenced in ' + 'the profile "%s" does not exist.' % ( + source_profile, self._profile_name)) + + access_key_id=profiles[source_profile]['aws_access_key_id'] + secret_key=profiles[source_profile]['aws_secret_access_key'] + session_token=profiles[source_profile].get('aws_session_token') + return Credentials( + access_key=access_key_id, + secret_key=secret_key, + token=session_token) def _create_creds_from_response(self, response): config = self._get_role_config_values() @@ -286,11 +305,12 @@ def _create_creds_from_response(self, response): refresh_using=refresh_func) def _create_client_from_config(self, config): - source_cred_values = config['source_cred_values'] + source_profile=config['source_profile']; + creds=self._get_source_profile_credentials(source_profile); client = self._client_creator( - 'sts', aws_access_key_id=source_cred_values['aws_access_key_id'], - aws_secret_access_key=source_cred_values['aws_secret_access_key'], - aws_session_token=source_cred_values.get('aws_session_token'), + 'sts', aws_access_key_id=creds.access_key, + aws_secret_access_key=creds.secret_key, + aws_session_token=creds.token, ) return client diff --git a/tests/unit/customizations/test_assumerole.py b/tests/unit/customizations/test_assumerole.py index c8f2cd4d8747..fc02b528aa02 100644 --- a/tests/unit/customizations/test_assumerole.py +++ b/tests/unit/customizations/test_assumerole.py @@ -18,13 +18,15 @@ import mock from botocore.hooks import HierarchicalEmitter -from botocore.exceptions import PartialCredentialsError +from botocore.credentials import Credentials + from dateutil.tz import tzlocal from awscli.testutils import unittest from awscli.customizations import assumerole + class TestAssumeRolePlugin(unittest.TestCase): def test_assume_role_provider_injected(self): session = mock.Mock() @@ -309,13 +311,44 @@ def test_no_config_is_noop(self): def test_source_profile_not_provided(self): del self.fake_config['profiles']['development']['source_profile'] + + + fallback_creds=Credentials( + access_key='access-fallback', + secret_key='secret-fallback', + token='token-fallback') + + mock_fallback_provider=mock.Mock() + mock_fallback_provider.load.return_value=fallback_creds + + response = { + 'Credentials': { + 'AccessKeyId': 'AKI', + 'SecretAccessKey': 'SAK', + 'SessionToken': 'ST', + 'Expiration': datetime.now(tzlocal()).isoformat() + }, + } + client = mock.Mock() + client.assume_role.return_value = response + def side_effect_of_create_client(*args,**kwargs): + self.assertEqual(args[0],'sts') + self.assertEqual(kwargs['aws_access_key_id'],'access-fallback') + self.assertEqual(kwargs['aws_secret_access_key'],'secret-fallback') + self.assertEqual(kwargs['aws_session_token'],'token-fallback') + return client; + + client_creator=mock.Mock(side_effect=side_effect_of_create_client) + provider = assumerole.AssumeRoleProvider( self.create_config_loader(), - mock.Mock(), cache={}, profile_name='development') + client_creator, cache={}, profile_name='development', + fallback_cred_provider=mock_fallback_provider) - # source_profile is required, we shoudl get an error. - with self.assertRaises(PartialCredentialsError): - provider.load() + credentials=provider.load() + self.assertEqual(credentials.access_key, 'AKI') + self.assertEqual(credentials.secret_key, 'SAK') + self.assertEqual(credentials.token, 'ST') def test_source_profile_does_not_exist(self): dev_profile = self.fake_config['profiles']['development'] From 720727058eb1188f1bc89db40f1c24e724b9fdac Mon Sep 17 00:00:00 2001 From: Peter Dolberg Date: Mon, 24 Aug 2015 12:40:21 -0700 Subject: [PATCH 3/4] Revert "Can now assume a role from an instance profile (EC2 role)" This reverts commit 3bacb039712bb0e80aa13acf553a3ca8d4933358. --- awscli/customizations/assumerole.py | 78 ++++++++------------ tests/unit/customizations/test_assumerole.py | 43 ++--------- 2 files changed, 34 insertions(+), 87 deletions(-) diff --git a/awscli/customizations/assumerole.py b/awscli/customizations/assumerole.py index 345009014cf4..70b727fbd3dc 100644 --- a/awscli/customizations/assumerole.py +++ b/awscli/customizations/assumerole.py @@ -1,17 +1,17 @@ -from datetime import datetime -import getpass -import json -import logging import os import time +import json +import logging +import getpass -from botocore import credentials -from botocore.compat import total_seconds -from botocore.credentials import InstanceMetadataProvider, Credentials -from botocore.utils import InstanceMetadataFetcher from dateutil.parser import parse +from datetime import datetime from dateutil.tz import tzlocal +from botocore import credentials +from botocore.compat import total_seconds +from botocore.exceptions import PartialCredentialsError + LOG = logging.getLogger(__name__) @@ -138,8 +138,7 @@ class AssumeRoleProvider(credentials.CredentialProvider): EXPIRY_WINDOW_SECONDS = 60 * 15 def __init__(self, load_config, client_creator, cache, profile_name, - prompter=getpass.getpass, - fallback_cred_provider=None): + prompter=getpass.getpass): """ :type load_config: callable @@ -172,12 +171,6 @@ def __init__(self, load_config, client_creator, cache, profile_name, self._profile_name = profile_name self._cache = cache self._prompter = prompter - if fallback_cred_provider==None: - self._fallback_cred_provider=InstanceMetadataProvider( - iam_role_fetcher=InstanceMetadataFetcher()) - else: - self._fallback_cred_provider = fallback_cred_provider - # The _loaded_config attribute will be populated from the # load_config() function once the configuration is actually # loaded. The reason we go through all this instead of just @@ -188,7 +181,6 @@ def __init__(self, load_config, client_creator, cache, profile_name, self._loaded_config = {} def load(self): - LOG.debug("Attempting load from assume role provider") self._loaded_config = self._load_config() if self._has_assume_role_config_vars(): return self._load_creds_via_assume_role() @@ -250,40 +242,29 @@ def _write_cached_credentials(self, creds, cache_key): def _get_role_config_values(self): # This returns the role related configuration. profiles = self._loaded_config.get('profiles', {}) - role_profile=profiles[self._profile_name]; - - source_profile = role_profile.get('source_profile') - role_arn = role_profile['role_arn'] - mfa_serial = role_profile.get('mfa_serial') - external_id = role_profile.get('external_id') - role_session_name = role_profile.get('role_session_name') - + try: + source_profile = profiles[self._profile_name]['source_profile'] + role_arn = profiles[self._profile_name]['role_arn'] + mfa_serial = profiles[self._profile_name].get('mfa_serial') + except KeyError as e: + raise PartialCredentialsError(provider=self.METHOD, + cred_var=str(e)) + external_id = profiles[self._profile_name].get('external_id') + role_session_name = profiles[self._profile_name].get('role_session_name') + if source_profile not in profiles: + raise InvalidConfigError( + 'The source_profile "%s" referenced in ' + 'the profile "%s" does not exist.' % ( + source_profile, self._profile_name)) + source_cred_values = profiles[source_profile] return { 'role_arn': role_arn, 'external_id': external_id, 'source_profile': source_profile, 'mfa_serial': mfa_serial, + 'source_cred_values': source_cred_values, 'role_session_name': role_session_name } - - def _get_source_profile_credentials(self,source_profile): - profiles = self._loaded_config.get('profiles', {}) - if source_profile == None : - return self._fallback_cred_provider.load() - - if source_profile not in profiles: - raise InvalidConfigError( - 'The source_profile "%s" referenced in ' - 'the profile "%s" does not exist.' % ( - source_profile, self._profile_name)) - - access_key_id=profiles[source_profile]['aws_access_key_id'] - secret_key=profiles[source_profile]['aws_secret_access_key'] - session_token=profiles[source_profile].get('aws_session_token') - return Credentials( - access_key=access_key_id, - secret_key=secret_key, - token=session_token) def _create_creds_from_response(self, response): config = self._get_role_config_values() @@ -305,12 +286,11 @@ def _create_creds_from_response(self, response): refresh_using=refresh_func) def _create_client_from_config(self, config): - source_profile=config['source_profile']; - creds=self._get_source_profile_credentials(source_profile); + source_cred_values = config['source_cred_values'] client = self._client_creator( - 'sts', aws_access_key_id=creds.access_key, - aws_secret_access_key=creds.secret_key, - aws_session_token=creds.token, + 'sts', aws_access_key_id=source_cred_values['aws_access_key_id'], + aws_secret_access_key=source_cred_values['aws_secret_access_key'], + aws_session_token=source_cred_values.get('aws_session_token'), ) return client diff --git a/tests/unit/customizations/test_assumerole.py b/tests/unit/customizations/test_assumerole.py index fc02b528aa02..c8f2cd4d8747 100644 --- a/tests/unit/customizations/test_assumerole.py +++ b/tests/unit/customizations/test_assumerole.py @@ -18,15 +18,13 @@ import mock from botocore.hooks import HierarchicalEmitter -from botocore.credentials import Credentials - +from botocore.exceptions import PartialCredentialsError from dateutil.tz import tzlocal from awscli.testutils import unittest from awscli.customizations import assumerole - class TestAssumeRolePlugin(unittest.TestCase): def test_assume_role_provider_injected(self): session = mock.Mock() @@ -311,44 +309,13 @@ def test_no_config_is_noop(self): def test_source_profile_not_provided(self): del self.fake_config['profiles']['development']['source_profile'] - - - fallback_creds=Credentials( - access_key='access-fallback', - secret_key='secret-fallback', - token='token-fallback') - - mock_fallback_provider=mock.Mock() - mock_fallback_provider.load.return_value=fallback_creds - - response = { - 'Credentials': { - 'AccessKeyId': 'AKI', - 'SecretAccessKey': 'SAK', - 'SessionToken': 'ST', - 'Expiration': datetime.now(tzlocal()).isoformat() - }, - } - client = mock.Mock() - client.assume_role.return_value = response - def side_effect_of_create_client(*args,**kwargs): - self.assertEqual(args[0],'sts') - self.assertEqual(kwargs['aws_access_key_id'],'access-fallback') - self.assertEqual(kwargs['aws_secret_access_key'],'secret-fallback') - self.assertEqual(kwargs['aws_session_token'],'token-fallback') - return client; - - client_creator=mock.Mock(side_effect=side_effect_of_create_client) - provider = assumerole.AssumeRoleProvider( self.create_config_loader(), - client_creator, cache={}, profile_name='development', - fallback_cred_provider=mock_fallback_provider) + mock.Mock(), cache={}, profile_name='development') - credentials=provider.load() - self.assertEqual(credentials.access_key, 'AKI') - self.assertEqual(credentials.secret_key, 'SAK') - self.assertEqual(credentials.token, 'ST') + # source_profile is required, we shoudl get an error. + with self.assertRaises(PartialCredentialsError): + provider.load() def test_source_profile_does_not_exist(self): dev_profile = self.fake_config['profiles']['development'] From b4d463f7a2c4a41bc407c83d35517aa57dcb1b13 Mon Sep 17 00:00:00 2001 From: Peter Dolberg Date: Mon, 24 Aug 2015 15:02:08 -0700 Subject: [PATCH 4/4] Adding role_session_name into cache key --- awscli/customizations/assumerole.py | 7 +++++- tests/unit/customizations/test_assumerole.py | 24 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/awscli/customizations/assumerole.py b/awscli/customizations/assumerole.py index 70b727fbd3dc..3cbbb64f1bf1 100644 --- a/awscli/customizations/assumerole.py +++ b/awscli/customizations/assumerole.py @@ -233,7 +233,12 @@ def _create_cache_key(self): # On windows, ':' is not allowed in filenames, so we'll # replace them with '_' instead. role_arn = role_config['role_arn'].replace(':', '_') - cache_key = '%s--%s' % (self._profile_name, role_arn) + role_session_name=role_config.get('role_session_name') + if role_session_name: + cache_key = '%s--%s--%s' % (self._profile_name, role_arn, role_session_name) + else: + cache_key = '%s--%s' % (self._profile_name, role_arn) + return cache_key.replace('/', '-') def _write_cached_credentials(self, creds, cache_key): diff --git a/tests/unit/customizations/test_assumerole.py b/tests/unit/customizations/test_assumerole.py index c8f2cd4d8747..ad6fdddbaa34 100644 --- a/tests/unit/customizations/test_assumerole.py +++ b/tests/unit/customizations/test_assumerole.py @@ -157,6 +157,30 @@ def test_cache_key_is_windows_safe(self): # to replace any ':' that come up. self.assertEqual(cache['development--arn_aws_iam__foo-role'], response) + + def test_cache_key_with_role_session_name(self): + response = { + 'Credentials': { + 'AccessKeyId': 'foo', + 'SecretAccessKey': 'bar', + 'SessionToken': 'baz', + 'Expiration': datetime.now(tzlocal()).isoformat() + }, + } + cache = {} + self.fake_config['profiles']['development']['role_arn'] = ( + 'arn:aws:iam::foo-role') + self.fake_config['profiles']['development']['role_session_name'] = ( + 'foo_role_session_name') + + client_creator = self.create_client_creator(with_response=response) + provider = assumerole.AssumeRoleProvider( + self.create_config_loader(), + client_creator, cache=cache, profile_name='development') + + provider.load() + self.assertEqual(cache['development--arn_aws_iam__foo-role--foo_role_session_name'], + response) def test_assume_role_in_cache_but_expired(self): expired_creds = datetime.utcnow()