Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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": "Add a prompt to ``aws login`` to warn users when updating a profile with existing credentials."
}
48 changes: 48 additions & 0 deletions awscli/customizations/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ def _run_main(self, parsed_args, parsed_globals):
if profile_name not in self._session.available_profiles:
self._session._profile_map[profile_name] = {}

if not self.accept_existing_credentials_warning_if_needed(
profile_name
):
return

config = botocore.config.Config(
region_name=region,
signature_version=botocore.UNSIGNED,
Expand Down Expand Up @@ -177,6 +182,49 @@ def accept_change_to_existing_profile_if_needed(
else:
uni_print('Invalid response. Please enter "y" or "n"')

def accept_existing_credentials_warning_if_needed(self, profile_name):
"""
Checks if the specified profile is already configured with a
different style of credentials. If so, warn the user and prompt them to
continue.
"""
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 not existing_credentials_style:
return True

while True:
response = compat_input(
f'\nWarning: Profile \'{profile_name}\' is already configured '
f'with {existing_credentials_style} credentials. '
f'If you continue to log in, the CLI and other tools may '
f'continue to use the existing credentials instead.\n\n'
f'You may run \'aws login --profile new-profile-name\' to '
f'create a new profile, or else you may manually remove the '
f'existing credentials from \'{profile_name}\'.\n\n'
f'Do you want to continue adding login credentials '
f'to \'{profile_name}\'? (y/n): '
)

if response.lower() in ('y', 'yes'):
return True
elif response.lower() in ('n', 'no'):
return False
else:
uni_print('Invalid response. Please enter "y" or "n"')

@staticmethod
def resolve_sign_in_type(parsed_args):
if parsed_args.remote:
Expand Down
101 changes: 101 additions & 0 deletions tests/functional/login/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,104 @@ def test_new_profile_without_region(
},
'configfile',
)


@pytest.mark.parametrize(
'profile_config,expect_prompt_to_be_called',
[
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.login.compat_input', return_value='y')
@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri')
@mock.patch(
'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token'
)
def test_accept_change_to_existing_profile_if_needed(
mock_token_fetcher,
mock_base_sign_in_uri,
mock_input,
mock_login_command,
mock_session,
profile_config,
expect_prompt_to_be_called,
):
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}}

mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)
mock_token_fetcher.assert_called_once()

assert mock_input.called == expect_prompt_to_be_called


@mock.patch('awscli.customizations.login.login.compat_input', return_value='n')
@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri')
@mock.patch(
'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token'
)
def test_decline_change_to_existing_profile_does_not_update(
mock_token_fetcher,
mock_base_sign_in_uri,
mock_input,
mock_login_command,
mock_session,
mock_config_file_writer,
mock_token_loader,
):
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': {'aws_access_key_id': 'AKIAIOSFODNN7EXAMPLE'}
}
}

mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)

# Because we mocked 'n' to compat_input above, we don't expect the command
# to have finished when the user declines the existing credential prompt
mock_input.assert_called_once()
mock_token_loader.save_token.assert_not_called()
mock_config_file_writer.update_config.assert_not_called()