mirror of
https://github.com/ansible/awx.git
synced 2026-01-10 15:32:07 -03:30
feat: Add migrator for Google OAuth2 authenticator (#7018)
Signed-off-by: Fabricio Aguiar <fabricio.aguiar@gmail.com>
This commit is contained in:
parent
e746589019
commit
8e58fee49c
@ -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,
|
||||
|
||||
104
awx/sso/tests/unit/test_google_oauth2_migrator.py
Normal file
104
awx/sso/tests/unit/test_google_oauth2_migrator.py
Normal 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
|
||||
@ -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',
|
||||
]
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user