From abc46922314155d5febc8ed3a1a6ba844a1fbdd1 Mon Sep 17 00:00:00 2001 From: Bruno Cesar Rocha Date: Wed, 16 Jul 2025 16:00:34 +0100 Subject: [PATCH] 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 --- .../commands/import_auth_config_to_gateway.py | 6 ++ .../tests/unit/utils/test_base_migrator.py | 4 +- awx/sso/tests/conftest.py | 7 ++ awx/sso/tests/unit/test_radius_migrator.py | 17 ++++ awx/sso/utils/radius_migrator.py | 86 +++++++++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 awx/sso/tests/unit/test_radius_migrator.py create mode 100644 awx/sso/utils/radius_migrator.py diff --git a/awx/main/management/commands/import_auth_config_to_gateway.py b/awx/main/management/commands/import_auth_config_to_gateway.py index 3b52439c53..8812df99e0 100644 --- a/awx/main/management/commands/import_auth_config_to_gateway.py +++ b/awx/main/management/commands/import_auth_config_to_gateway.py @@ -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, diff --git a/awx/main/tests/unit/utils/test_base_migrator.py b/awx/main/tests/unit/utils/test_base_migrator.py index 471314caba..0e2278cee1 100644 --- a/awx/main/tests/unit/utils/test_base_migrator.py +++ b/awx/main/tests/unit/utils/test_base_migrator.py @@ -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 diff --git a/awx/sso/tests/conftest.py b/awx/sso/tests/conftest.py index 5cb719808c..90566b4c68 100644 --- a/awx/sso/tests/conftest.py +++ b/awx/sso/tests/conftest.py @@ -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 = { diff --git a/awx/sso/tests/unit/test_radius_migrator.py b/awx/sso/tests/unit/test_radius_migrator.py new file mode 100644 index 0000000000..ffc1bbed36 --- /dev/null +++ b/awx/sso/tests/unit/test_radius_migrator.py @@ -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 diff --git a/awx/sso/utils/radius_migrator.py b/awx/sso/utils/radius_migrator.py new file mode 100644 index 0000000000..1e8a6f7a96 --- /dev/null +++ b/awx/sso/utils/radius_migrator.py @@ -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)