[AAP-46830]: Fix AWX CLI authentication with AAP Gateway environments (#16113)

* migrate pr #16081 to new fork

* add test coverage - add clearer error messaging

* update tests to use monkeypatch
This commit is contained in:
Andrea Restle-Lay 2025-10-02 10:06:10 -04:00 committed by GitHub
parent f51af03424
commit 0d18308112
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 136 additions and 1 deletions

View File

@ -82,7 +82,38 @@ class CLI(object):
return '--help' in self.argv or '-h' in self.argv
def authenticate(self):
"""Configure the current session for basic auth"""
"""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.
For AAP Gateway environments, set AWXKIT_FORCE_BASIC_AUTH=true to bypass
session login restrictions.
"""
# Check if Basic auth is forced via environment variable
if config.get('force_basic_auth', False):
config.use_sessions = False
# Validate credentials are provided
username = self.get_config('username')
password = self.get_config('password')
if not username or not password:
raise ValueError(
"Basic authentication requires both username and password. "
"Provide --conf.username and --conf.password or set "
"CONTROLLER_USERNAME and CONTROLLER_PASSWORD environment variables."
)
# Apply Basic auth credentials to the session
try:
self.root.connection.login(username, password)
self.root.get()
except Exception as e:
raise RuntimeError(f"Basic authentication failed: {str(e)}. " "Verify credentials and network connectivity.") from e
return
# Use session-based authentication (default)
config.use_sessions = True
self.root.load_session().get()

View File

@ -32,6 +32,7 @@ config.assume_untrusted = config.get('assume_untrusted', True)
config.client_connection_attempts = int(os.getenv('AWXKIT_CLIENT_CONNECTION_ATTEMPTS', 5))
config.prevent_teardown = to_bool(os.getenv('AWXKIT_PREVENT_TEARDOWN', False))
config.use_sessions = to_bool(os.getenv('AWXKIT_SESSIONS', False))
config.force_basic_auth = to_bool(os.getenv('AWXKIT_FORCE_BASIC_AUTH', False))
config.api_base_path = os.getenv('CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX', '/api/')
config.api_base_path = os.getenv('AWXKIT_API_BASE_PATH', config.api_base_path)
config.gateway_base_path = os.getenv('AWXKIT_GATEWAY_BASE_PATH', '/api/gateway/')

View File

@ -0,0 +1,103 @@
import pytest
from typing import Tuple, List, Optional
from unittest.mock import Mock
from awxkit.cli import CLI
from awxkit import config
@pytest.fixture(autouse=True)
def reset_config_state(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ensure clean config state for each test to prevent parallel test interference"""
monkeypatch.setattr(config, 'force_basic_auth', False, raising=False)
monkeypatch.setattr(config, 'use_sessions', False, raising=False)
# ============================================================================
# Test Helper Functions
# ============================================================================
def setup_basic_auth(cli_args: Optional[List[str]] = None) -> Tuple[CLI, Mock, Mock]:
"""Set up CLI with mocked connection for Basic auth testing"""
cli = CLI()
cli.parse_args(cli_args or ['awx', '--conf.username', 'testuser', '--conf.password', 'testpass'])
mock_root = Mock()
mock_connection = Mock()
mock_root.connection = mock_connection
cli.root = mock_root
return cli, mock_root, mock_connection
def setup_session_auth(cli_args: Optional[List[str]] = None) -> Tuple[CLI, Mock, Mock]:
"""Set up CLI with mocked session for Session auth testing"""
cli = CLI()
cli.parse_args(cli_args or ['awx', '--conf.username', 'testuser', '--conf.password', 'testpass'])
mock_root = Mock()
mock_load_session = Mock()
mock_root.load_session.return_value = mock_load_session
cli.root = mock_root
return cli, mock_root, mock_load_session
def test_basic_auth_enabled(monkeypatch):
"""Test that AWXKIT_FORCE_BASIC_AUTH=true enables Basic authentication"""
cli, mock_root, mock_connection = setup_basic_auth()
monkeypatch.setattr(config, 'force_basic_auth', True)
cli.authenticate()
mock_connection.login.assert_called_once_with('testuser', 'testpass')
mock_root.get.assert_called_once()
assert not config.use_sessions
def test_session_auth_default(monkeypatch):
"""Test that session auth is used by default (backward compatibility)"""
cli, mock_root, mock_load_session = setup_session_auth()
monkeypatch.setattr(config, 'force_basic_auth', False)
cli.authenticate()
mock_root.load_session.assert_called_once()
mock_load_session.get.assert_called_once()
assert config.use_sessions
def test_aap_gateway_scenario(monkeypatch):
"""Test the specific AAP Gateway scenario from AAP-46830"""
cli, mock_root, mock_connection = setup_basic_auth(
['awx', '--conf.host', 'https://aap-sbx.cambiahealth.com', '--conf.username', 'puretest', '--conf.password', 'testpass']
)
monkeypatch.setattr(config, 'force_basic_auth', True)
cli.authenticate()
mock_connection.login.assert_called_once_with('puretest', 'testpass')
mock_root.get.assert_called_once()
assert not config.use_sessions
def test_empty_credentials_error(monkeypatch):
"""Test error handling for explicitly empty credentials"""
cli, mock_root, mock_connection = setup_basic_auth(['awx', '--conf.username', '', '--conf.password', ''])
monkeypatch.setattr(config, 'force_basic_auth', True)
with pytest.raises(ValueError, match="Basic authentication requires both username and password"):
cli.authenticate()
mock_connection.login.assert_not_called()
def test_connection_failure(monkeypatch):
"""Test error handling when Basic auth connection fails"""
cli, mock_root, mock_connection = setup_basic_auth()
mock_connection.login.side_effect = Exception("Connection failed")
monkeypatch.setattr(config, 'force_basic_auth', True)
with pytest.raises(RuntimeError, match="Basic authentication failed: Connection failed"):
cli.authenticate()
mock_connection.login.assert_called_once_with('testuser', 'testpass')
assert not config.use_sessions