mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
[AAP-48496] SAML Migration from Controller to Gateway (#6998)
This PR migrates the SAML configuration from the Controller to the Gateway, it intentionally skips setting the CALLBACK_URL so that the Gateway can fill in the appropriate URL.
This commit is contained in:
parent
c2c0f2b828
commit
512857c2a9
@ -4,6 +4,7 @@ import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from awx.sso.utils.github_migrator import GitHubMigrator
|
||||
from awx.sso.utils.oidc_migrator import OIDCMigrator
|
||||
from awx.sso.utils.saml_migrator import SAMLMigrator
|
||||
from awx.main.utils.gateway_client import GatewayClient, GatewayAPIError
|
||||
|
||||
|
||||
@ -52,6 +53,7 @@ class Command(BaseCommand):
|
||||
if not skip_oidc:
|
||||
migrators.append(GitHubMigrator(gateway_client, self))
|
||||
migrators.append(OIDCMigrator(gateway_client, self))
|
||||
migrators.append(SAMLMigrator(gateway_client, self))
|
||||
# if not skip_ldap:
|
||||
# migrators.append(LDAPMigrator(gateway_client, self))
|
||||
|
||||
|
||||
@ -32,3 +32,33 @@ def existing_tacacsplus_user():
|
||||
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
|
||||
enterprise_auth.save()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_saml_config(settings):
|
||||
settings.SAML_SECURITY_CONFIG = {
|
||||
"wantNameId": True,
|
||||
"signMetadata": False,
|
||||
"digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
"nameIdEncrypted": False,
|
||||
"signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
"authnRequestsSigned": False,
|
||||
"logoutRequestSigned": False,
|
||||
"wantNameIdEncrypted": False,
|
||||
"logoutResponseSigned": False,
|
||||
"wantAssertionsSigned": True,
|
||||
"requestedAuthnContext": False,
|
||||
"wantAssertionsEncrypted": False,
|
||||
}
|
||||
settings.SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
||||
"example": {
|
||||
"attr_email": "email",
|
||||
"attr_first_name": "first_name",
|
||||
"attr_last_name": "last_name",
|
||||
"attr_user_permanent_id": "username",
|
||||
"attr_username": "username",
|
||||
"entity_id": "https://www.example.com/realms/sample",
|
||||
"url": "https://www.example.com/realms/sample/protocol/saml",
|
||||
"x509cert": "A" * 64 + "B" * 64 + "C" * 23,
|
||||
}
|
||||
}
|
||||
|
||||
18
awx/sso/tests/unit/test_saml_migrator.py
Normal file
18
awx/sso/tests/unit/test_saml_migrator.py
Normal file
@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from awx.sso.utils.saml_migrator import SAMLMigrator
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_controller_config(test_saml_config):
|
||||
gateway_client = MagicMock()
|
||||
command_obj = MagicMock()
|
||||
obj = SAMLMigrator(gateway_client, command_obj)
|
||||
|
||||
result = obj.get_controller_config()
|
||||
lines = result[0]['settings']['configuration']['IDP_X509_CERT'].splitlines()
|
||||
assert lines[0] == '-----BEGIN CERTIFICATE-----'
|
||||
assert lines[1] == "A" * 64
|
||||
assert lines[2] == "B" * 64
|
||||
assert lines[3] == "C" * 23
|
||||
assert lines[-1] == '-----END CERTIFICATE-----'
|
||||
@ -4,14 +4,31 @@ SAML authenticator migrator.
|
||||
This module handles the migration of SAML authenticators from AWX to Gateway.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _split_chunks(data: str, length: int = 64) -> list[str]:
|
||||
return [data[i : i + length] for i in range(0, len(data), length)]
|
||||
|
||||
|
||||
def _to_pem_cert(data: str) -> list[str]:
|
||||
items = ["-----BEGIN CERTIFICATE-----"]
|
||||
items += _split_chunks(data)
|
||||
items.append("-----END CERTIFICATE-----")
|
||||
return items
|
||||
|
||||
|
||||
class SAMLMigrator(BaseAuthenticatorMigrator):
|
||||
"""
|
||||
Handles the migration of SAML authenticators from AWX to Gateway.
|
||||
"""
|
||||
|
||||
CATEGORY = "SAML"
|
||||
AUTH_TYPE = "ansible_base.authentication.authenticator_plugins.saml"
|
||||
|
||||
def get_authenticator_type(self):
|
||||
"""Get the human-readable authenticator type name."""
|
||||
return "SAML"
|
||||
@ -24,29 +41,96 @@ class SAMLMigrator(BaseAuthenticatorMigrator):
|
||||
Returns:
|
||||
list: List of configured SAML authentication providers with their settings
|
||||
"""
|
||||
# TODO: Implement SAML configuration retrieval
|
||||
# SAML settings typically include:
|
||||
# - SOCIAL_AUTH_SAML_SP_ENTITY_ID
|
||||
# - SOCIAL_AUTH_SAML_SP_PUBLIC_CERT
|
||||
# - SOCIAL_AUTH_SAML_SP_PRIVATE_KEY
|
||||
# - SOCIAL_AUTH_SAML_ORG_INFO
|
||||
# - SOCIAL_AUTH_SAML_TECHNICAL_CONTACT
|
||||
# - SOCIAL_AUTH_SAML_SUPPORT_CONTACT
|
||||
# - SOCIAL_AUTH_SAML_ENABLED_IDPS
|
||||
# - SOCIAL_AUTH_SAML_ORGANIZATION_MAP
|
||||
# - SOCIAL_AUTH_SAML_TEAM_MAP
|
||||
found_configs = []
|
||||
|
||||
enabled = True
|
||||
remove_users = True
|
||||
create_objects = getattr(settings, "SAML_AUTO_CREATE_OBJECTS", True)
|
||||
idps = getattr(settings, "SOCIAL_AUTH_SAML_ENABLED_IDPS", {})
|
||||
security_config = getattr(settings, "SOCIAL_AUTH_SAML_SECURITY_CONFIG", {})
|
||||
|
||||
org_map_value = getattr(settings, "SOCIAL_AUTH_SAML_ORGANIZATION_MAP", None)
|
||||
team_map_value = getattr(settings, "SOCIAL_AUTH_SAML_TEAM_MAP", None)
|
||||
extra_data = getattr(settings, "SOCIAL_AUTH_SAML_EXTRA_DATA", None)
|
||||
support_contact = getattr(settings, "SOCIAL_AUTH_SAML_SUPPORT_CONTACT", {})
|
||||
technical_contact = getattr(settings, "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT", {})
|
||||
org_info = getattr(settings, "SOCIAL_AUTH_SAML_ORG_INFO", {})
|
||||
|
||||
sp_private_key = getattr(settings, "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY", None)
|
||||
sp_public_cert = getattr(settings, "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT", None)
|
||||
sp_entity_id = getattr(settings, "SOCIAL_AUTH_SAML_SP_ENTITY_ID", None)
|
||||
sp_extra = getattr(settings, "SOCIAL_AUTH_SAML_SP_EXTRA", {})
|
||||
|
||||
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)
|
||||
|
||||
for name, value in idps.items():
|
||||
config_data = {
|
||||
"name": name,
|
||||
"type": self.AUTH_TYPE,
|
||||
"enabled": enabled,
|
||||
"create_objects": create_objects,
|
||||
"remove_users": remove_users,
|
||||
"configuration": {
|
||||
"IDP_URL": value.get("url"),
|
||||
"IDP_X509_CERT": "\n".join(_to_pem_cert(value.get("x509cert"))),
|
||||
"IDP_ENTITY_ID": value.get("entity_id"),
|
||||
"IDP_ATTR_EMAIL": value.get("attr_email"),
|
||||
"IDP_ATTR_USERNAME": value.get("attr_username"),
|
||||
"IDP_ATTR_FIRST_NAME": value.get("attr_first_name"),
|
||||
"IDP_ATTR_LAST_NAME": value.get("attr_last_name"),
|
||||
"IDP_ATTR_USER_PERMANENT_ID": value.get("attr_user_permanent_id"),
|
||||
"IDP_GROUPS": value.get("attr_groups"),
|
||||
"SP_ENTITY_ID": sp_entity_id,
|
||||
"SP_PUBLIC_CERT": sp_public_cert,
|
||||
"SP_PRIVATE_KEY": sp_private_key,
|
||||
"ORG_INFO": org_info,
|
||||
"TECHNICAL_CONTACT": technical_contact,
|
||||
"SUPPORT_CONTACT": support_contact,
|
||||
"SECURITY_CONFIG": security_config,
|
||||
"SP_EXTRA": sp_extra,
|
||||
"EXTRA_DATA": extra_data,
|
||||
},
|
||||
}
|
||||
|
||||
found_configs.append(
|
||||
{
|
||||
"category": self.CATEGORY,
|
||||
"settings": config_data,
|
||||
"org_mappers": org_mappers,
|
||||
"team_mappers": team_mappers,
|
||||
}
|
||||
)
|
||||
return found_configs
|
||||
|
||||
def create_gateway_authenticator(self, config):
|
||||
"""Create a SAML authenticator in Gateway."""
|
||||
# TODO: Implement SAML authenticator creation
|
||||
# When implementing, use this pattern for slug generation:
|
||||
# entity_id = settings.get('SOCIAL_AUTH_SAML_SP_ENTITY_ID', 'saml')
|
||||
# authenticator_slug = self._generate_authenticator_slug('saml', category, entity_id)
|
||||
# SAML requires complex configuration including:
|
||||
# - SP entity ID, certificates, metadata
|
||||
# - IdP configuration and metadata
|
||||
# - Attribute mapping
|
||||
self._write_output('SAML authenticator creation not yet implemented', 'warning')
|
||||
return False
|
||||
category = config["category"]
|
||||
config_settings = config["settings"]
|
||||
name = config_settings["name"]
|
||||
|
||||
# Generate authenticator name and slug
|
||||
authenticator_name = f"AWX-{category.replace('-', '_').title()}-{name}"
|
||||
authenticator_slug = self._generate_authenticator_slug("saml", category, name)
|
||||
|
||||
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: {config_settings['type']}")
|
||||
|
||||
# Build Gateway authenticator configuration
|
||||
gateway_config = {
|
||||
"name": authenticator_name,
|
||||
"slug": authenticator_slug,
|
||||
"type": config_settings["type"],
|
||||
"enabled": True,
|
||||
"create_objects": True, # Allow Gateway to create users/orgs/teams
|
||||
"remove_users": False, # Don't remove users by default
|
||||
"configuration": config_settings["configuration"],
|
||||
}
|
||||
|
||||
# CALLBACK_URL - automatically created by Gateway
|
||||
ignore_keys = ["CALLBACK_URL"]
|
||||
|
||||
# Submit the authenticator (create or update as needed)
|
||||
return self.submit_authenticator(gateway_config, ignore_keys, config)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user