mirror of
https://github.com/ansible/awx.git
synced 2026-01-10 15:32:07 -03:30
add force flag to enforce updates even when authenticator already exists (#7015)
* add force flag to enforce updates even when authenticator already exists * remove cleartext field * update list of encrypted fields * show updated and unchanged authenticators in report
This commit is contained in:
parent
c5e55fe0f5
commit
ab9bde3698
@ -18,6 +18,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('--force', action='store_true', help='Force migration even if configurations already exist')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Read Gateway connection parameters from environment variables
|
||||
@ -30,6 +31,7 @@ class Command(BaseCommand):
|
||||
skip_ldap = options['skip_ldap']
|
||||
skip_ad = options['skip_ad']
|
||||
skip_saml = options['skip_saml']
|
||||
force = options['force']
|
||||
|
||||
# If the management command isn't called with all parameters needed to talk to Gateway, consider
|
||||
# it a dry-run and exit cleanly
|
||||
@ -57,21 +59,23 @@ class Command(BaseCommand):
|
||||
# Initialize migrators
|
||||
migrators = []
|
||||
if not skip_oidc:
|
||||
migrators.append(GitHubMigrator(gateway_client, self))
|
||||
migrators.append(OIDCMigrator(gateway_client, self))
|
||||
migrators.append(GitHubMigrator(gateway_client, self, force=force))
|
||||
migrators.append(OIDCMigrator(gateway_client, self, force=force))
|
||||
|
||||
if not skip_saml:
|
||||
migrators.append(SAMLMigrator(gateway_client, self))
|
||||
migrators.append(SAMLMigrator(gateway_client, self, force=force))
|
||||
|
||||
if not skip_ad:
|
||||
migrators.append(AzureADMigrator(gateway_client, self))
|
||||
migrators.append(AzureADMigrator(gateway_client, self, force=force))
|
||||
|
||||
if not skip_ldap:
|
||||
migrators.append(LDAPMigrator(gateway_client, self))
|
||||
migrators.append(LDAPMigrator(gateway_client, self, force=force))
|
||||
|
||||
# Run migrations
|
||||
total_results = {
|
||||
'created': 0,
|
||||
'updated': 0,
|
||||
'unchanged': 0,
|
||||
'failed': 0,
|
||||
'mappers_created': 0,
|
||||
'mappers_updated': 0,
|
||||
@ -93,6 +97,8 @@ class Command(BaseCommand):
|
||||
# Overall summary
|
||||
self.stdout.write(self.style.SUCCESS('\n=== Migration Summary ==='))
|
||||
self.stdout.write(f'Total authenticators created: {total_results["created"]}')
|
||||
self.stdout.write(f'Total authenticators updated: {total_results["updated"]}')
|
||||
self.stdout.write(f'Total authenticators unchanged: {total_results["unchanged"]}')
|
||||
self.stdout.write(f'Total authenticators failed: {total_results["failed"]}')
|
||||
self.stdout.write(f'Total mappers created: {total_results["mappers_created"]}')
|
||||
self.stdout.write(f'Total mappers updated: {total_results["mappers_updated"]}')
|
||||
@ -113,6 +119,8 @@ class Command(BaseCommand):
|
||||
"""Print a summary of the export results."""
|
||||
self.stdout.write(f'\n--- {config_type} Export Summary ---')
|
||||
self.stdout.write(f'Authenticators created: {result.get("created", 0)}')
|
||||
self.stdout.write(f'Authenticators updated: {result.get("updated", 0)}')
|
||||
self.stdout.write(f'Authenticators unchanged: {result.get("unchanged", 0)}')
|
||||
self.stdout.write(f'Authenticators failed: {result.get("failed", 0)}')
|
||||
self.stdout.write(f'Mappers created: {result.get("mappers_created", 0)}')
|
||||
self.stdout.write(f'Mappers updated: {result.get("mappers_updated", 0)}')
|
||||
|
||||
@ -13,16 +13,32 @@ class BaseAuthenticatorMigrator:
|
||||
Defines the contract that all specific authenticator migrators must follow.
|
||||
"""
|
||||
|
||||
def __init__(self, gateway_client=None, command=None):
|
||||
def __init__(self, gateway_client=None, command=None, force=False):
|
||||
"""
|
||||
Initialize the authenticator migrator.
|
||||
|
||||
Args:
|
||||
gateway_client: GatewayClient instance for API calls
|
||||
command: Optional Django management command instance (for styled output)
|
||||
force: If True, force migration even if configurations already exist
|
||||
"""
|
||||
self.gateway_client = gateway_client
|
||||
self.command = command
|
||||
self.force = force
|
||||
self.encrypted_fields = [
|
||||
# LDAP Fields
|
||||
'BIND_PASSWORD',
|
||||
# The following authenticators all use the same key to store encrypted information:
|
||||
# Generic OIDC
|
||||
# RADIUS
|
||||
# TACACS+
|
||||
# GitHub OAuth2
|
||||
# Azure AD OAuth2
|
||||
# Google OAuth2
|
||||
'SECRET',
|
||||
# SAML Fields
|
||||
'SP_PRIVATE_KEY',
|
||||
]
|
||||
|
||||
def migrate(self):
|
||||
"""
|
||||
@ -36,23 +52,36 @@ class BaseAuthenticatorMigrator:
|
||||
|
||||
if not configs:
|
||||
self._write_output(f'No {self.get_authenticator_type()} authenticators found to migrate.', 'warning')
|
||||
return {'created': 0, 'failed': 0, 'mappers_created': 0, 'mappers_failed': 0}
|
||||
return {'created': 0, 'updated': 0, 'unchanged': 0, 'failed': 0, 'mappers_created': 0, 'mappers_updated': 0, 'mappers_failed': 0}
|
||||
|
||||
self._write_output(f'Found {len(configs)} {self.get_authenticator_type()} authentication configuration(s).', 'success')
|
||||
|
||||
# Process each authenticator configuration
|
||||
created_authenticators = []
|
||||
for config in configs:
|
||||
if self.create_gateway_authenticator(config):
|
||||
created_authenticators.append(config)
|
||||
updated_authenticators = []
|
||||
unchanged_authenticators = []
|
||||
failed_authenticators = []
|
||||
|
||||
# Process mappers for successfully created/updated authenticators
|
||||
for config in configs:
|
||||
result = self.create_gateway_authenticator(config)
|
||||
if result['success']:
|
||||
if result['action'] == 'created':
|
||||
created_authenticators.append(config)
|
||||
elif result['action'] == 'updated':
|
||||
updated_authenticators.append(config)
|
||||
elif result['action'] == 'skipped':
|
||||
unchanged_authenticators.append(config)
|
||||
else:
|
||||
failed_authenticators.append(config)
|
||||
|
||||
# Process mappers for successfully created/updated/unchanged authenticators
|
||||
mappers_created = 0
|
||||
mappers_updated = 0
|
||||
mappers_failed = 0
|
||||
if created_authenticators:
|
||||
successful_authenticators = created_authenticators + updated_authenticators + unchanged_authenticators
|
||||
if successful_authenticators:
|
||||
self._write_output('\n=== Processing Authenticator Mappers ===', 'success')
|
||||
for config in created_authenticators:
|
||||
for config in successful_authenticators:
|
||||
mapper_result = self._process_gateway_mappers(config)
|
||||
mappers_created += mapper_result['created']
|
||||
mappers_updated += mapper_result['updated']
|
||||
@ -60,7 +89,9 @@ class BaseAuthenticatorMigrator:
|
||||
|
||||
return {
|
||||
'created': len(created_authenticators),
|
||||
'failed': len(configs) - len(created_authenticators),
|
||||
'updated': len(updated_authenticators),
|
||||
'unchanged': len(unchanged_authenticators),
|
||||
'failed': len(failed_authenticators),
|
||||
'mappers_created': mappers_created,
|
||||
'mappers_updated': mappers_updated,
|
||||
'mappers_failed': mappers_failed,
|
||||
@ -98,7 +129,7 @@ class BaseAuthenticatorMigrator:
|
||||
|
||||
def _generate_authenticator_slug(self, auth_type, category):
|
||||
"""Generate a deterministic slug for an authenticator."""
|
||||
return f"aap-{auth_type}-{category}"
|
||||
return f"aap-{auth_type}-{category}".lower()
|
||||
|
||||
def submit_authenticator(self, gateway_config, ignore_keys=[], config={}):
|
||||
"""
|
||||
@ -110,12 +141,12 @@ class BaseAuthenticatorMigrator:
|
||||
config: Optional AWX config dict to store result data
|
||||
|
||||
Returns:
|
||||
bool: True if authenticator was submitted successfully, False otherwise
|
||||
dict: Result with 'success' (bool), 'action' ('created', 'updated', 'skipped'), 'error' (str or None)
|
||||
"""
|
||||
authenticator_slug = gateway_config.get('slug')
|
||||
if not authenticator_slug:
|
||||
self._write_output('Gateway config missing slug, cannot submit authenticator', 'error')
|
||||
return False
|
||||
return {'success': False, 'action': None, 'error': 'Missing slug'}
|
||||
|
||||
try:
|
||||
# Check if authenticator already exists by slug
|
||||
@ -132,7 +163,7 @@ class BaseAuthenticatorMigrator:
|
||||
# Store the existing result for mapper creation
|
||||
config['gateway_authenticator_id'] = authenticator_id
|
||||
config['gateway_authenticator'] = existing_authenticator
|
||||
return True
|
||||
return {'success': True, 'action': 'skipped', 'error': None}
|
||||
else:
|
||||
self._write_output(f'⚠ Authenticator exists but configuration differs (ID: {authenticator_id})', 'warning')
|
||||
self._write_output(' Configuration comparison:')
|
||||
@ -155,12 +186,12 @@ class BaseAuthenticatorMigrator:
|
||||
# Store the updated result for mapper creation
|
||||
config['gateway_authenticator_id'] = authenticator_id
|
||||
config['gateway_authenticator'] = result
|
||||
return True
|
||||
return {'success': True, 'action': 'updated', 'error': None}
|
||||
except GatewayAPIError as e:
|
||||
self._write_output(f'✗ Failed to update authenticator: {e.message}', 'error')
|
||||
if e.response_data:
|
||||
self._write_output(f' Details: {e.response_data}', 'error')
|
||||
return False
|
||||
return {'success': False, 'action': 'update_failed', 'error': e.message}
|
||||
else:
|
||||
# Authenticator doesn't exist, create it
|
||||
self._write_output('Creating new authenticator...')
|
||||
@ -173,16 +204,16 @@ class BaseAuthenticatorMigrator:
|
||||
# Store the result for potential mapper creation later
|
||||
config['gateway_authenticator_id'] = result.get('id')
|
||||
config['gateway_authenticator'] = result
|
||||
return True
|
||||
return {'success': True, 'action': 'created', 'error': None}
|
||||
|
||||
except GatewayAPIError as e:
|
||||
self._write_output(f'✗ Failed to submit authenticator: {e.message}', 'error')
|
||||
if e.response_data:
|
||||
self._write_output(f' Details: {e.response_data}', 'error')
|
||||
return False
|
||||
return {'success': False, 'action': 'failed', 'error': e.message}
|
||||
except Exception as e:
|
||||
self._write_output(f'✗ Unexpected error submitting authenticator: {str(e)}', 'error')
|
||||
return False
|
||||
return {'success': False, 'action': 'failed', 'error': str(e)}
|
||||
|
||||
def _authenticator_configs_match(self, existing_auth, new_config, ignore_keys=[]):
|
||||
"""
|
||||
@ -197,6 +228,11 @@ class BaseAuthenticatorMigrator:
|
||||
Returns:
|
||||
bool: True if configurations match, False otherwise
|
||||
"""
|
||||
# Add encrypted fields to ignore_keys if force flag is not set
|
||||
# This prevents secrets from being updated unless explicitly forced
|
||||
effective_ignore_keys = ignore_keys.copy()
|
||||
if not self.force:
|
||||
effective_ignore_keys.extend(self.encrypted_fields)
|
||||
|
||||
# Keep track of the differences between the existing and the new configuration
|
||||
# Logging them makes debugging much easier
|
||||
@ -219,7 +255,7 @@ class BaseAuthenticatorMigrator:
|
||||
|
||||
# Helper function to check if a key should be ignored
|
||||
def should_ignore_key(config_key):
|
||||
return config_key in ignore_keys
|
||||
return config_key in effective_ignore_keys
|
||||
|
||||
# Check if all keys in new config exist in existing config with same values
|
||||
for key, value in new_config_section.items():
|
||||
|
||||
@ -158,7 +158,7 @@ class GitHubMigrator(BaseAuthenticatorMigrator):
|
||||
# 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']
|
||||
ignore_keys = ['CALLBACK_URL', 'SCOPE']
|
||||
|
||||
# Submit the authenticator (create or update as needed)
|
||||
return self.submit_authenticator(gateway_config, ignore_keys, config)
|
||||
|
||||
@ -165,7 +165,7 @@ class LDAPMigrator(BaseAuthenticatorMigrator):
|
||||
|
||||
# LDAP authenticators have auto-generated fields that should be ignored during comparison
|
||||
# BIND_PASSWORD - encrypted value, can't be compared
|
||||
ignore_keys = ['BIND_PASSWORD']
|
||||
ignore_keys = []
|
||||
|
||||
# Submit the authenticator using the base class method
|
||||
return self.submit_authenticator(gateway_config, config=config, ignore_keys=ignore_keys)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user