From 849f5f796cbb40f8597e99423b78f0b1b915d27c Mon Sep 17 00:00:00 2001 From: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:32:07 -0400 Subject: [PATCH] Restore oauth_token backward compatibility for collection token auth (#16500) * Restore oauth_token backward compatibility for collection token auth The aap_token rename (c8981e321e) restored module-level token auth but left two interfaces from earlier collection releases broken: - The lookup (controller_api) and inventory (controller) plugins previously declared an oauth_token option. Add oauth_token as an alias of aap_token in the auth_plugin doc fragment and in AUTH_ARGSPEC so query(..., oauth_token=...) and inventory YAML keys keep working. - tower_cli.cfg-style config files used an oauth_token key under [general]; it was silently ignored after the rename, quietly degrading auth. load_config() now also reads the legacy oauth_token key and maps it to aap_token, with the new aap_token key winning when both are present. aap_token remains the canonical attribute used by _parse_aap_token() and the Bearer header logic. Also make the test helper compatible with ansible-core 2.21+, which requires a serialization profile alongside _ANSIBLE_ARGS, and extend the tests to cover the oauth_token alias and legacy config file key. No changelog fragment added: awx_collection has no changelogs/ directory on devel. Co-Authored-By: Claude Opus 4.8 * Document oauth_token alias in module auth doc fragment The oauth_token alias was added to aap_token in AUTH_ARGSPEC but not to the module doc fragment, failing the validate-modules sanity check (undocumented argument alias). Co-Authored-By: Claude Opus 4.8 * Generalize version references in compat comments Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- awx_collection/plugins/doc_fragments/auth.py | 2 +- .../plugins/doc_fragments/auth_plugin.py | 2 +- .../plugins/module_utils/controller_api.py | 11 +++++- awx_collection/test/awx/test_token_auth.py | 37 ++++++++++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/doc_fragments/auth.py b/awx_collection/plugins/doc_fragments/auth.py index 272ca0da67..6ff2485325 100644 --- a/awx_collection/plugins/doc_fragments/auth.py +++ b/awx_collection/plugins/doc_fragments/auth.py @@ -42,7 +42,7 @@ options: - If value not set, will try environment variable C(CONTROLLER_OAUTH_TOKEN) and then config files type: raw version_added: "3.7.0" - aliases: [ controller_oauthtoken, tower_oauthtoken ] + aliases: [ oauth_token, controller_oauthtoken, tower_oauthtoken ] validate_certs: description: - Whether to allow insecure connections to AWX. diff --git a/awx_collection/plugins/doc_fragments/auth_plugin.py b/awx_collection/plugins/doc_fragments/auth_plugin.py index 846659d7a4..925452167a 100644 --- a/awx_collection/plugins/doc_fragments/auth_plugin.py +++ b/awx_collection/plugins/doc_fragments/auth_plugin.py @@ -58,7 +58,7 @@ options: why: Collection name change alternatives: 'AAP_TOKEN' - name: AAP_TOKEN - aliases: [ controller_oauthtoken, tower_oauthtoken ] + aliases: [ oauth_token, controller_oauthtoken, tower_oauthtoken ] verify_ssl: description: - Specify whether Ansible should verify the SSL certificate of the controller host. diff --git a/awx_collection/plugins/module_utils/controller_api.py b/awx_collection/plugins/module_utils/controller_api.py index 2a2d4604ac..b2d266e23d 100644 --- a/awx_collection/plugins/module_utils/controller_api.py +++ b/awx_collection/plugins/module_utils/controller_api.py @@ -99,7 +99,7 @@ class ControllerModule(AnsibleModule): aap_token=dict( type='raw', no_log=True, - aliases=['controller_oauthtoken', 'tower_oauthtoken'], + aliases=['oauth_token', 'controller_oauthtoken', 'tower_oauthtoken'], required=False, fallback=(env_fallback, ['CONTROLLER_OAUTH_TOKEN', 'TOWER_OAUTH_TOKEN', 'AAP_TOKEN']) ), @@ -298,7 +298,8 @@ class ControllerModule(AnsibleModule): # If we made it here then we have values from reading the ini file, so let's pull them out into a dict config_data = {} - for honorred_setting in self.short_params: + # 'oauth_token' is the legacy (pre-aap_token) config file key, kept for backward compatibility + for honorred_setting in list(self.short_params) + ['oauth_token']: try: config_data[honorred_setting] = config.get('general', honorred_setting) except NoOptionError: @@ -310,6 +311,12 @@ class ControllerModule(AnsibleModule): except Exception as e: raise_from(ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)), e) + # Backward compatibility: config files written for older collection + # releases used the oauth_token key; map it to aap_token. + # If both keys are present, the new aap_token key wins. + if 'oauth_token' in config_data and 'aap_token' not in config_data: + config_data['aap_token'] = config_data['oauth_token'] + # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here for honorred_setting in self.short_params: if honorred_setting in config_data: diff --git a/awx_collection/test/awx/test_token_auth.py b/awx_collection/test/awx/test_token_auth.py index 5400d6f7a5..d007ef9b49 100644 --- a/awx_collection/test/awx/test_token_auth.py +++ b/awx_collection/test/awx/test_token_auth.py @@ -45,6 +45,10 @@ def make_module(collection_import, module_args, **kwargs): # patch the cached args directly: AnsibleModule caches sys.argv parsing in # basic._ANSIBLE_ARGS, so patching sys.argv would leak args between tests with mock.patch.object(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps(cli_data))): + # ansible-core 2.21+ also requires a serialization profile alongside the args + if hasattr(basic, '_ANSIBLE_PROFILE'): + with mock.patch.object(basic, '_ANSIBLE_PROFILE', 'legacy'): + return ControllerAPIModule(argument_spec=dict(), **kwargs) return ControllerAPIModule(argument_spec=dict(), **kwargs) @@ -70,12 +74,43 @@ def test_aap_token_sends_bearer_header(collection_import, token_value): assert module.authenticated is False -@pytest.mark.parametrize('param', ['controller_oauthtoken', 'tower_oauthtoken']) +@pytest.mark.parametrize('param', ['oauth_token', 'controller_oauthtoken', 'tower_oauthtoken']) def test_aap_token_legacy_aliases(collection_import, param): module = make_module(collection_import, {param: 'legacy-token'}) assert module.aap_token == 'legacy-token' +def test_lookup_oauth_token_option_maps_to_aap_token(collection_import): + # Older lookup/inventory plugin releases pass options through as direct + # params keyed by the plugin option name; oauth_token must resolve to + # aap_token via the argspec alias. + module = make_module(collection_import, {'oauth_token': 'plugin-token'}) + assert module.aap_token == 'plugin-token' + + opener, calls = make_recorder() + with mock.patch('ansible.module_utils.urls.Request.open', new=opener): + module.get_endpoint('ping') + + assert calls[0]['headers']['Authorization'] == 'Bearer plugin-token' + + +def test_config_file_legacy_oauth_token_key(collection_import, tmp_path): + # tower_cli.cfg-style config files from older releases used the oauth_token key + config_file = tmp_path / 'tower_cli.cfg' + config_file.write_text('[general]\nhost = https://127.0.0.1\noauth_token = ini-legacy-token\n') + + module = make_module(collection_import, {'controller_config_file': str(config_file)}) + assert module.aap_token == 'ini-legacy-token' + + +def test_config_file_aap_token_wins_over_legacy_key(collection_import, tmp_path): + config_file = tmp_path / 'tower_cli.cfg' + config_file.write_text('[general]\nhost = https://127.0.0.1\noauth_token = ini-legacy-token\naap_token = ini-new-token\n') + + module = make_module(collection_import, {'controller_config_file': str(config_file)}) + assert module.aap_token == 'ini-new-token' + + def test_aap_token_dict_without_token_entry_fails(collection_import): errors = []