Make aap_token functional for collection token auth (#16498)

The aap_token parameter was added to the collection argspec and docs
in #16025, but nothing consumed it after token auth was removed in
#15623: modules silently ignored the token and fell back to basic
auth, breaking token authentication through the AAP gateway.

Wire it up so requests authenticate with the provided token (e.g. one
issued by the AAP gateway, which validates it and proxies to the
controller):

- Send "Authorization: Bearer <token>" in make_request when aap_token
  is set, skipping the basic-auth login probe; basic auth is unchanged
  when no token is given
- Accept the token as a string or as the dict set as a fact by the
  ansible.platform.token module ({token: ..., id: ...}), which is the
  documented cross-collection mint/use/delete workflow
- Restore controller_oauthtoken and tower_oauthtoken as aliases for
  back-compat with pre-#15623 playbooks, matching downstream
- Forward aap_token through the controller_api lookup and controller
  inventory plugins via short_params, and add the missing
  CONTROLLER_OAUTH_TOKEN/TOWER_OAUTH_TOKEN env sources to the plugin
  doc fragment (plugins resolve env vars from doc fragments, not
  env_fallback); AAP_TOKEN is no longer marked deprecated there
- Support tokens in the awxkit-based export/import modules
- Add unit tests covering the Bearer header for both token forms, the
  aliases, the bad-dict failure, and the basic-auth fallback

Verified end-to-end against a live gateway-fronted AAP 2.7 deployment:
modules, the lookup plugin, both aliases, all env sources, dict-form
tokens, job launch/wait, and a clean HTTP 401 on an invalid token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hao Liu
2026-06-10 18:18:25 -04:00
committed by GitHub
parent d5e5ea3670
commit c8981e321e
5 changed files with 151 additions and 11 deletions

View File

@@ -0,0 +1,103 @@
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))):
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', ['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_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