From af9ed8027a326d9dea5a5bccc1a1bf9139a59dc3 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Thu, 18 Sep 2014 22:18:37 -0700 Subject: [PATCH] The configure command writes out cred vars to shared credentials file 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. --- awscli/customizations/configure.py | 57 +++++++++++++++++ awscli/examples/configure/_description.rst | 5 ++ .../examples/configure/set/_description.rst | 5 ++ tests/unit/customizations/test_configure.py | 63 ++++++++++++++++--- 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/awscli/customizations/configure.py b/awscli/customizations/configure.py index d87256137d6c..0359a3b44416 100644 --- a/awscli/customizations/configure.py +++ b/awscli/customizations/configure.py @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/awscli/examples/configure/_description.rst b/awscli/examples/configure/_description.rst index 040dd5b88e8c..a9c01ac3f3da 100644 --- a/awscli/examples/configure/_description.rst +++ b/awscli/examples/configure/_description.rst @@ -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 ======================= diff --git a/awscli/examples/configure/set/_description.rst b/awscli/examples/configure/set/_description.rst index b4bad9e53829..b915e39680cf 100644 --- a/awscli/examples/configure/set/_description.rst +++ b/awscli/examples/configure/set/_description.rst @@ -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``). diff --git a/tests/unit/customizations/test_configure.py b/tests/unit/customizations/test_configure.py index b70276b878ea..12511139fa5f 100644 --- a/tests/unit/customizations/test_configure.py +++ b/tests/unit/customizations/test_configure.py @@ -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: @@ -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): @@ -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') @@ -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') @@ -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):