Remove LDAP authentication (#15546)

Remove LDAP authentication from AWX
This commit is contained in:
Djebran Lezzoum
2024-10-02 15:40:16 +02:00
committed by jessicamack
parent 6dea7bfe17
commit f22b192fb4
67 changed files with 172 additions and 2813 deletions

View File

@@ -642,10 +642,7 @@ class UserAccess(BaseAccess):
"""
model = User
prefetch_related = (
'profile',
'resource',
)
prefetch_related = ('resource',)
def filtered_queryset(self):
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()):

View File

@@ -1,7 +1,6 @@
import json
import os
import sys
import re
from typing import Any
from django.core.management.base import BaseCommand
@@ -11,7 +10,7 @@ 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'
help = 'Dump the current auth configuration in django_ansible_base.authenticator format, currently supports SAML'
DAB_SAML_AUTHENTICATOR_KEYS = {
"SP_ENTITY_ID": True,
@@ -27,20 +26,6 @@ class Command(BaseCommand):
"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 is_enabled(self, settings, keys):
missing_fields = []
for key, required in keys.items():
@@ -50,41 +35,6 @@ class Command(BaseCommand):
return False, missing_fields
return True, None
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(", ")
grouped_settings[index][new_key] = value
if type(value).__name__ == "LDAPSearch":
data = []
data.append(value.base_dn)
data.append("SCOPE_SUBTREE")
data.append(value.filterstr)
grouped_settings[index][new_key] = data
return grouped_settings
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'):
@@ -157,23 +107,6 @@ class Command(BaseCommand):
else:
data.append({"SAML_missing_fields": saml_missing_fields})
# dump LDAP settings
awx_ldap_group_settings = self.get_awx_ldap_settings()
for awx_ldap_name, awx_ldap_settings in awx_ldap_group_settings.items():
awx_ldap_enabled, ldap_missing_fields = self.is_enabled(awx_ldap_settings, self.DAB_LDAP_AUTHENTICATOR_KEYS)
if awx_ldap_enabled:
data.append(
self.format_config_data(
awx_ldap_enabled,
awx_ldap_settings,
"ldap",
self.DAB_LDAP_AUTHENTICATOR_KEYS,
f"LDAP_{awx_ldap_name}",
)
)
else:
data.append({f"LDAP_{awx_ldap_name}_missing_fields": ldap_missing_fields})
# write to file if requested
if options["output_file"]:
# Define the path for the output JSON file

View File

@@ -93,7 +93,7 @@ class DisableLocalAuthMiddleware(MiddlewareMixin):
user = request.user
if not user.pk:
return
if not (user.profile.ldap_dn or user.social_auth.exists() or user.enterprise_auth.exists()):
if not (user.social_auth.exists() or user.enterprise_auth.exists()):
logout(request)

View File

@@ -0,0 +1,16 @@
# Generated by Django 4.2.10 on 2024-08-09 16:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0195_EE_permissions'),
]
operations = [
migrations.DeleteModel(
name='Profile',
),
]

View File

@@ -18,7 +18,7 @@ from ansible_base.lib.utils.models import user_summary_fields
# AWX
from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa
from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate, StdoutMaxBytesExceeded # noqa
from awx.main.models.organization import Organization, Profile, Team, UserSessionMembership # noqa
from awx.main.models.organization import Organization, Team, UserSessionMembership # noqa
from awx.main.models.credential import Credential, CredentialType, CredentialInputSource, ManagedCredentialType, build_safe_env # noqa
from awx.main.models.projects import Project, ProjectUpdate # noqa
from awx.main.models.receptor_address import ReceptorAddress # noqa
@@ -292,7 +292,6 @@ activity_stream_registrar.connect(Job)
activity_stream_registrar.connect(AdHocCommand)
# activity_stream_registrar.connect(JobHostSummary)
# activity_stream_registrar.connect(JobEvent)
# activity_stream_registrar.connect(Profile)
activity_stream_registrar.connect(Schedule)
activity_stream_registrar.connect(NotificationTemplate)
activity_stream_registrar.connect(Notification)

View File

@@ -15,8 +15,8 @@ from ansible_base.resource_registry.fields import AnsibleResourceField
# AWX
from awx.api.versioning import reverse
from awx.main.fields import AutoOneToOneField, ImplicitRoleField, OrderedManyToManyField
from awx.main.models.base import BaseModel, CommonModel, CommonModelNameNotUnique, CreatedModifiedModel, NotificationFieldsModel
from awx.main.fields import ImplicitRoleField, OrderedManyToManyField
from awx.main.models.base import BaseModel, CommonModel, CommonModelNameNotUnique, NotificationFieldsModel
from awx.main.models.rbac import (
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
ROLE_SINGLETON_SYSTEM_AUDITOR,
@@ -24,7 +24,7 @@ from awx.main.models.rbac import (
from awx.main.models.unified_jobs import UnifiedJob
from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin
__all__ = ['Organization', 'Team', 'Profile', 'UserSessionMembership']
__all__ = ['Organization', 'Team', 'UserSessionMembership']
class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin):
@@ -167,22 +167,6 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
return reverse('api:team_detail', kwargs={'pk': self.pk}, request=request)
class Profile(CreatedModifiedModel):
"""
Profile model related to User object. Currently stores LDAP DN for users
loaded from LDAP.
"""
class Meta:
app_label = 'main'
user = AutoOneToOneField('auth.User', related_name='profile', editable=False, on_delete=models.CASCADE)
ldap_dn = models.CharField(
max_length=1024,
default='',
)
class UserSessionMembership(BaseModel):
"""
A lookup table for API session membership given user. Note, there is a

View File

@@ -66,82 +66,6 @@ def test_awx_task_env_validity(get, patch, admin, value, expected):
assert resp.data['AWX_TASK_ENV'] == dict()
@pytest.mark.django_db
def test_ldap_settings(get, put, patch, delete, admin):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'})
get(url, user=admin, expect=200)
# The PUT below will fail at the moment because AUTH_LDAP_GROUP_TYPE
# defaults to None but cannot be set to None.
# put(url, user=admin, data=response.data, expect=200)
delete(url, user=admin, expect=204)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': ''}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap.example.com'}, expect=400)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldaps://ldap.example.com'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com:389'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldaps://ldap.example.com:636'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com ldap://ldap2.example.com'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com,ldap://ldap2.example.com'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com, ldap://ldap2.example.com'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_BIND_DN': 'cn=Manager,dc=example,dc=com'}, expect=200)
patch(url, user=admin, data={'AUTH_LDAP_BIND_DN': u'cn=暴力膜,dc=大新闻,dc=真的粉丝'}, expect=200)
@pytest.mark.django_db
@pytest.mark.parametrize(
'value',
[
None,
'',
'INVALID',
1,
[1],
['INVALID'],
],
)
def test_ldap_user_flags_by_group_invalid_dn(get, patch, admin, value):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'})
patch(url, user=admin, data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': value}}, expect=400)
@pytest.mark.django_db
def test_ldap_user_flags_by_group_string(get, patch, admin):
expected = 'CN=Admins,OU=Groups,DC=example,DC=com'
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'})
patch(url, user=admin, data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': expected}}, expect=200)
resp = get(url, user=admin)
assert resp.data['AUTH_LDAP_USER_FLAGS_BY_GROUP']['is_superuser'] == [expected]
@pytest.mark.django_db
def test_ldap_user_flags_by_group_list(get, patch, admin):
expected = ['CN=Admins,OU=Groups,DC=example,DC=com', 'CN=Superadmins,OU=Groups,DC=example,DC=com']
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'})
patch(url, user=admin, data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': expected}}, expect=200)
resp = get(url, user=admin)
assert resp.data['AUTH_LDAP_USER_FLAGS_BY_GROUP']['is_superuser'] == expected
@pytest.mark.parametrize(
'setting',
[
'AUTH_LDAP_USER_DN_TEMPLATE',
'AUTH_LDAP_REQUIRE_GROUP',
'AUTH_LDAP_DENY_GROUP',
],
)
@pytest.mark.django_db
def test_empty_ldap_dn(get, put, patch, delete, admin, setting):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'})
patch(url, user=admin, data={setting: ''}, expect=200)
resp = get(url, user=admin, expect=200)
assert resp.data[setting] is None
patch(url, user=admin, data={setting: None}, expect=200)
resp = get(url, user=admin, expect=200)
assert resp.data[setting] is None
@pytest.mark.django_db
def test_radius_settings(get, put, patch, delete, admin, settings):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'radius'})

View File

@@ -1,103 +0,0 @@
import ldap
import ldif
import pytest
import os
from mockldap import MockLdap
from awx.api.versioning import reverse
@pytest.fixture
def ldap_generator():
def fn(fname, host='localhost'):
fh = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), fname), 'rb')
ctrl = ldif.LDIFRecordList(fh)
ctrl.parse()
directory = dict(ctrl.all_records)
mockldap = MockLdap(directory)
mockldap.start()
mockldap['ldap://{}/'.format(host)]
conn = ldap.initialize('ldap://{}/'.format(host))
return conn
# mockldap.stop()
return fn
@pytest.fixture
def ldap_settings_generator():
def fn(prefix='', dc='ansible', host='ldap.ansible.com'):
prefix = '_{}'.format(prefix) if prefix else ''
data = {
'AUTH_LDAP_SERVER_URI': 'ldap://{}'.format(host),
'AUTH_LDAP_BIND_DN': 'cn=eng_user1,ou=people,dc={},dc=com'.format(dc),
'AUTH_LDAP_BIND_PASSWORD': 'password',
"AUTH_LDAP_USER_SEARCH": ["ou=people,dc={},dc=com".format(dc), "SCOPE_SUBTREE", "(cn=%(user)s)"],
"AUTH_LDAP_TEAM_MAP": {
"LDAP Sales": {"organization": "LDAP Organization", "users": "cn=sales,ou=groups,dc={},dc=com".format(dc), "remove": True},
"LDAP IT": {"organization": "LDAP Organization", "users": "cn=it,ou=groups,dc={},dc=com".format(dc), "remove": True},
"LDAP Engineering": {"organization": "LDAP Organization", "users": "cn=engineering,ou=groups,dc={},dc=com".format(dc), "remove": True},
},
"AUTH_LDAP_REQUIRE_GROUP": None,
"AUTH_LDAP_USER_ATTR_MAP": {"first_name": "givenName", "last_name": "sn", "email": "mail"},
"AUTH_LDAP_GROUP_SEARCH": ["dc={},dc=com".format(dc), "SCOPE_SUBTREE", "(objectClass=groupOfNames)"],
"AUTH_LDAP_USER_FLAGS_BY_GROUP": {"is_superuser": "cn=superusers,ou=groups,dc={},dc=com".format(dc)},
"AUTH_LDAP_ORGANIZATION_MAP": {
"LDAP Organization": {
"admins": "cn=engineering_admins,ou=groups,dc={},dc=com".format(dc),
"remove_admins": False,
"users": [
"cn=engineering,ou=groups,dc={},dc=com".format(dc),
"cn=sales,ou=groups,dc={},dc=com".format(dc),
"cn=it,ou=groups,dc={},dc=com".format(dc),
],
"remove_users": False,
}
},
}
if prefix:
data_new = dict()
for k, v in data.items():
k_new = k.replace('AUTH_LDAP', 'AUTH_LDAP{}'.format(prefix))
data_new[k_new] = v
else:
data_new = data
return data_new
return fn
# Note: mockldap isn't fully featured. Fancy queries aren't fully baked.
# However, objects returned are solid so they should flow through django ldap middleware nicely.
@pytest.mark.skip(reason="Needs Update - CA")
@pytest.mark.django_db
def test_login(ldap_generator, patch, post, admin, ldap_settings_generator):
auth_url = reverse('api:auth_token_view')
ldap_settings_url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'})
# Generate mock ldap servers and init with ldap data
ldap_generator("../data/ldap_example.ldif", "ldap.example.com")
ldap_generator("../data/ldap_redhat.ldif", "ldap.redhat.com")
ldap_generator("../data/ldap_ansible.ldif", "ldap.ansible.com")
ldap_settings_example = ldap_settings_generator(dc='example')
ldap_settings_ansible = ldap_settings_generator(prefix='1', dc='ansible')
ldap_settings_redhat = ldap_settings_generator(prefix='2', dc='redhat')
# eng_user1 exists in ansible and redhat but not example
patch(ldap_settings_url, user=admin, data=ldap_settings_example, expect=200)
post(auth_url, data={'username': 'eng_user1', 'password': 'password'}, expect=400)
patch(ldap_settings_url, user=admin, data=ldap_settings_ansible, expect=200)
patch(ldap_settings_url, user=admin, data=ldap_settings_redhat, expect=200)
post(auth_url, data={'username': 'eng_user1', 'password': 'password'}, expect=200)

View File

@@ -28,21 +28,6 @@ settings_dict = {
}
},
"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,
}
@@ -93,27 +78,6 @@ class TestDumpAuthConfigCommand(TestCase):
"IDP_ATTR_USER_PERMANENT_ID": "name_id",
},
},
{
"type": "ansible_base.authentication.authenticator_plugins.ldap",
"name": "LDAP_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):
@@ -123,10 +87,3 @@ class TestDumpAuthConfigCommand(TestCase):
# check configured SAML return
assert cmmd_output[0] == self.expected_config[0]
# check configured LDAP return
assert cmmd_output[2] == self.expected_config[1]
# check unconfigured LDAP return
assert "LDAP_0_missing_fields" in cmmd_output[1]
assert cmmd_output[1]["LDAP_0_missing_fields"] == ['SERVER_URI', 'GROUP_TYPE', 'GROUP_TYPE_PARAMS', 'USER_DN_TEMPLATE', 'USER_ATTR_MAP']