diff --git a/awscli/botocore/useragent.py b/awscli/botocore/useragent.py index e1537381fc37..36e5a00d3918 100644 --- a/awscli/botocore/useragent.py +++ b/awscli/botocore/useragent.py @@ -104,6 +104,7 @@ 'FLEXIBLE_CHECKSUMS_REQ_XXHASH3': 'AG', 'FLEXIBLE_CHECKSUMS_REQ_XXHASH64': 'AH', 'FLEXIBLE_CHECKSUMS_REQ_XXHASH128': 'AI', + 'SSO_LOGIN_VANITY_URL': 'AM', } diff --git a/awscli/botocore/utils.py b/awscli/botocore/utils.py index a3ac03e745e8..3746a27597cd 100644 --- a/awscli/botocore/utils.py +++ b/awscli/botocore/utils.py @@ -78,6 +78,7 @@ UnsupportedS3ControlArnError, UnsupportedS3ControlConfigurationError, ) +from botocore.useragent import register_feature_id from dateutil.tz import tzutc from urllib3.exceptions import LocationParseError @@ -3221,11 +3222,13 @@ def __init__( cache=None, on_pending_authorization=None, time_fetcher=None, + feature_ids=None, ): self._sso_region = sso_region self._client_creator = client_creator self._parsed_globals = parsed_globals self._on_pending_authorization = on_pending_authorization + self._feature_ids = feature_ids or [] if time_fetcher is None: time_fetcher = self._utc_now @@ -3275,11 +3278,21 @@ def _client(self): signature_version=botocore.UNSIGNED, user_agent_extra=self._USER_AGENT_EXTRA, ) - return self._client_creator( + client = self._client_creator( 'sso-oidc', config=config, verify=self._parsed_globals.verify_ssl, ) + if self._feature_ids: + client.meta.events.register( + 'before-parameter-build.sso-oidc.*', + self._register_feature_ids, + ) + return client + + def _register_feature_ids(self, **kwargs): + for feature_id in self._feature_ids: + register_feature_id(feature_id) def _generate_client_name(self, session_name): if session_name is None: @@ -3330,6 +3343,7 @@ def __init__( on_pending_authorization=None, time_fetcher=None, sleep=None, + feature_ids=None, ): super().__init__( sso_region, @@ -3338,6 +3352,7 @@ def __init__( cache, on_pending_authorization, time_fetcher, + feature_ids=feature_ids, ) if sleep is None: @@ -3553,6 +3568,7 @@ def __init__( cache=None, on_pending_authorization=None, time_fetcher=None, + feature_ids=None, ): super().__init__( sso_region, @@ -3561,6 +3577,7 @@ def __init__( cache, on_pending_authorization, time_fetcher, + feature_ids=feature_ids, ) self._auth_code_fetcher = auth_code_fetcher diff --git a/awscli/customizations/agenttoolkit/agents.py b/awscli/customizations/agenttoolkit/agents.py index 4c38488f531b..7b2d13fc71f4 100644 --- a/awscli/customizations/agenttoolkit/agents.py +++ b/awscli/customizations/agenttoolkit/agents.py @@ -276,9 +276,6 @@ def __init__(self, agent, name, path): self.path = path -# TODO: Verify detection, skills, and MCP config paths against actual -# installations before release. Currently only tested with Kiro and -# simulated agent directories. AGENT_CONFIGS = [ # https://docs.anthropic.com/en/docs/claude-code/mcp AgentConfig( diff --git a/awscli/customizations/configure/sso_commands.py b/awscli/customizations/configure/sso_commands.py index cfd90a3beba8..84ad6bfe95e0 100644 --- a/awscli/customizations/configure/sso_commands.py +++ b/awscli/customizations/configure/sso_commands.py @@ -383,6 +383,7 @@ def _prompt_for_registration_args_with_legacy_format(self, verify=None): args = {'start_url': start_url, 'sso_region': sso_region} if resolved_url: args['resolved_start_url'] = resolved_url + args['feature_ids'] = ['SSO_LOGIN_VANITY_URL'] return args def _get_sso_registration_args_from_sso_config(self, sso_session): @@ -415,6 +416,7 @@ def _prompt_for_registration_args_for_new_sso_session( } if resolved_url: args['resolved_start_url'] = resolved_url + args['feature_ids'] = ['SSO_LOGIN_VANITY_URL'] return args def _store_sso_session_prompter_answers_to_profile_config(self): diff --git a/awscli/customizations/sso/login.py b/awscli/customizations/sso/login.py index 3ad71608746f..84f6faf77ff9 100644 --- a/awscli/customizations/sso/login.py +++ b/awscli/customizations/sso/login.py @@ -62,6 +62,10 @@ def _run_main(self, parsed_args, parsed_globals): verify=verify, ) + feature_ids = [] + if resolved_url != start_url: + feature_ids.append('SSO_LOGIN_VANITY_URL') + on_pending_authorization = None if parsed_args.no_browser: on_pending_authorization = PrintOnlyHandler() @@ -76,6 +80,7 @@ def _run_main(self, parsed_args, parsed_globals): session_name=sso_config.get('session_name'), registration_scopes=sso_config.get('registration_scopes'), use_device_code=parsed_args.use_device_code, + feature_ids=feature_ids, ) # Only rewrite sso_region after successful login. diff --git a/awscli/customizations/sso/utils.py b/awscli/customizations/sso/utils.py index 564203dae501..84482271f0ce 100644 --- a/awscli/customizations/sso/utils.py +++ b/awscli/customizations/sso/utils.py @@ -86,6 +86,7 @@ def do_sso_login( session_name=None, use_device_code=False, resolved_start_url=None, + feature_ids=None, ): if token_cache is None: token_cache = JSONFileCache(SSO_TOKEN_DIR, dumps_func=_sso_json_dumps) @@ -104,6 +105,7 @@ def do_sso_login( auth_code_fetcher=AuthCodeFetcher(), cache=token_cache, on_pending_authorization=on_pending_authorization, + feature_ids=feature_ids, ) else: token_fetcher = SSOTokenFetcher( @@ -112,6 +114,7 @@ def do_sso_login( parsed_globals=parsed_globals, cache=token_cache, on_pending_authorization=on_pending_authorization, + feature_ids=feature_ids, ) return token_fetcher.fetch_token( diff --git a/tests/functional/sso/test_login.py b/tests/functional/sso/test_login.py index 6f618be0ef27..894b1608d34a 100644 --- a/tests/functional/sso/test_login.py +++ b/tests/functional/sso/test_login.py @@ -560,6 +560,11 @@ def test_config_not_rewritten_when_region_matches(self): config_content = f.read() self.assertIn('sso_region=us-east-1', config_content) + def test_vanity_url_sets_feature_id_in_user_agent(self): + self.add_oidc_auth_code_responses(self.access_token) + self.run_cmd('sso login') + self.assertIn('AM', self.last_request_dict['headers']['User-Agent']) + class TestLoginWithVanityUrlLegacy(BaseSSOTest): def setUp(self): @@ -628,3 +633,9 @@ def test_login_uses_configured_region(self): self.add_oidc_auth_code_responses(self.access_token) self.run_cmd('sso login') self.assert_used_expected_sso_region('us-west-2') + + def test_direct_url_does_not_set_vanity_feature_id(self): + self.add_oidc_auth_code_responses(self.access_token) + self.run_cmd('sso login') + user_agent = self.last_request_dict['headers']['User-Agent'] + self.assertNotIn('AM', user_agent.split('m/')[-1])