From 994a2b3c046bd7f960eb2aedbee754fe61103383 Mon Sep 17 00:00:00 2001 From: Stevenson Michel Date: Mon, 16 Feb 2026 10:14:31 -0500 Subject: [PATCH] [Devel][AAP-65384]Restoration of Token Authentication for AWX CLI (#16281) * Added token authentication in logic, arguments, and test --- awxkit/awxkit/api/client.py | 13 +++++++- awxkit/awxkit/cli/client.py | 17 +++++++++-- awxkit/awxkit/cli/format.py | 6 ++++ awxkit/test/cli/test_authentication.py | 42 ++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/awxkit/awxkit/api/client.py b/awxkit/awxkit/api/client.py index 5d5726f339..44df4fd4eb 100644 --- a/awxkit/awxkit/api/client.py +++ b/awxkit/awxkit/api/client.py @@ -12,6 +12,15 @@ class ConnectionException(exc.Common): pass +class TokenAuth(requests.auth.AuthBase): + def __init__(self, token): + self.token = token + + def __call__(self, request): + request.headers['Authorization'] = 'Bearer {0.token}'.format(self) + return request + + def log_elapsed(r, *args, **kwargs): # requests hook to display API elapsed time log.debug('"{0.request.method} {0.url}" elapsed: {0.elapsed}'.format(r)) @@ -37,7 +46,7 @@ class Connection(object): self.get(config.api_base_path) # this causes a cookie w/ the CSRF token to be set return dict(next=next) - def login(self, username=None, password=None, **kwargs): + def login(self, username=None, password=None, token=None, **kwargs): if username and password: _next = kwargs.get('next') if _next: @@ -52,6 +61,8 @@ class Connection(object): self.uses_session_cookie = True else: self.session.auth = (username, password) + elif token: + self.session.auth = TokenAuth(token) else: self.session.auth = None diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index ff1328fbd3..cdb2134b2e 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -83,12 +83,23 @@ class CLI(object): def authenticate(self): """Configure the current session for authentication. - Uses Basic authentication when AWXKIT_FORCE_BASIC_AUTH environment variable - is set to true, otherwise defaults to session-based authentication. + Authentication priority: + 1. Token authentication (if --conf.token provided) + 2. Basic authentication (if AWXKIT_FORCE_BASIC_AUTH=true) + 3. Session-based authentication (default) + For AAP Gateway environments, set AWXKIT_FORCE_BASIC_AUTH=true to bypass - session login restrictions. + session login restrictions when using username/password. + """ + # Token authentication (if token is provided) + token = self.get_config('token') + if token: + config.use_sessions = False + self.root.connection.login(None, None, token=token) + return + # Check if Basic auth is forced via environment variable if config.get('force_basic_auth', False): config.use_sessions = False diff --git a/awxkit/awxkit/cli/format.py b/awxkit/awxkit/cli/format.py index 5e81e3295c..67aa753883 100644 --- a/awxkit/awxkit/cli/format.py +++ b/awxkit/awxkit/cli/format.py @@ -59,6 +59,12 @@ def add_authentication_arguments(parser, env): default=env.get('CONTROLLER_PASSWORD', env.get('TOWER_PASSWORD', config_password)), metavar='TEXT', ) + auth.add_argument( + '--conf.token', + default=env.get('CONTROLLER_OAUTH_TOKEN', env.get('TOWER_OAUTH_TOKEN', None)), + metavar='TEXT', + help='OAuth2 token for authentication (takes precedence over username/password)', + ) auth.add_argument( '-k', diff --git a/awxkit/test/cli/test_authentication.py b/awxkit/test/cli/test_authentication.py index f196f66043..e0b7282a9f 100644 --- a/awxkit/test/cli/test_authentication.py +++ b/awxkit/test/cli/test_authentication.py @@ -44,6 +44,48 @@ def setup_session_auth(cli_args: Optional[List[str]] = None) -> Tuple[CLI, Mock, return cli, mock_root, mock_load_session +def setup_token_auth(cli_args: Optional[List[str]] = None) -> Tuple[CLI, Mock, Mock]: + """Set up CLI with mocked connection for Token auth testing""" + cli = CLI() + cli.parse_args(cli_args or ['awx', '--conf.token', 'test-token-abc123']) + + mock_root = Mock() + mock_connection = Mock() + mock_root.connection = mock_connection + cli.root = mock_root + + return cli, mock_root, mock_connection + + +def test_token_auth_preserved(monkeypatch): + """ + REGRESSION TEST: Token authentication must still work (existed in 4.6.12) + + This test documents the customer's working scenario from 4.6.12: + awx login --conf.host URL --conf.username USER --conf.password PASS + # Returns: {"token": "E*******J"} + + awx --conf.host URL --conf.token E*******J job_templates launch ... + # This WORKED in 4.6.12 + + BREAKING CHANGE: Version 4.6.21 removed token authentication entirely, + causing customer to report: "neither token no username/password are working" + + This test will FAIL with current code and PASS once fixed. + """ + cli, mock_root, mock_connection = setup_token_auth(['awx', '--conf.host', 'https://aap-sbx.testbank.com', '--conf.token', 'E1234567890J']) + monkeypatch.setattr(config, 'force_basic_auth', False) + + # Execute authentication + cli.authenticate() + + # Token auth should call login with token parameter + mock_connection.login.assert_called_once_with(None, None, token='E1234567890J') + + # Should NOT use sessions when token is provided + assert not config.use_sessions + + def test_basic_auth_enabled(monkeypatch): """Test that AWXKIT_FORCE_BASIC_AUTH=true enables Basic authentication""" cli, mock_root, mock_connection = setup_basic_auth()