diff --git a/awx/main/management/commands/dump_auth_config.py b/awx/main/management/commands/dump_auth_config.py new file mode 100644 index 0000000000..ce8b778486 --- /dev/null +++ b/awx/main/management/commands/dump_auth_config.py @@ -0,0 +1,179 @@ +import json +import os +import sys +import re + +from typing import Any +from django.core.management.base import BaseCommand +from django.conf import settings +from awx.conf import settings_registry + + +class Command(BaseCommand): + help = 'Dump the current auth configuration in django_ansible_base.authenticator format, currently supports LDAP and SAML' + + DAB_SAML_AUTHENTICATOR_KEYS = { + "SP_ENTITY_ID": True, + "SP_PUBLIC_CERT": True, + "SP_PRIVATE_KEY": True, + "ORG_INFO": True, + "TECHNICAL_CONTACT": True, + "SUPPORT_CONTACT": True, + "SP_EXTRA": False, + "SECURITY_CONFIG": False, + "EXTRA_DATA": False, + "ENABLED_IDPS": True, + "CALLBACK_URL": False, + } + + DAB_LDAP_AUTHENTICATOR_KEYS = { + "SERVER_URI": True, + "BIND_DN": False, + "BIND_PASSWORD": False, + "CONNECTION_OPTIONS": False, + "GROUP_TYPE": True, + "GROUP_TYPE_PARAMS": True, + "GROUP_SEARCH": False, + "START_TLS": False, + "USER_DN_TEMPLATE": True, + "USER_ATTR_MAP": True, + "USER_SEARCH": False, + } + + def get_awx_ldap_settings(self) -> dict[str, dict[str, Any]]: + awx_ldap_settings = {} + + for awx_ldap_setting in settings_registry.get_registered_settings(category_slug='ldap'): + key = awx_ldap_setting.removeprefix("AUTH_LDAP_") + value = getattr(settings, awx_ldap_setting, None) + awx_ldap_settings[key] = value + + grouped_settings = {} + + for key, value in awx_ldap_settings.items(): + match = re.search(r'(\d+)', key) + index = int(match.group()) if match else 0 + new_key = re.sub(r'\d+_', '', key) + + if index not in grouped_settings: + grouped_settings[index] = {} + + grouped_settings[index][new_key] = value + if new_key == "GROUP_TYPE" and value: + grouped_settings[index][new_key] = type(value).__name__ + + if new_key == "SERVER_URI" and value: + value = value.split(", ") + + return grouped_settings + + def is_enabled(self, settings, keys): + for key, required in keys.items(): + if required and not settings.get(key): + return False + return True + + def get_awx_saml_settings(self) -> dict[str, Any]: + awx_saml_settings = {} + for awx_saml_setting in settings_registry.get_registered_settings(category_slug='saml'): + awx_saml_settings[awx_saml_setting.removeprefix("SOCIAL_AUTH_SAML_")] = getattr(settings, awx_saml_setting, None) + + return awx_saml_settings + + def format_config_data(self, enabled, awx_settings, type, keys, name): + config = { + "type": f"awx.authentication.authenticator_plugins.{type}", + "name": name, + "enabled": enabled, + "create_objects": True, + "users_unique": False, + "remove_users": True, + "configuration": {}, + } + for k in keys: + v = awx_settings.get(k) + config["configuration"].update({k: v}) + + if type == "saml": + idp_to_key_mapping = { + "url": "IDP_URL", + "x509cert": "IDP_X509_CERT", + "entity_id": "IDP_ENTITY_ID", + "attr_email": "IDP_ATTR_EMAIL", + "attr_groups": "IDP_GROUPS", + "attr_username": "IDP_ATTR_USERNAME", + "attr_last_name": "IDP_ATTR_LAST_NAME", + "attr_first_name": "IDP_ATTR_FIRST_NAME", + "attr_user_permanent_id": "IDP_ATTR_USER_PERMANENT_ID", + } + for idp_name in awx_settings.get("ENABLED_IDPS", {}): + for key in idp_to_key_mapping: + value = awx_settings["ENABLED_IDPS"][idp_name].get(key) + if value is not None: + config["name"] = idp_name + config["configuration"].update({idp_to_key_mapping[key]: value}) + + return config + + def add_arguments(self, parser): + parser.add_argument( + "output_file", + nargs="?", + type=str, + default=None, + help="Output JSON file path", + ) + + def handle(self, *args, **options): + try: + data = [] + + # dump SAML settings + awx_saml_settings = self.get_awx_saml_settings() + awx_saml_enabled = self.is_enabled(awx_saml_settings, self.DAB_SAML_AUTHENTICATOR_KEYS) + if awx_saml_enabled: + awx_saml_name = awx_saml_settings["ENABLED_IDPS"] + data.append( + self.format_config_data( + awx_saml_enabled, + awx_saml_settings, + "saml", + self.DAB_SAML_AUTHENTICATOR_KEYS, + awx_saml_name, + ) + ) + + # dump LDAP settings + awx_ldap_group_settings = self.get_awx_ldap_settings() + for awx_ldap_name, awx_ldap_settings in enumerate(awx_ldap_group_settings.values()): + enabled = self.is_enabled(awx_ldap_settings, self.DAB_LDAP_AUTHENTICATOR_KEYS) + if enabled: + data.append( + self.format_config_data( + enabled, + awx_ldap_settings, + "ldap", + self.DAB_LDAP_AUTHENTICATOR_KEYS, + str(awx_ldap_name), + ) + ) + + # write to file if requested + if options["output_file"]: + # Define the path for the output JSON file + output_file = options["output_file"] + + # Ensure the directory exists + os.makedirs(os.path.dirname(output_file), exist_ok=True) + + # Write data to the JSON file + with open(output_file, "w") as f: + json.dump(data, f, indent=4) + + self.stdout.write(self.style.SUCCESS(f"Auth config data dumped to {output_file}")) + else: + self.stdout.write(json.dumps(data, indent=4)) + + except Exception as e: + self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}")) + sys.exit(1) diff --git a/awx/main/tests/unit/commands/test_dump_auth_config.py b/awx/main/tests/unit/commands/test_dump_auth_config.py new file mode 100644 index 0000000000..96f6aeb865 --- /dev/null +++ b/awx/main/tests/unit/commands/test_dump_auth_config.py @@ -0,0 +1,122 @@ +from io import StringIO +import json +from django.core.management import call_command +from django.test import TestCase, override_settings + + +settings_dict = { + "SOCIAL_AUTH_SAML_SP_ENTITY_ID": "SP_ENTITY_ID", + "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "SP_PUBLIC_CERT", + "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "SP_PRIVATE_KEY", + "SOCIAL_AUTH_SAML_ORG_INFO": "ORG_INFO", + "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": "TECHNICAL_CONTACT", + "SOCIAL_AUTH_SAML_SUPPORT_CONTACT": "SUPPORT_CONTACT", + "SOCIAL_AUTH_SAML_SP_EXTRA": "SP_EXTRA", + "SOCIAL_AUTH_SAML_SECURITY_CONFIG": "SECURITY_CONFIG", + "SOCIAL_AUTH_SAML_EXTRA_DATA": "EXTRA_DATA", + "SOCIAL_AUTH_SAML_ENABLED_IDPS": { + "Keycloak": { + "attr_last_name": "last_name", + "attr_groups": "groups", + "attr_email": "email", + "attr_user_permanent_id": "name_id", + "attr_username": "username", + "entity_id": "https://example.com/auth/realms/awx", + "url": "https://example.com/auth/realms/awx/protocol/saml", + "x509cert": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----", + "attr_first_name": "first_name", + } + }, + "SOCIAL_AUTH_SAML_CALLBACK_URL": "CALLBACK_URL", + "AUTH_LDAP_1_SERVER_URI": "SERVER_URI", + "AUTH_LDAP_1_BIND_DN": "BIND_DN", + "AUTH_LDAP_1_BIND_PASSWORD": "BIND_PASSWORD", + "AUTH_LDAP_1_GROUP_SEARCH": ["GROUP_SEARCH"], + "AUTH_LDAP_1_GROUP_TYPE": "string object", + "AUTH_LDAP_1_GROUP_TYPE_PARAMS": {"member_attr": "member", "name_attr": "cn"}, + "AUTH_LDAP_1_USER_DN_TEMPLATE": "USER_DN_TEMPLATE", + "AUTH_LDAP_1_USER_SEARCH": ["USER_SEARCH"], + "AUTH_LDAP_1_USER_ATTR_MAP": { + "email": "email", + "last_name": "last_name", + "first_name": "first_name", + }, + "AUTH_LDAP_1_CONNECTION_OPTIONS": {}, + "AUTH_LDAP_1_START_TLS": None, +} + + +@override_settings(**settings_dict) +class TestDumpAuthConfigCommand(TestCase): + def setUp(self): + super().setUp() + self.expected_config = [ + { + "type": "awx.authentication.authenticator_plugins.saml", + "name": "Keycloak", + "enabled": True, + "create_objects": True, + "users_unique": False, + "remove_users": True, + "configuration": { + "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", + "SP_EXTRA": "SP_EXTRA", + "SECURITY_CONFIG": "SECURITY_CONFIG", + "EXTRA_DATA": "EXTRA_DATA", + "ENABLED_IDPS": { + "Keycloak": { + "attr_last_name": "last_name", + "attr_groups": "groups", + "attr_email": "email", + "attr_user_permanent_id": "name_id", + "attr_username": "username", + "entity_id": "https://example.com/auth/realms/awx", + "url": "https://example.com/auth/realms/awx/protocol/saml", + "x509cert": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----", + "attr_first_name": "first_name", + } + }, + "CALLBACK_URL": "CALLBACK_URL", + "IDP_URL": "https://example.com/auth/realms/awx/protocol/saml", + "IDP_X509_CERT": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----", + "IDP_ENTITY_ID": "https://example.com/auth/realms/awx", + "IDP_ATTR_EMAIL": "email", + "IDP_GROUPS": "groups", + "IDP_ATTR_USERNAME": "username", + "IDP_ATTR_LAST_NAME": "last_name", + "IDP_ATTR_FIRST_NAME": "first_name", + "IDP_ATTR_USER_PERMANENT_ID": "name_id", + }, + }, + { + "type": "awx.authentication.authenticator_plugins.ldap", + "name": "1", + "enabled": True, + "create_objects": True, + "users_unique": False, + "remove_users": True, + "configuration": { + "SERVER_URI": "SERVER_URI", + "BIND_DN": "BIND_DN", + "BIND_PASSWORD": "BIND_PASSWORD", + "CONNECTION_OPTIONS": {}, + "GROUP_TYPE": "str", + "GROUP_TYPE_PARAMS": {"member_attr": "member", "name_attr": "cn"}, + "GROUP_SEARCH": ["GROUP_SEARCH"], + "START_TLS": None, + "USER_DN_TEMPLATE": "USER_DN_TEMPLATE", + "USER_ATTR_MAP": {"email": "email", "last_name": "last_name", "first_name": "first_name"}, + "USER_SEARCH": ["USER_SEARCH"], + }, + }, + ] + + def test_json_returned_from_cmd(self): + output = StringIO() + call_command("dump_auth_config", stdout=output) + assert json.loads(output.getvalue()) == self.expected_config