mirror of
https://github.com/ansible/awx.git
synced 2026-02-25 23:16:01 -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:
@@ -4,6 +4,7 @@ import os
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from awx.sso.utils.github_migrator import GitHubMigrator
|
from awx.sso.utils.github_migrator import GitHubMigrator
|
||||||
from awx.sso.utils.oidc_migrator import OIDCMigrator
|
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
|
from awx.main.utils.gateway_client import GatewayClient, GatewayAPIError
|
||||||
|
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ class Command(BaseCommand):
|
|||||||
if not skip_oidc:
|
if not skip_oidc:
|
||||||
migrators.append(GitHubMigrator(gateway_client, self))
|
migrators.append(GitHubMigrator(gateway_client, self))
|
||||||
migrators.append(OIDCMigrator(gateway_client, self))
|
migrators.append(OIDCMigrator(gateway_client, self))
|
||||||
|
migrators.append(SAMLMigrator(gateway_client, self))
|
||||||
# if not skip_ldap:
|
# if not skip_ldap:
|
||||||
# migrators.append(LDAPMigrator(gateway_client, self))
|
# migrators.append(LDAPMigrator(gateway_client, self))
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,33 @@ def existing_tacacsplus_user():
|
|||||||
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
|
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
|
||||||
enterprise_auth.save()
|
enterprise_auth.save()
|
||||||
return user
|
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.
|
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
|
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):
|
class SAMLMigrator(BaseAuthenticatorMigrator):
|
||||||
"""
|
"""
|
||||||
Handles the migration of SAML authenticators from AWX to Gateway.
|
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):
|
def get_authenticator_type(self):
|
||||||
"""Get the human-readable authenticator type name."""
|
"""Get the human-readable authenticator type name."""
|
||||||
return "SAML"
|
return "SAML"
|
||||||
@@ -24,29 +41,96 @@ class SAMLMigrator(BaseAuthenticatorMigrator):
|
|||||||
Returns:
|
Returns:
|
||||||
list: List of configured SAML authentication providers with their settings
|
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 = []
|
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
|
return found_configs
|
||||||
|
|
||||||
def create_gateway_authenticator(self, config):
|
def create_gateway_authenticator(self, config):
|
||||||
"""Create a SAML authenticator in Gateway."""
|
"""Create a SAML authenticator in Gateway."""
|
||||||
# TODO: Implement SAML authenticator creation
|
category = config["category"]
|
||||||
# When implementing, use this pattern for slug generation:
|
config_settings = config["settings"]
|
||||||
# entity_id = settings.get('SOCIAL_AUTH_SAML_SP_ENTITY_ID', 'saml')
|
name = config_settings["name"]
|
||||||
# authenticator_slug = self._generate_authenticator_slug('saml', category, entity_id)
|
|
||||||
# SAML requires complex configuration including:
|
# Generate authenticator name and slug
|
||||||
# - SP entity ID, certificates, metadata
|
authenticator_name = f"AWX-{category.replace('-', '_').title()}-{name}"
|
||||||
# - IdP configuration and metadata
|
authenticator_slug = self._generate_authenticator_slug("saml", category, name)
|
||||||
# - Attribute mapping
|
|
||||||
self._write_output('SAML authenticator creation not yet implemented', 'warning')
|
self._write_output(f"\n--- Processing {category} authenticator ---")
|
||||||
return False
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user