Skip to content

Commit de34039

Browse files
committed
Add a prompt to 'aws login' to warn users when updating a profile with existing credentials
1 parent ec56d0e commit de34039

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "``login``",
4+
"description": "Add a prompt to ``aws login`` to warn users when updating a profile with existing credentials."
5+
}

awscli/customizations/login/login.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ def _run_main(self, parsed_args, parsed_globals):
9696
if profile_name not in self._session.available_profiles:
9797
self._session._profile_map[profile_name] = {}
9898

99+
if not self.accept_existing_credentials_warning_if_needed(
100+
profile_name
101+
):
102+
return
103+
99104
config = botocore.config.Config(
100105
region_name=region,
101106
signature_version=botocore.UNSIGNED,
@@ -177,6 +182,49 @@ def accept_change_to_existing_profile_if_needed(
177182
else:
178183
uni_print('Invalid response. Please enter "y" or "n"')
179184

185+
def accept_existing_credentials_warning_if_needed(self, profile_name):
186+
"""
187+
Checks if the specified profile is already configured with a
188+
different style of credentials. If so, warn the user and prompt them to
189+
continue.
190+
"""
191+
config = self._session.full_config['profiles'].get(profile_name, {})
192+
existing_credentials_style = None
193+
194+
if 'web_identity_token_file' in config:
195+
existing_credentials_style = 'Web Identity'
196+
elif 'sso_role_name' in config or 'sso_account_id' in config:
197+
existing_credentials_style = 'SSO'
198+
elif 'aws_access_key_id' in config:
199+
existing_credentials_style = 'Access Key'
200+
elif 'role_arn' in config:
201+
existing_credentials_style = 'Assume Role'
202+
elif 'credential_process' in config:
203+
existing_credentials_style = 'Credential Process'
204+
205+
if not existing_credentials_style:
206+
return True
207+
208+
while True:
209+
response = compat_input(
210+
f'\nWarning: Profile \'{profile_name}\' is already configured '
211+
f'with {existing_credentials_style} credentials. '
212+
f'If you continue to log in, the CLI and other tools may '
213+
f'continue to use the existing credentials instead.\n\n'
214+
f'You may run \'aws login --profile new-profile-name\' to '
215+
f'create a new profile, or else you may manually remove the '
216+
f'existing credentials from \'{profile_name}\'.\n\n'
217+
f'Do you want to continue adding login credentials '
218+
f'to \'{profile_name}\'? (y/n): '
219+
)
220+
221+
if response.lower() in ('y', 'yes'):
222+
return True
223+
elif response.lower() in ('n', 'no'):
224+
return False
225+
else:
226+
uni_print('Invalid response. Please enter "y" or "n"')
227+
180228
@staticmethod
181229
def resolve_sign_in_type(parsed_args):
182230
if parsed_args.remote:

tests/functional/login/test_login.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,104 @@ def test_new_profile_without_region(
230230
},
231231
'configfile',
232232
)
233+
234+
235+
@pytest.mark.parametrize(
236+
'profile_config,expect_prompt_to_be_called',
237+
[
238+
pytest.param({}, False, id="Empty profile"),
239+
pytest.param(
240+
{'login_session': 'arn:aws:iam::0123456789012:user/Admin'},
241+
False,
242+
id="Existing login profile",
243+
),
244+
pytest.param(
245+
{'web_identity_token_file': '/path'},
246+
True,
247+
id="Web Identity Token profile",
248+
),
249+
pytest.param({'sso_role_name': 'role'}, True, id="SSO profile"),
250+
pytest.param(
251+
{'aws_access_key_id': 'AKIAIOSFODNN7EXAMPLE'},
252+
True,
253+
id="IAM access key profile",
254+
),
255+
pytest.param(
256+
{'role_arn': 'arn:aws:iam::123456789012:role/MyRole'},
257+
True,
258+
id="Assume role profile",
259+
),
260+
pytest.param(
261+
{'credential_process': '/path/to/credential/process'},
262+
True,
263+
id="Credential process profile",
264+
),
265+
],
266+
)
267+
@mock.patch('awscli.customizations.login.login.compat_input', return_value='y')
268+
@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri')
269+
@mock.patch(
270+
'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token'
271+
)
272+
def test_accept_change_to_existing_profile_if_needed(
273+
mock_token_fetcher,
274+
mock_base_sign_in_uri,
275+
mock_input,
276+
mock_login_command,
277+
mock_session,
278+
profile_config,
279+
expect_prompt_to_be_called,
280+
):
281+
mock_base_sign_in_uri.return_value = 'https://foo'
282+
mock_token_fetcher.return_value = (
283+
{
284+
'accessToken': 'access_token',
285+
'idToken': SAMPLE_ID_TOKEN,
286+
'expiresIn': 3600,
287+
},
288+
'arn:aws:iam::0123456789012:user/Admin',
289+
)
290+
mock_session.full_config = {'profiles': {'profile-name': profile_config}}
291+
292+
mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)
293+
mock_token_fetcher.assert_called_once()
294+
295+
assert mock_input.called == expect_prompt_to_be_called
296+
297+
298+
@mock.patch('awscli.customizations.login.login.compat_input', return_value='n')
299+
@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri')
300+
@mock.patch(
301+
'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token'
302+
)
303+
def test_decline_change_to_existing_profile_does_not_update(
304+
mock_token_fetcher,
305+
mock_base_sign_in_uri,
306+
mock_input,
307+
mock_login_command,
308+
mock_session,
309+
mock_config_file_writer,
310+
mock_token_loader,
311+
):
312+
mock_base_sign_in_uri.return_value = 'https://foo'
313+
mock_token_fetcher.return_value = (
314+
{
315+
'accessToken': 'access_token',
316+
'idToken': SAMPLE_ID_TOKEN,
317+
'expiresIn': 3600,
318+
},
319+
'arn:aws:iam::0123456789012:user/Admin',
320+
)
321+
mock_session.full_config = {
322+
'profiles': {
323+
'profile-name': {'aws_access_key_id': 'AKIAIOSFODNN7EXAMPLE'}
324+
}
325+
}
326+
327+
mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)
328+
329+
# Because we mocked 'n' to compat_input above, we don't expect the command
330+
# to have finished when the user declines the existing credential prompt
331+
mock_input.assert_called_once()
332+
mock_token_loader.save_token.assert_not_called()
333+
mock_config_file_writer.update_config.assert_not_called()

0 commit comments

Comments
 (0)