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
No known key found for this signature in database
GPG Key ID: E84C42ACF75B0768
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.radius_migrator import RADIUSMigrator
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
@ -22,6 +23,7 @@ class Command(BaseCommand):
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-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')
def handle(self, *args, **options):
@ -37,6 +39,7 @@ class Command(BaseCommand):
skip_saml = options['skip_saml']
skip_radius = options['skip_radius']
skip_tacacs = options['skip_tacacs']
skip_google = options['skip_google']
force = options['force']
# 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:
migrators.append(TACACSMigrator(gateway_client, self, force=force))
if not skip_google:
migrators.append(GoogleOAuth2Migrator(gateway_client, self, force=force))
# Run migrations
total_results = {
'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.
"""
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
@ -24,27 +25,69 @@ class GoogleOAuth2Migrator(BaseAuthenticatorMigrator):
Returns:
list: List of configured Google OAuth2 authentication providers with their settings
"""
# TODO: Implement Google OAuth2 configuration retrieval
# Google OAuth2 settings typically include:
# - SOCIAL_AUTH_GOOGLE_OAUTH2_KEY
# - SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET
# - SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE
# - SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS
# - SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_EMAILS
# - SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP
# - SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP
found_configs = []
return found_configs
from django.conf import settings
if not getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None):
return []
config_data = {
'SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL,
'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY,
'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET,
'SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE,
}
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):
"""Create a Google OAuth2 authenticator in Gateway."""
# TODO: Implement Google OAuth2 authenticator creation
# When implementing, use this pattern for slug generation:
# client_id = settings.get('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'google')
# authenticator_slug = self._generate_authenticator_slug('google_oauth2', category, client_id)
# Similar to GitHub OAuth2 but with Google-specific endpoints
# - Extract GOOGLE_OAUTH2_KEY and GOOGLE_OAUTH2_SECRET
# - Handle whitelisted domains/emails
# - Configure Google OAuth2 scope
self._write_output('Google OAuth2 authenticator creation not yet implemented', 'warning')
return False
category = config["category"]
config_settings = config['settings']
authenticator_slug = self._generate_authenticator_slug('google-oauth2', category.replace(" ", "-"))
self._write_output(f"\n--- Processing {category} authenticator ---")
gateway_config = {
"name": "google",
"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)