feat: Add migrator for Google OAuth2 authenticator (#7018)

Signed-off-by: Fabricio Aguiar <fabricio.aguiar@gmail.com>
This commit is contained in:
Fabricio Aguiar
2025-07-18 14:49:39 +01:00
committed by thedoubl3j
parent e746589019
commit 8e58fee49c
4 changed files with 191 additions and 21 deletions

View File

@@ -9,6 +9,7 @@ from awx.sso.utils.oidc_migrator import OIDCMigrator
from awx.sso.utils.saml_migrator import SAMLMigrator from awx.sso.utils.saml_migrator import SAMLMigrator
from awx.sso.utils.radius_migrator import RADIUSMigrator from awx.sso.utils.radius_migrator import RADIUSMigrator
from awx.sso.utils.tacacs_migrator import TACACSMigrator from awx.sso.utils.tacacs_migrator import TACACSMigrator
from awx.sso.utils.google_oauth2_migrator import GoogleOAuth2Migrator
from awx.main.utils.gateway_client import GatewayClient, GatewayAPIError from awx.main.utils.gateway_client import GatewayClient, GatewayAPIError
@@ -22,6 +23,7 @@ class Command(BaseCommand):
parser.add_argument('--skip-saml', action='store_true', help='Skip importing SAML 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('--skip-radius', action='store_true', help='Skip importing RADIUS authenticator')
parser.add_argument('--skip-tacacs', action='store_true', help='Skip importing TACACS+ authenticator') parser.add_argument('--skip-tacacs', action='store_true', help='Skip importing TACACS+ authenticator')
parser.add_argument('--skip-google', action='store_true', help='Skip importing Google OAuth2 authenticator')
parser.add_argument('--force', action='store_true', help='Force migration even if configurations already exist') parser.add_argument('--force', action='store_true', help='Force migration even if configurations already exist')
def handle(self, *args, **options): def handle(self, *args, **options):
@@ -37,6 +39,7 @@ class Command(BaseCommand):
skip_saml = options['skip_saml'] skip_saml = options['skip_saml']
skip_radius = options['skip_radius'] skip_radius = options['skip_radius']
skip_tacacs = options['skip_tacacs'] skip_tacacs = options['skip_tacacs']
skip_google = options['skip_google']
force = options['force'] force = options['force']
# If the management command isn't called with all parameters needed to talk to Gateway, consider # If the management command isn't called with all parameters needed to talk to Gateway, consider
@@ -83,6 +86,9 @@ class Command(BaseCommand):
if not skip_tacacs: if not skip_tacacs:
migrators.append(TACACSMigrator(gateway_client, self, force=force)) migrators.append(TACACSMigrator(gateway_client, self, force=force))
if not skip_google:
migrators.append(GoogleOAuth2Migrator(gateway_client, self, force=force))
# Run migrations # Run migrations
total_results = { total_results = {
'created': 0, 'created': 0,

View File

@@ -0,0 +1,104 @@
import pytest
from unittest.mock import MagicMock
from awx.sso.utils.google_oauth2_migrator import GoogleOAuth2Migrator
@pytest.fixture
def test_google_config(settings):
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = "test_key"
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = "test_secret"
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL = "https://tower.example.com/sso/complete/google-oauth2/"
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {"My Org": {"users": True}}
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {"My Team": {"organization": "My Org", "users": True}}
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ["profile", "email"]
@pytest.mark.django_db
def test_get_controller_config(test_google_config):
gateway_client = MagicMock()
command_obj = MagicMock()
obj = GoogleOAuth2Migrator(gateway_client, command_obj)
result = obj.get_controller_config()
assert len(result) == 1
config = result[0]
assert config['category'] == 'Google OAuth2'
settings = config['settings']
assert settings['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'] == 'test_key'
assert settings['SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'] == 'test_secret'
assert settings['SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL'] == "https://tower.example.com/sso/complete/google-oauth2/"
assert settings['SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE'] == ["profile", "email"]
# Assert that other settings are not present in the returned config
assert 'SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP' not in settings
assert 'SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP' not in settings
@pytest.mark.django_db
def test_create_gateway_authenticator(mocker, test_google_config):
mocker.patch('django.conf.settings.LOGGING', {})
gateway_client = MagicMock()
command_obj = MagicMock()
obj = GoogleOAuth2Migrator(gateway_client, command_obj)
mock_submit = MagicMock(return_value=True)
obj.submit_authenticator = mock_submit
configs = obj.get_controller_config()
result = obj.create_gateway_authenticator(configs[0])
assert result is True
mock_submit.assert_called_once()
# Assert payload sent to gateway
payload = mock_submit.call_args[0][0]
assert payload['name'] == 'google'
assert payload['slug'] == 'aap-google-oauth2-google-oauth2'
assert payload['type'] == 'ansible_base.authentication.authenticator_plugins.google_oauth2'
assert payload['enabled'] is True
assert payload['create_objects'] is True
assert payload['remove_users'] is False
# Assert configuration details
configuration = payload['configuration']
assert configuration['KEY'] == 'test_key'
assert configuration['SECRET'] == 'test_secret'
assert configuration['CALLBACK_URL'] == 'https://tower.example.com/sso/complete/google-oauth2/'
assert configuration['SCOPE'] == ['profile', 'email']
# Assert mappers
assert len(payload['mappers']) == 2
assert payload['mappers'][0]['map_type'] == 'organization'
assert payload['mappers'][1]['map_type'] == 'team'
# Assert ignore_keys
ignore_keys = mock_submit.call_args[0][1]
assert ignore_keys == ["ACCESS_TOKEN_METHOD", "REVOKE_TOKEN_METHOD"]
@pytest.mark.django_db
def test_create_gateway_authenticator_no_optional_values(mocker, settings):
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = "test_key"
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = "test_secret"
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = None
settings.SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL = None
mocker.patch('django.conf.settings.LOGGING', {})
gateway_client = MagicMock()
command_obj = MagicMock()
obj = GoogleOAuth2Migrator(gateway_client, command_obj)
mock_submit = MagicMock(return_value=True)
obj.submit_authenticator = mock_submit
configs = obj.get_controller_config()
obj.create_gateway_authenticator(configs[0])
payload = mock_submit.call_args[0][0]
assert 'CALLBACK_URL' not in payload['configuration']
assert 'SCOPE' not in payload['configuration']
ignore_keys = mock_submit.call_args[0][1]
assert 'CALLBACK_URL' in ignore_keys
assert 'SCOPE' in ignore_keys

View File

@@ -0,0 +1,17 @@
from awx.sso.utils.azure_ad_migrator import AzureADMigrator
from awx.sso.utils.github_migrator import GitHubMigrator
from awx.sso.utils.google_oauth2_migrator import GoogleOAuth2Migrator
from awx.sso.utils.ldap_migrator import LDAPMigrator
from awx.sso.utils.oidc_migrator import OIDCMigrator
from awx.sso.utils.radius_migrator import RADIUSMigrator
from awx.sso.utils.saml_migrator import SAMLMigrator
__all__ = [
'AzureADMigrator',
'GitHubMigrator',
'GoogleOAuth2Migrator',
'LDAPMigrator',
'OIDCMigrator',
'RADIUSMigrator',
'SAMLMigrator',
]

View File

@@ -4,6 +4,7 @@ Google OAuth2 authenticator migrator.
This module handles the migration of Google OAuth2 authenticators from AWX to Gateway. This module handles the migration of Google OAuth2 authenticators from AWX to Gateway.
""" """
from awx.main.utils.gateway_mapping import org_map_to_gateway_format, team_map_to_gateway_format
from awx.sso.utils.base_migrator import BaseAuthenticatorMigrator from awx.sso.utils.base_migrator import BaseAuthenticatorMigrator
@@ -24,27 +25,69 @@ class GoogleOAuth2Migrator(BaseAuthenticatorMigrator):
Returns: Returns:
list: List of configured Google OAuth2 authentication providers with their settings list: List of configured Google OAuth2 authentication providers with their settings
""" """
# TODO: Implement Google OAuth2 configuration retrieval from django.conf import settings
# Google OAuth2 settings typically include:
# - SOCIAL_AUTH_GOOGLE_OAUTH2_KEY if not getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None):
# - SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET return []
# - SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE
# - SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS config_data = {
# - SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_EMAILS 'SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL,
# - SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY,
# - SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET,
found_configs = [] 'SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE,
return found_configs }
return [
{
"category": self.get_authenticator_type(),
"settings": config_data,
}
]
def _build_mappers(self):
org_map = self.get_social_org_map('SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP')
team_map = self.get_social_team_map('SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP')
mappers, order = org_map_to_gateway_format(org_map, 1)
team_mappers, _ = team_map_to_gateway_format(team_map, order)
mappers.extend(team_mappers)
return mappers
def create_gateway_authenticator(self, config): def create_gateway_authenticator(self, config):
"""Create a Google OAuth2 authenticator in Gateway.""" """Create a Google OAuth2 authenticator in Gateway."""
# TODO: Implement Google OAuth2 authenticator creation category = config["category"]
# When implementing, use this pattern for slug generation: config_settings = config['settings']
# client_id = settings.get('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'google')
# authenticator_slug = self._generate_authenticator_slug('google_oauth2', category, client_id) authenticator_slug = self._generate_authenticator_slug('google-oauth2', category.replace(" ", "-"))
# Similar to GitHub OAuth2 but with Google-specific endpoints
# - Extract GOOGLE_OAUTH2_KEY and GOOGLE_OAUTH2_SECRET self._write_output(f"\n--- Processing {category} authenticator ---")
# - Handle whitelisted domains/emails
# - Configure Google OAuth2 scope gateway_config = {
self._write_output('Google OAuth2 authenticator creation not yet implemented', 'warning') "name": "google",
return False "slug": authenticator_slug,
"type": "ansible_base.authentication.authenticator_plugins.google_oauth2",
"enabled": True,
"create_objects": True, # Allow Gateway to create users/orgs/teams
"remove_users": False, # Don't remove users by default
"configuration": {
"KEY": config_settings.get('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'),
"SECRET": config_settings.get('SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'),
"REDIRECT_STATE": True,
},
"mappers": self._build_mappers(),
}
ignore_keys = ["ACCESS_TOKEN_METHOD", "REVOKE_TOKEN_METHOD"]
optional = {
"CALLBACK_URL": config_settings.get('SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL'),
"SCOPE": config_settings.get('SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE'),
}
for key, value in optional.items():
if value:
gateway_config["configuration"][key] = value
else:
ignore_keys.append(key)
return self.submit_authenticator(gateway_config, ignore_keys, config)