feat: AAP-48498 RADIUS authenticator migrator (#7013)

* feat: AAP-48498 Radius authenticator migrator

Issue: AAP-48498

* fix: Namingm Style and tests

* enabled by default

* test: SECRET is now ignored unless --force is set
This commit is contained in:
Bruno Cesar Rocha 2025-07-16 16:00:34 +01:00 committed by thedoubl3j
parent ab9bde3698
commit abc4692231
No known key found for this signature in database
GPG Key ID: E84C42ACF75B0768
5 changed files with 118 additions and 2 deletions

View File

@ -7,6 +7,7 @@ from awx.sso.utils.github_migrator import GitHubMigrator
from awx.sso.utils.ldap_migrator import LDAPMigrator
from awx.sso.utils.oidc_migrator import OIDCMigrator
from awx.sso.utils.saml_migrator import SAMLMigrator
from awx.sso.utils.radius_migrator import RADIUSMigrator
from awx.main.utils.gateway_client import GatewayClient, GatewayAPIError
@ -18,6 +19,7 @@ class Command(BaseCommand):
parser.add_argument('--skip-ldap', action='store_true', help='Skip importing LDAP authenticators')
parser.add_argument('--skip-ad', action='store_true', help='Skip importing Azure AD authenticator')
parser.add_argument('--skip-saml', action='store_true', help='Skip importing SAML authenticator')
parser.add_argument('--skip-radius', action='store_true', help='Skip importing RADIUS authenticator')
parser.add_argument('--force', action='store_true', help='Force migration even if configurations already exist')
def handle(self, *args, **options):
@ -31,6 +33,7 @@ class Command(BaseCommand):
skip_ldap = options['skip_ldap']
skip_ad = options['skip_ad']
skip_saml = options['skip_saml']
skip_radius = options['skip_radius']
force = options['force']
# If the management command isn't called with all parameters needed to talk to Gateway, consider
@ -71,6 +74,9 @@ class Command(BaseCommand):
if not skip_ldap:
migrators.append(LDAPMigrator(gateway_client, self, force=force))
if not skip_radius:
migrators.append(RADIUSMigrator(gateway_client, self, force=force))
# Run migrations
total_results = {
'created': 0,

View File

@ -247,12 +247,12 @@ class TestAuthenticatorConfigComparison:
match, differences = self.migrator._authenticator_configs_match(existing_auth, new_config, ignore_keys)
assert match is False
assert len(differences) == 3 # KEY, SECRET, NEW_FIELD
assert len(differences) == 2 # KEY, NEW_FIELD (SECRET shows up only if --force is used)
# Check that all expected differences are captured
difference_text = ' '.join(differences)
assert 'KEY:' in difference_text
assert 'SECRET:' in difference_text
# assert 'SECRET:' in difference_text # SECRET shows up only if --force is used
assert 'NEW_FIELD:' in difference_text
assert 'CALLBACK_URL' not in difference_text # Should be ignored

View File

@ -34,6 +34,13 @@ def existing_tacacsplus_user():
return user
@pytest.fixture
def test_radius_config(settings):
settings.RADIUS_SERVER = '127.0.0.1'
settings.RADIUS_PORT = 1812
settings.RADIUS_SECRET = 'secret'
@pytest.fixture
def test_saml_config(settings):
settings.SAML_SECURITY_CONFIG = {

View File

@ -0,0 +1,17 @@
import pytest
from unittest.mock import MagicMock
from awx.sso.utils.radius_migrator import RADIUSMigrator
@pytest.mark.django_db
def test_get_controller_config(test_radius_config):
gateway_client = MagicMock()
command_obj = MagicMock()
obj = RADIUSMigrator(gateway_client, command_obj)
result = obj.get_controller_config()
config = result[0]['settings']['configuration']
assert config['SERVER'] == '127.0.0.1'
assert config['PORT'] == 1812
assert config['SECRET'] == 'secret'
assert len(config) == 3

View File

@ -0,0 +1,86 @@
"""
RADIUS authenticator migrator.
This module handles the migration of RADIUS authenticators from AWX to Gateway.
"""
from django.conf import settings
from awx.sso.utils.base_migrator import BaseAuthenticatorMigrator
class RADIUSMigrator(BaseAuthenticatorMigrator):
"""
Handles the migration of RADIUS authenticators from AWX to Gateway.
"""
CATEGORY = "RADIUS"
AUTH_TYPE = "ansible_base.authentication.authenticator_plugins.radius"
def get_authenticator_type(self):
"""Get the human-readable authenticator type name."""
return "RADIUS"
def get_controller_config(self):
"""
Export RADIUS authenticators. A RADIUS authenticator is only exported if
required configuration is present.
Returns:
list: List of configured RADIUS authentication providers with their settings
"""
server = getattr(settings, "RADIUS_SERVER", None)
if not server:
return []
port = getattr(settings, "RADIUS_PORT", 1812)
secret = getattr(settings, "RADIUS_SECRET", "")
config_data = {
"name": "default",
"type": self.AUTH_TYPE,
"enabled": True,
"create_objects": True,
"remove_users": False,
"configuration": {
"SERVER": server,
"PORT": port,
"SECRET": secret,
},
}
return [
{
"category": self.CATEGORY,
"settings": config_data,
}
]
def create_gateway_authenticator(self, config):
"""Create a RADIUS authenticator in Gateway."""
category = config["category"]
config_settings = config["settings"]
name = config_settings["name"]
# Generate authenticator name and slug
authenticator_name = f"AWX-{category.replace('-', '_').title()}-{name}"
authenticator_slug = self._generate_authenticator_slug("radius", category)
self._write_output(f"\n--- Processing {category} authenticator ---")
self._write_output(f"Name: {authenticator_name}")
self._write_output(f"Slug: {authenticator_slug}")
self._write_output(f"Type: {config_settings['type']}")
# Build Gateway authenticator configuration
gateway_config = {
"name": authenticator_name,
"slug": authenticator_slug,
"type": config_settings["type"],
"enabled": config_settings["enabled"],
"create_objects": config_settings["create_objects"],
"remove_users": config_settings["remove_users"],
"configuration": config_settings["configuration"],
}
# Submit the authenticator (create or update as needed)
return self.submit_authenticator(gateway_config, config=config)