Skip to content

Commit

Permalink
The configure command writes out cred vars to shared credentials file
Browse files Browse the repository at this point in the history
Fixes #847.  The change is implemented as specified in the issue:

* Anytime you set credential variables (access_key/secret_key/session_token)
  using the `configure` command or the `configure set` command, the
  values are always written to `~/.aws/credentials`.
* Getting access_key/secret_key/session_token will look in the
  shared credentials file first before looking in the CLI config file.

This does *not* do anything with automatically migrating over to the
shared credentials file, that is, if you have credentials in the CLI
config file and you run `aws configure` and hit enter 4 times, we
will not write out values to the shared credentials file because
there are no new values to write out.
  • Loading branch information
jamesls committed Sep 19, 2014
1 parent 08db225 commit af9ed80
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 7 deletions.
57 changes: 57 additions & 0 deletions awscli/customizations/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,32 @@ class ConfigFileWriter(object):
)

def update_config(self, new_values, config_filename):
"""Update config file with new values.
This method will update a section in a config file with
new key value pairs.
This method provides a few conveniences:
* If the ``config_filename`` does not exist, it will
be created. Any parent directories will also be created
if necessary.
* If the section to update does not exist, it will be created.
* Any existing lines that specified by ``new_values``
**will not be touched**. This ensures that commented out
values are left unaltered.
:type new_values: dict
:param new_values: The values to update. There is a special
key ``__section__``, that specifies what section in the INI
file to update. If this key is not present, then the
``default`` section will be updated with the new values.
:type config_filename: str
:param config_filename: The config filename where values will be
written.
"""
section_name = new_values.pop('__section__', 'default')
if not os.path.isfile(config_filename):
self._create_file(config_filename)
Expand Down Expand Up @@ -336,6 +362,8 @@ class ConfigureSetCommand(BasicCommand):
'action': 'store',
'cli_type_name': 'string', 'positional_arg': True},
]
_WRITE_TO_CREDS_FILE = ['aws_access_key_id', 'aws_secret_access_key',
'aws_session_token']

def __init__(self, session, config_writer=None):
super(ConfigureSetCommand, self).__init__(session)
Expand Down Expand Up @@ -380,6 +408,12 @@ def _run_main(self, args, parsed_globals):
config_filename = os.path.expanduser(
self._session.get_config_variable('config_file'))
updated_config = {'__section__': section, varname: value}
if varname in self._WRITE_TO_CREDS_FILE:
config_filename = os.path.expanduser(
self._session.get_config_variable('credentials_file'))
section_name = updated_config['__section__']
if section_name.startswith('profile '):
updated_config['__section__'] = section_name[8:]
self._config_writer.update_config(updated_config, config_filename)


Expand Down Expand Up @@ -518,7 +552,30 @@ def _run_main(self, parsed_args, parsed_globals):
config_filename = os.path.expanduser(
self._session.get_config_variable('config_file'))
if new_values:
self._write_out_creds_file_values(new_values,
parsed_globals.profile)
if parsed_globals.profile is not None:
new_values['__section__'] = (
'profile %s' % parsed_globals.profile)
self._config_writer.update_config(new_values, config_filename)

def _write_out_creds_file_values(self, new_values, profile_name):
# The access_key/secret_key are now *always* written to the shared
# credentials file (~/.aws/credentials), see aws/aws-cli#847.
# post-conditions: ~/.aws/credentials will have the updated credential
# file values and new_values will have the cred vars removed.
credential_file_values = {}
if 'aws_access_key_id' in new_values:
credential_file_values['aws_access_key_id'] = new_values.pop(
'aws_access_key_id')
if 'aws_secret_access_key' in new_values:
credential_file_values['aws_secret_access_key'] = new_values.pop(
'aws_secret_access_key')
if credential_file_values:
if profile_name is not None:
credential_file_values['__section__'] = profile_name
shared_credentials_filename = self._session.get_config_variable(
'credentials_file')
self._config_writer.update_config(
credential_file_values,
shared_credentials_filename)
5 changes: 5 additions & 0 deletions awscli/examples/configure/_description.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ When you are prompted for information, the current value will be displayed in
config file. It does not use any configuration values from environment
variables or the IAM role.

Note: the values you provide for the AWS Access Key ID and the AWS Secret
Access Key will be written to the shared credentials file
(``~/.aws/credentials``).


=======================
Configuration Variables
=======================
Expand Down
5 changes: 5 additions & 0 deletions awscli/examples/configure/set/_description.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ configuration value.
If the config file does not exist, one will automatically be created. If the
configuration value already exists in the config file, it will updated with the
new configuration value.

Setting a value for the ``aws_access_key_id``, ``aws_secret_access_key``, or
the ``aws_session_token`` will result in the value being writen to the
shared credentials file (``~/.aws/credentials``). All other values will
be written to the config file (default location is ``~/.aws/config``).
63 changes: 56 additions & 7 deletions tests/unit/customizations/test_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def get_scoped_config(self):
return self.config

def get_config_variable(self, name, methods=None):
if name == 'credentials_file':
# The credentials_file var doesn't require a
# profile to exist.
return 'fake_credentials_file'
if self.profile_does_not_exist and not name == 'config_file':
raise ProfileNotFound(profile='foo')
if methods is not None:
Expand Down Expand Up @@ -98,12 +102,22 @@ def setUp(self):
prompter=self.precanned,
config_writer=self.writer)

def assert_credentials_file_updated_with(self, new_values):
called_args = self.writer.update_config.call_args_list
credentials_file_call = called_args[0]
self.assertEqual(credentials_file_call,
mock.call(new_values, 'fake_credentials_file'))

def test_configure_command_sends_values_to_writer(self):
self.configure(args=[], parsed_globals=self.global_args)
self.writer.update_config.assert_called_with(
# Credentials are always written to the shared credentials file.
self.assert_credentials_file_updated_with(
{'aws_access_key_id': 'new_value',
'aws_secret_access_key': 'new_value',
'region': 'new_value',
'aws_secret_access_key': 'new_value'})

# Non-credentials config is written to the config file.
self.writer.update_config.assert_called_with(
{'region': 'new_value',
'output': 'new_value'}, 'myconfigfile')

def test_same_values_are_not_changed(self):
Expand Down Expand Up @@ -154,10 +168,12 @@ def test_section_name_can_be_changed_for_profiles(self):
self.global_args.profile = 'myname'
self.configure(args=[], parsed_globals=self.global_args)
# Note the __section__ key name.
self.assert_credentials_file_updated_with(
{'aws_access_key_id': 'new_value',
'aws_secret_access_key': 'new_value',
'__section__': 'myname'})
self.writer.update_config.assert_called_with(
{'__section__': 'profile myname',
'aws_access_key_id': 'new_value',
'aws_secret_access_key': 'new_value',
'region': 'new_value',
'output': 'new_value'}, 'myconfigfile')

Expand All @@ -173,10 +189,12 @@ def test_session_says_profile_does_not_exist(self):
config_writer=self.writer)
self.global_args.profile = 'profile-does-not-exist'
self.configure(args=[], parsed_globals=self.global_args)
self.assert_credentials_file_updated_with(
{'aws_access_key_id': 'new_value',
'aws_secret_access_key': 'new_value',
'__section__': 'profile-does-not-exist'})
self.writer.update_config.assert_called_with(
{'__section__': 'profile profile-does-not-exist',
'aws_access_key_id': 'new_value',
'aws_secret_access_key': 'new_value',
'region': 'new_value',
'output': 'new_value'}, 'myconfigfile')

Expand Down Expand Up @@ -760,6 +778,37 @@ def test_configure_set_with_profile_nested(self):
{'__section__': 'profile foo',
's3': {'signature_version': 's3v4'}}, 'myconfigfile')

def test_access_key_written_to_shared_credentials_file(self):
set_command = configure.ConfigureSetCommand(self.session, self.config_writer)
set_command(args=['aws_access_key_id', 'foo'],
parsed_globals=None)
self.config_writer.update_config.assert_called_with(
{'__section__': 'default',
'aws_access_key_id': 'foo'}, 'fake_credentials_file')

def test_secret_key_written_to_shared_credentials_file(self):
set_command = configure.ConfigureSetCommand(self.session, self.config_writer)
set_command(args=['aws_secret_access_key', 'foo'],
parsed_globals=None)
self.config_writer.update_config.assert_called_with(
{'__section__': 'default',
'aws_secret_access_key': 'foo'}, 'fake_credentials_file')

def test_session_token_written_to_shared_credentials_file(self):
set_command = configure.ConfigureSetCommand(self.session, self.config_writer)
set_command(args=['aws_session_token', 'foo'],
parsed_globals=None)
self.config_writer.update_config.assert_called_with(
{'__section__': 'default',
'aws_session_token': 'foo'}, 'fake_credentials_file')

def test_access_key_written_to_shared_credentials_file_profile(self):
set_command = configure.ConfigureSetCommand(self.session, self.config_writer)
set_command(args=['profile.foo.aws_access_key_id', 'bar'],
parsed_globals=None)
self.config_writer.update_config.assert_called_with(
{'__section__': 'foo',
'aws_access_key_id': 'bar'}, 'fake_credentials_file')

class TestConfigValueMasking(unittest.TestCase):
def test_config_value_is_masked(self):
Expand Down

0 comments on commit af9ed80

Please sign in to comment.