mirror of
https://github.com/ansible/awx.git
synced 2026-06-23 07:37:50 -02:30
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* Generalize version references in compat comments
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from ansible.module_utils import basic
|
|
from ansible.module_utils.common.text.converters import to_bytes
|
|
from requests.models import Response
|
|
from unittest import mock
|
|
|
|
|
|
def getheader(self, header_name, default):
|
|
return default
|
|
|
|
|
|
def read(self):
|
|
return json.dumps({})
|
|
|
|
|
|
def status(self):
|
|
return 200
|
|
|
|
|
|
def make_recorder():
|
|
"""Build a mock for Request.open that records every call made through it."""
|
|
calls = []
|
|
|
|
def opener(self, method, url, **kwargs):
|
|
calls.append({'method': method, 'url': url, 'headers': kwargs.get('headers') or {}})
|
|
r = Response()
|
|
r.getheader = getheader.__get__(r)
|
|
r.read = read.__get__(r)
|
|
r.status = status.__get__(r)
|
|
return r
|
|
|
|
return opener, calls
|
|
|
|
|
|
def make_module(collection_import, module_args, **kwargs):
|
|
ControllerAPIModule = collection_import('plugins.module_utils.controller_api').ControllerAPIModule
|
|
cli_data = {'ANSIBLE_MODULE_ARGS': module_args}
|
|
# 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)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'token_value',
|
|
[
|
|
'a-token-string',
|
|
{'token': 'a-token-string', 'id': 1}, # the aap_token fact set by ansible.platform.token
|
|
],
|
|
ids=['string', 'dict'],
|
|
)
|
|
def test_aap_token_sends_bearer_header(collection_import, token_value):
|
|
module = make_module(collection_import, {'aap_token': token_value})
|
|
assert module.aap_token == 'a-token-string'
|
|
|
|
opener, calls = make_recorder()
|
|
with mock.patch('ansible.module_utils.urls.Request.open', new=opener):
|
|
module.get_endpoint('ping')
|
|
|
|
assert len(calls) == 1, calls
|
|
assert calls[0]['headers']['Authorization'] == 'Bearer a-token-string'
|
|
# a token needs no login round-trip
|
|
assert module.authenticated is False
|
|
|
|
|
|
@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 = []
|
|
|
|
def error_callback(**kwargs):
|
|
errors.append(kwargs)
|
|
raise SystemExit(1)
|
|
|
|
with pytest.raises(SystemExit):
|
|
make_module(collection_import, {'aap_token': {'id': 1}}, error_callback=error_callback)
|
|
|
|
assert 'did not properly contain the token entry' in errors[0]['msg']
|
|
|
|
|
|
def test_no_token_falls_back_to_basic_auth(collection_import):
|
|
module = make_module(collection_import, {'controller_username': 'admin', 'controller_password': 'secret'})
|
|
|
|
opener, calls = make_recorder()
|
|
with mock.patch('ansible.module_utils.urls.Request.open', new=opener):
|
|
module.get_endpoint('ping')
|
|
|
|
# first call is the authentication probe, second is the actual request
|
|
assert len(calls) == 2, calls
|
|
for call in calls:
|
|
assert call['headers']['Authorization'].startswith('Basic '), call
|
|
assert module.authenticated is True
|