awx/awx/sso/utils/github_migrator.py
Peter Braun c5e55fe0f5
Aap 48489 (#7003)
* collect controller ldap configuration

* translate role mapping and submit ldap authenticator

* implement require and deny group mapping

* remove all references of awx in the naming

* fix linter issues

* address PR feedback

* update ldap authenticator naming

* update github authenticator naming

* assume that server_uri is always a string

* update order of evaluation for require and deny groups

* cleanup and move ldap related functions into the ldap migrator

* add skip option for saml

* update saml authenticator to new slug format

* update azuread authenticator to new slug format
2025-09-04 15:03:57 -04:00

199 lines
8.7 KiB
Python

"""
GitHub authenticator migrator.
This module handles the migration of GitHub authenticators from AWX to Gateway.
"""
from django.conf import settings
from awx.conf import settings_registry
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
import re
class GitHubMigrator(BaseAuthenticatorMigrator):
"""
Handles the migration of GitHub authenticators from AWX to Gateway.
"""
def get_authenticator_type(self):
"""Get the human-readable authenticator type name."""
return "GitHub"
def get_controller_config(self):
"""
Export all GitHub authenticators. A GitHub authenticator is only exported if both,
id and secret, are defined. Otherwise it will be skipped.
Returns:
list: List of configured GitHub authentication providers with their settings
"""
github_categories = ['github', 'github-org', 'github-team', 'github-enterprise', 'github-enterprise-org', 'github-enterprise-team']
found_configs = []
for category in github_categories:
try:
category_settings = settings_registry.get_registered_settings(category_slug=category)
if category_settings:
config_data = {}
key_setting = None
secret_setting = None
# Ensure category_settings is iterable and contains strings
if isinstance(category_settings, re.Pattern) or not hasattr(category_settings, '__iter__') or isinstance(category_settings, str):
continue
for setting_name in category_settings:
# Skip if setting_name is not a string (e.g., regex pattern)
if not isinstance(setting_name, str):
continue
if setting_name.endswith('_KEY'):
key_setting = setting_name
elif setting_name.endswith('_SECRET'):
secret_setting = setting_name
# Skip this category if KEY or SECRET is missing or empty
if not key_setting or not secret_setting:
continue
key_value = getattr(settings, key_setting, None)
secret_value = getattr(settings, secret_setting, None)
# Skip this category if OIDC Key and/or Secret are not configured
if not key_value or not secret_value:
continue
# If we have both key and secret, collect all settings
org_map_value = None
team_map_value = None
for setting_name in category_settings:
# Skip if setting_name is not a string (e.g., regex pattern)
if not isinstance(setting_name, str):
continue
value = getattr(settings, setting_name, None)
config_data[setting_name] = value
# Capture org and team map values for special processing
if setting_name.endswith('_ORGANIZATION_MAP'):
org_map_value = value
elif setting_name.endswith('_TEAM_MAP'):
team_map_value = value
# Convert GitHub org and team mappings from AWX to the Gateway format
# Start with order 1 and maintain sequence across both org and team mappers
org_mappers, next_order = org_map_to_gateway_format(org_map_value, start_order=1)
team_mappers, _ = team_map_to_gateway_format(team_map_value, start_order=next_order)
found_configs.append({'category': category, 'settings': config_data, 'org_mappers': org_mappers, 'team_mappers': team_mappers})
except Exception as e:
raise Exception(f'Could not retrieve {category} settings: {str(e)}')
return found_configs
def create_gateway_authenticator(self, config):
"""Create a GitHub/OIDC authenticator in Gateway."""
category = config['category']
settings = config['settings']
# Extract the OAuth2 credentials
key_value = None
secret_value = None
for setting_name, value in settings.items():
if setting_name.endswith('_KEY') and value:
key_value = value
elif setting_name.endswith('_SECRET') and value:
secret_value = value
if not key_value or not secret_value:
self._write_output(f'Skipping {category}: missing OAuth2 credentials', 'warning')
return False
# Generate authenticator name and slug
authenticator_name = category
authenticator_slug = self._generate_authenticator_slug('github', category)
# Map AWX category to Gateway authenticator type
type_mapping = {
'github': 'ansible_base.authentication.authenticator_plugins.github',
'github-org': 'ansible_base.authentication.authenticator_plugins.github_org',
'github-team': 'ansible_base.authentication.authenticator_plugins.github_team',
'github-enterprise': 'ansible_base.authentication.authenticator_plugins.github_enterprise',
'github-enterprise-org': 'ansible_base.authentication.authenticator_plugins.github_enterprise_org',
'github-enterprise-team': 'ansible_base.authentication.authenticator_plugins.github_enterprise_team',
}
authenticator_type = type_mapping.get(category)
if not authenticator_type:
self._write_output(f'Unknown category {category}, skipping', 'warning')
return False
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: {authenticator_type}')
self._write_output(f'Client ID: {key_value}')
self._write_output(f'Client Secret: {"*" * 8}')
# Build Gateway authenticator configuration
gateway_config = {
"name": authenticator_name,
"slug": authenticator_slug,
"type": authenticator_type,
"enabled": True,
"create_objects": True, # Allow Gateway to create users/orgs/teams
"remove_users": False, # Don't remove users by default
"configuration": {"KEY": key_value, "SECRET": secret_value},
}
# Add any additional configuration based on AWX settings
additional_config = self._build_additional_config(category, settings)
gateway_config["configuration"].update(additional_config)
# GitHub authenticators have auto-generated fields that should be ignored during comparison
# CALLBACK_URL - automatically created by Gateway
# SCOPE - relevant for mappers with team/org requirement, allows to read the org or team
# SECRET - the secret is encrypted in Gateway, we have no way of comparing the decrypted value
ignore_keys = ['CALLBACK_URL', 'SCOPE', 'SECRET']
# Submit the authenticator (create or update as needed)
return self.submit_authenticator(gateway_config, ignore_keys, config)
def _build_additional_config(self, category, settings):
"""Build additional configuration for specific authenticator types."""
additional_config = {}
# Add scope configuration if present
for setting_name, value in settings.items():
if setting_name.endswith('_SCOPE') and value:
additional_config['SCOPE'] = value
break
# Add GitHub Enterprise URL if present
if 'enterprise' in category:
for setting_name, value in settings.items():
if setting_name.endswith('_API_URL') and value:
additional_config['API_URL'] = value
elif setting_name.endswith('_URL') and value:
additional_config['URL'] = value
# Add organization name for org-specific authenticators
if 'org' in category:
for setting_name, value in settings.items():
if setting_name.endswith('_NAME') and value:
additional_config['NAME'] = value
break
# Add team ID for team-specific authenticators
if 'team' in category:
for setting_name, value in settings.items():
if setting_name.endswith('_ID') and value:
additional_config['ID'] = value
break
return additional_config