diff --git a/.changes/next-release/enhancement-login-27668.json b/.changes/next-release/enhancement-login-27668.json new file mode 100644 index 000000000000..d34fab79d458 --- /dev/null +++ b/.changes/next-release/enhancement-login-27668.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "``login``", + "description": "Prevent ``aws login`` from updating a profile with a different style of existing credentials." +} diff --git a/awscli/customizations/login/login.py b/awscli/customizations/login/login.py index 4959b6d9c104..f102ab2ae795 100644 --- a/awscli/customizations/login/login.py +++ b/awscli/customizations/login/login.py @@ -20,6 +20,7 @@ RequiredInputValidator, ) from awscli.customizations.configure.writer import ConfigFileWriter +from awscli.customizations.exceptions import ConfigurationError from awscli.customizations.login.utils import ( CrossDeviceLoginTokenFetcher, LoginType, @@ -96,6 +97,10 @@ def _run_main(self, parsed_args, parsed_globals): if profile_name not in self._session.available_profiles: self._session._profile_map[profile_name] = {} + # Abort if the profile is already configured with a different style + # of credentials, since they'd still have precedence over login + self.ensure_profile_does_not_have_existing_credentials(profile_name) + config = botocore.config.Config( region_name=region, signature_version=botocore.UNSIGNED, @@ -177,6 +182,37 @@ def accept_change_to_existing_profile_if_needed( else: uni_print('Invalid response. Please enter "y" or "n"') + def ensure_profile_does_not_have_existing_credentials(self, profile_name): + """ + Raises an error if the specified profile is already + configured with a different style of credentials. + """ + config = self._session.full_config['profiles'].get(profile_name, {}) + existing_credentials_style = None + + if 'web_identity_token_file' in config: + existing_credentials_style = 'Web Identity' + elif 'sso_role_name' in config or 'sso_account_id' in config: + existing_credentials_style = 'SSO' + elif 'aws_access_key_id' in config: + existing_credentials_style = 'Access Key' + elif 'role_arn' in config: + existing_credentials_style = 'Assume Role' + elif 'credential_process' in config: + existing_credentials_style = 'Credential Process' + + if existing_credentials_style: + raise ConfigurationError( + f'Profile \'{profile_name}\' is already configured ' + f'with {existing_credentials_style} credentials.\n\n' + f'You may run \'aws login --profile new-profile-name\' to ' + f'create a new profile with the specified name. Otherwise you ' + f'must first manually remove the existing credentials ' + f'from \'{profile_name}\'.\n' + ) + + return False + @staticmethod def resolve_sign_in_type(parsed_args): if parsed_args.remote: diff --git a/tests/functional/login/test_login.py b/tests/functional/login/test_login.py index 29c870b72e51..a89fa0a31670 100644 --- a/tests/functional/login/test_login.py +++ b/tests/functional/login/test_login.py @@ -6,6 +6,7 @@ import pytest +from awscli.customizations.exceptions import ConfigurationError from awscli.customizations.login.login import LoginCommand DEFAULT_ARGS = Namespace(remote=False) @@ -230,3 +231,68 @@ def test_new_profile_without_region( }, 'configfile', ) + + +@pytest.mark.parametrize( + 'profile_config,expected_to_abort', + [ + pytest.param({}, False, id="Empty profile"), + pytest.param( + {'login_session': 'arn:aws:iam::0123456789012:user/Admin'}, + False, + id="Existing login profile", + ), + pytest.param( + {'web_identity_token_file': '/path'}, + True, + id="Web Identity Token profile", + ), + pytest.param({'sso_role_name': 'role'}, True, id="SSO profile"), + pytest.param( + {'aws_access_key_id': 'AKIAIOSFODNN7EXAMPLE'}, + True, + id="IAM access key profile", + ), + pytest.param( + {'role_arn': 'arn:aws:iam::123456789012:role/MyRole'}, + True, + id="Assume role profile", + ), + pytest.param( + {'credential_process': '/path/to/credential/process'}, + True, + id="Credential process profile", + ), + ], +) +@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri') +@mock.patch( + 'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token' +) +def test_abort_if_profile_has_existing_credentials( + mock_token_fetcher, + mock_base_sign_in_uri, + mock_login_command, + mock_session, + mock_token_loader, + profile_config, + expected_to_abort, +): + mock_base_sign_in_uri.return_value = 'https://foo' + mock_token_fetcher.return_value = ( + { + 'accessToken': 'access_token', + 'idToken': SAMPLE_ID_TOKEN, + 'expiresIn': 3600, + }, + 'arn:aws:iam::0123456789012:user/Admin', + ) + mock_session.full_config = {'profiles': {'profile-name': profile_config}} + + if expected_to_abort: + with pytest.raises(ConfigurationError): + mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS) + mock_token_fetcher.assert_not_called() + else: + mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS) + mock_token_fetcher.assert_called_once()