forked from ansible/awx
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add dump_auth_config management cmd (for SAML and LDAP) (ansible#14947)
* Add dump_auth_config management cmd - Dump SAML config from AWX to DAB authenticator config in json format * Add dumping of LDAP settings * add test for command * Fix is_enabled * fix command name typo Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> * add fields to config, add name to data * break out IDP values * change test fields and value comparison * edit help text, reformat settings --------- Co-authored-by: jessicamack <jmack@redhat.com>
- Loading branch information
1 parent
a635445
commit 8ff7260
Showing
2 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |