Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-login-27668.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "``login``",
"description": "Prevent ``aws login`` from updating a profile with a different style of existing credentials."
}
36 changes: 36 additions & 0 deletions awscli/customizations/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
66 changes: 66 additions & 0 deletions tests/functional/login/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pytest

from awscli.customizations.exceptions import ConfigurationError
from awscli.customizations.login.login import LoginCommand

DEFAULT_ARGS = Namespace(remote=False)
Expand Down Expand Up @@ -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()