1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-26 07:55:24 +03:00

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

@ -33,8 +33,6 @@ MAIN_NODE_TYPE ?= hybrid
PGBOUNCER ?= false
# If set to true docker-compose will also start a keycloak instance
KEYCLOAK ?= false
# If set to true docker-compose will also start an ldap instance
LDAP ?= false
# If set to true docker-compose will also start a splunk instance
SPLUNK ?= false
# If set to true docker-compose will also start a prometheus instance
@ -508,7 +506,6 @@ docker-compose-sources: .git/hooks/pre-commit
-e minikube_container_group=$(MINIKUBE_CONTAINER_GROUP) \
-e enable_pgbouncer=$(PGBOUNCER) \
-e enable_keycloak=$(KEYCLOAK) \
-e enable_ldap=$(LDAP) \
-e enable_splunk=$(SPLUNK) \
-e enable_prometheus=$(PROMETHEUS) \
-e enable_grafana=$(GRAFANA) \
@ -525,8 +522,7 @@ docker-compose: awx/projects docker-compose-sources
ansible-galaxy install --ignore-certs -r tools/docker-compose/ansible/requirements.yml;
$(ANSIBLE_PLAYBOOK) -i tools/docker-compose/inventory tools/docker-compose/ansible/initialize_containers.yml \
-e enable_vault=$(VAULT) \
-e vault_tls=$(VAULT_TLS) \
-e enable_ldap=$(LDAP); \
-e vault_tls=$(VAULT_TLS); \
$(MAKE) docker-compose-up
docker-compose-up:
@ -598,7 +594,7 @@ docker-clean:
-$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);)
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
docker volume rm -f tools_var_lib_awx tools_awx_db tools_awx_db_15 tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(shell docker volume ls --filter name=tools_redis_socket_ -q)
docker volume rm -f tools_var_lib_awx tools_awx_db tools_awx_db_15 tools_vault_1 tools_grafana_storage tools_prometheus_storage $(shell docker volume ls --filter name=tools_redis_socket_ -q)
docker-refresh: docker-clean docker-compose

View File

@ -961,7 +961,6 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
class UserSerializer(BaseSerializer):
password = serializers.CharField(required=False, default='', help_text=_('Field used to change the password.'))
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service'))
is_system_auditor = serializers.BooleanField(default=False)
show_capabilities = ['edit', 'delete']
@ -979,7 +978,6 @@ class UserSerializer(BaseSerializer):
'is_superuser',
'is_system_auditor',
'password',
'ldap_dn',
'last_login',
'external_account',
)
@ -1028,8 +1026,10 @@ class UserSerializer(BaseSerializer):
def _update_password(self, obj, new_password):
# For now we're not raising an error, just not saving password for
# users managed by LDAP who already have an unusable password set.
# Get external password will return something like ldap or enterprise or None if the user isn't external. We only want to allow a password update for a None option
# users managed by external authentication services (who already have an unusable password set).
# get_external_account function will return something like social or enterprise when the user is external,
# and return None when the user isn't external.
# We want to allow a password update only for non-external accounts.
if new_password and new_password != '$encrypted$' and not self.get_external_account(obj):
obj.set_password(new_password)
obj.save(update_fields=['password'])
@ -1085,37 +1085,6 @@ class UserSerializer(BaseSerializer):
)
return res
def _validate_ldap_managed_field(self, value, field_name):
if not getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
return value
try:
is_ldap_user = bool(self.instance and self.instance.profile.ldap_dn)
except AttributeError:
is_ldap_user = False
if is_ldap_user:
ldap_managed_fields = ['username']
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys())
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
if field_name in ldap_managed_fields:
if value != getattr(self.instance, field_name):
raise serializers.ValidationError(_('Unable to change %s on user managed by LDAP.') % field_name)
return value
def validate_username(self, value):
return self._validate_ldap_managed_field(value, 'username')
def validate_first_name(self, value):
return self._validate_ldap_managed_field(value, 'first_name')
def validate_last_name(self, value):
return self._validate_ldap_managed_field(value, 'last_name')
def validate_email(self, value):
return self._validate_ldap_managed_field(value, 'email')
def validate_is_superuser(self, value):
return self._validate_ldap_managed_field(value, 'is_superuser')
class UserActivityStreamSerializer(UserSerializer):
"""Changes to system auditor status are shown as separate entries,

View File

@ -295,15 +295,6 @@ class ApiV2ConfigView(APIView):
become_methods=PRIVILEGE_ESCALATION_METHODS,
)
# If LDAP is enabled, user_ldap_fields will return a list of field
# names that are managed by LDAP and should be read-only for users with
# a non-empty ldap_dn attribute.
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
user_ldap_fields = ['username', 'password']
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys())
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
data['user_ldap_fields'] = user_ldap_fields
if (
request.user.is_superuser
or request.user.is_system_auditor

View File

@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# AWX
from awx.conf.migrations._ldap_group_type import fill_ldap_group_type_params
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [('conf', '0005_v330_rename_two_session_settings')]
operations = [migrations.RunPython(fill_ldap_group_type_params)]
# this migration is doing nothing, and is here to preserve migrations files integrity
operations = []

View File

@ -0,0 +1,115 @@
from django.db import migrations
LDAP_AUTH_CONF_KEYS = [
'AUTH_LDAP_SERVER_URI',
'AUTH_LDAP_BIND_DN',
'AUTH_LDAP_BIND_PASSWORD',
'AUTH_LDAP_START_TLS',
'AUTH_LDAP_CONNECTION_OPTIONS',
'AUTH_LDAP_USER_SEARCH',
'AUTH_LDAP_USER_DN_TEMPLATE',
'AUTH_LDAP_USER_ATTR_MAP',
'AUTH_LDAP_GROUP_SEARCH',
'AUTH_LDAP_GROUP_TYPE',
'AUTH_LDAP_GROUP_TYPE_PARAMS',
'AUTH_LDAP_REQUIRE_GROUP',
'AUTH_LDAP_DENY_GROUP',
'AUTH_LDAP_USER_FLAGS_BY_GROUP',
'AUTH_LDAP_ORGANIZATION_MAP',
'AUTH_LDAP_TEAM_MAP',
'AUTH_LDAP_1_SERVER_URI',
'AUTH_LDAP_1_BIND_DN',
'AUTH_LDAP_1_BIND_PASSWORD',
'AUTH_LDAP_1_START_TLS',
'AUTH_LDAP_1_CONNECTION_OPTIONS',
'AUTH_LDAP_1_USER_SEARCH',
'AUTH_LDAP_1_USER_DN_TEMPLATE',
'AUTH_LDAP_1_USER_ATTR_MAP',
'AUTH_LDAP_1_GROUP_SEARCH',
'AUTH_LDAP_1_GROUP_TYPE',
'AUTH_LDAP_1_GROUP_TYPE_PARAMS',
'AUTH_LDAP_1_REQUIRE_GROUP',
'AUTH_LDAP_1_DENY_GROUP',
'AUTH_LDAP_1_USER_FLAGS_BY_GROUP',
'AUTH_LDAP_1_ORGANIZATION_MAP',
'AUTH_LDAP_1_TEAM_MAP',
'AUTH_LDAP_2_SERVER_URI',
'AUTH_LDAP_2_BIND_DN',
'AUTH_LDAP_2_BIND_PASSWORD',
'AUTH_LDAP_2_START_TLS',
'AUTH_LDAP_2_CONNECTION_OPTIONS',
'AUTH_LDAP_2_USER_SEARCH',
'AUTH_LDAP_2_USER_DN_TEMPLATE',
'AUTH_LDAP_2_USER_ATTR_MAP',
'AUTH_LDAP_2_GROUP_SEARCH',
'AUTH_LDAP_2_GROUP_TYPE',
'AUTH_LDAP_2_GROUP_TYPE_PARAMS',
'AUTH_LDAP_2_REQUIRE_GROUP',
'AUTH_LDAP_2_DENY_GROUP',
'AUTH_LDAP_2_USER_FLAGS_BY_GROUP',
'AUTH_LDAP_2_ORGANIZATION_MAP',
'AUTH_LDAP_2_TEAM_MAP',
'AUTH_LDAP_3_SERVER_URI',
'AUTH_LDAP_3_BIND_DN',
'AUTH_LDAP_3_BIND_PASSWORD',
'AUTH_LDAP_3_START_TLS',
'AUTH_LDAP_3_CONNECTION_OPTIONS',
'AUTH_LDAP_3_USER_SEARCH',
'AUTH_LDAP_3_USER_DN_TEMPLATE',
'AUTH_LDAP_3_USER_ATTR_MAP',
'AUTH_LDAP_3_GROUP_SEARCH',
'AUTH_LDAP_3_GROUP_TYPE',
'AUTH_LDAP_3_GROUP_TYPE_PARAMS',
'AUTH_LDAP_3_REQUIRE_GROUP',
'AUTH_LDAP_3_DENY_GROUP',
'AUTH_LDAP_3_USER_FLAGS_BY_GROUP',
'AUTH_LDAP_3_ORGANIZATION_MAP',
'AUTH_LDAP_3_TEAM_MAP',
'AUTH_LDAP_4_SERVER_URI',
'AUTH_LDAP_4_BIND_DN',
'AUTH_LDAP_4_BIND_PASSWORD',
'AUTH_LDAP_4_START_TLS',
'AUTH_LDAP_4_CONNECTION_OPTIONS',
'AUTH_LDAP_4_USER_SEARCH',
'AUTH_LDAP_4_USER_DN_TEMPLATE',
'AUTH_LDAP_4_USER_ATTR_MAP',
'AUTH_LDAP_4_GROUP_SEARCH',
'AUTH_LDAP_4_GROUP_TYPE',
'AUTH_LDAP_4_GROUP_TYPE_PARAMS',
'AUTH_LDAP_4_REQUIRE_GROUP',
'AUTH_LDAP_4_DENY_GROUP',
'AUTH_LDAP_4_USER_FLAGS_BY_GROUP',
'AUTH_LDAP_4_ORGANIZATION_MAP',
'AUTH_LDAP_4_TEAM_MAP',
'AUTH_LDAP_5_SERVER_URI',
'AUTH_LDAP_5_BIND_DN',
'AUTH_LDAP_5_BIND_PASSWORD',
'AUTH_LDAP_5_START_TLS',
'AUTH_LDAP_5_CONNECTION_OPTIONS',
'AUTH_LDAP_5_USER_SEARCH',
'AUTH_LDAP_5_USER_DN_TEMPLATE',
'AUTH_LDAP_5_USER_ATTR_MAP',
'AUTH_LDAP_5_GROUP_SEARCH',
'AUTH_LDAP_5_GROUP_TYPE',
'AUTH_LDAP_5_GROUP_TYPE_PARAMS',
'AUTH_LDAP_5_REQUIRE_GROUP',
'AUTH_LDAP_5_DENY_GROUP',
'AUTH_LDAP_5_USER_FLAGS_BY_GROUP',
'AUTH_LDAP_5_ORGANIZATION_MAP',
'AUTH_LDAP_5_TEAM_MAP',
]
def remove_ldap_auth_conf(apps, scheme_editor):
setting = apps.get_model('conf', 'Setting')
setting.objects.filter(key__in=LDAP_AUTH_CONF_KEYS).delete()
class Migration(migrations.Migration):
dependencies = [
('conf', '0010_change_to_JSONField'),
]
operations = [
migrations.RunPython(remove_ldap_auth_conf),
]

View File

@ -1,31 +0,0 @@
import inspect
from django.conf import settings
import logging
logger = logging.getLogger('awx.conf.migrations')
def fill_ldap_group_type_params(apps, schema_editor):
group_type = getattr(settings, 'AUTH_LDAP_GROUP_TYPE', None)
Setting = apps.get_model('conf', 'Setting')
group_type_params = {'name_attr': 'cn', 'member_attr': 'member'}
qs = Setting.objects.filter(key='AUTH_LDAP_GROUP_TYPE_PARAMS')
entry = None
if qs.exists():
entry = qs[0]
group_type_params = entry.value
else:
return # for new installs we prefer to use the default value
init_attrs = set(inspect.getfullargspec(group_type.__init__).args[1:])
for k in list(group_type_params.keys()):
if k not in init_attrs:
del group_type_params[k]
entry.value = group_type_params
logger.warning(f'Migration updating AUTH_LDAP_GROUP_TYPE_PARAMS with value {entry.value}')
entry.save()

View File

@ -73,6 +73,6 @@ def disable_local_auth(**kwargs):
logger.warning("Triggering token invalidation for local users.")
qs = User.objects.filter(profile__ldap_dn='', enterprise_auth__isnull=True, social_auth__isnull=True)
qs = User.objects.filter(enterprise_auth__isnull=True, social_auth__isnull=True)
revoke_tokens(RefreshToken.objects.filter(revoked=None, user__in=qs))
revoke_tokens(OAuth2AccessToken.objects.filter(user__in=qs))

View File

@ -1,25 +0,0 @@
import pytest
from awx.conf.migrations._ldap_group_type import fill_ldap_group_type_params
from awx.conf.models import Setting
from django.apps import apps
@pytest.mark.django_db
def test_fill_group_type_params_no_op():
fill_ldap_group_type_params(apps, 'dont-use-me')
assert Setting.objects.count() == 0
@pytest.mark.django_db
def test_keep_old_setting_with_default_value():
Setting.objects.create(key='AUTH_LDAP_GROUP_TYPE', value={'name_attr': 'cn', 'member_attr': 'member'})
fill_ldap_group_type_params(apps, 'dont-use-me')
assert Setting.objects.count() == 1
s = Setting.objects.first()
assert s.value == {'name_attr': 'cn', 'member_attr': 'member'}
# NOTE: would be good to test the removal of attributes by migration
# but this requires fighting with the validator and is not done here

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']

View File

@ -9,8 +9,6 @@ import tempfile
import socket
from datetime import timedelta
# python-ldap
import ldap
from split_settings.tools import include
@ -394,12 +392,6 @@ REST_FRAMEWORK = {
}
AUTHENTICATION_BACKENDS = (
'awx.sso.backends.LDAPBackend',
'awx.sso.backends.LDAPBackend1',
'awx.sso.backends.LDAPBackend2',
'awx.sso.backends.LDAPBackend3',
'awx.sso.backends.LDAPBackend4',
'awx.sso.backends.LDAPBackend5',
'awx.sso.backends.RADIUSBackend',
'awx.sso.backends.TACACSPlusBackend',
'social_core.backends.google.GoogleOAuth2',
@ -425,14 +417,6 @@ OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken"
OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, 'REFRESH_TOKEN_EXPIRE_SECONDS': 2628000}
ALLOW_OAUTH2_FOR_EXTERNAL_USERS = False
# LDAP server (default to None to skip using LDAP authentication).
# Note: This setting may be overridden by database settings.
AUTH_LDAP_SERVER_URI = None
# Disable LDAP referrals by default (to prevent certain LDAP queries from
# hanging with AD).
# Note: This setting may be overridden by database settings.
AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS: 0, ldap.OPT_NETWORK_TIMEOUT: 30}
# Radius server settings (default to empty string to skip using Radius auth).
# Note: These settings may be overridden by database settings.
@ -932,7 +916,6 @@ LOGGING = {
'awx.analytics.broadcast_websocket': {'handlers': ['console', 'file', 'wsrelay', 'external_logger'], 'level': 'INFO', 'propagate': False},
'awx.analytics.performance': {'handlers': ['console', 'file', 'tower_warnings', 'external_logger'], 'level': 'DEBUG', 'propagate': False},
'awx.analytics.job_lifecycle': {'handlers': ['console', 'job_lifecycle'], 'level': 'DEBUG', 'propagate': False},
'django_auth_ldap': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'},
'social': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'},
'system_tracking_migrations': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'},
'rbac_migrations': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'},

View File

@ -2,26 +2,14 @@
# All Rights Reserved.
# Python
from collections import OrderedDict
import logging
import uuid
import ldap
# Django
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.conf import settings as django_settings
from django.core.signals import setting_changed
from django.utils.encoding import force_str
from django.http import HttpResponse
# django-auth-ldap
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
from django_auth_ldap.backend import populate_user
from django.core.exceptions import ImproperlyConfigured
# radiusauth
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
@ -35,143 +23,10 @@ from social_core.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityPr
# Ansible Tower
from awx.sso.models import UserEnterpriseAuth
from awx.sso.common import create_org_and_teams, reconcile_users_org_team_mappings
logger = logging.getLogger('awx.sso.backends')
class LDAPSettings(BaseLDAPSettings):
defaults = dict(list(BaseLDAPSettings.defaults.items()) + list({'ORGANIZATION_MAP': {}, 'TEAM_MAP': {}, 'GROUP_TYPE_PARAMS': {}}.items()))
def __init__(self, prefix='AUTH_LDAP_', defaults={}):
super(LDAPSettings, self).__init__(prefix, defaults)
# If a DB-backed setting is specified that wipes out the
# OPT_NETWORK_TIMEOUT, fall back to a sane default
if ldap.OPT_NETWORK_TIMEOUT not in getattr(self, 'CONNECTION_OPTIONS', {}):
options = getattr(self, 'CONNECTION_OPTIONS', {})
options[ldap.OPT_NETWORK_TIMEOUT] = 30
self.CONNECTION_OPTIONS = options
# when specifying `.set_option()` calls for TLS in python-ldap, the
# *order* in which you invoke them *matters*, particularly in Python3,
# where dictionary insertion order is persisted
#
# specifically, it is *critical* that `ldap.OPT_X_TLS_NEWCTX` be set *last*
# this manual sorting puts `OPT_X_TLS_NEWCTX` *after* other TLS-related
# options
#
# see: https://github.com/python-ldap/python-ldap/issues/55
newctx_option = self.CONNECTION_OPTIONS.pop(ldap.OPT_X_TLS_NEWCTX, None)
self.CONNECTION_OPTIONS = OrderedDict(self.CONNECTION_OPTIONS)
if newctx_option is not None:
self.CONNECTION_OPTIONS[ldap.OPT_X_TLS_NEWCTX] = newctx_option
class LDAPBackend(BaseLDAPBackend):
"""
Custom LDAP backend for AWX.
"""
settings_prefix = 'AUTH_LDAP_'
def __init__(self, *args, **kwargs):
self._dispatch_uid = uuid.uuid4()
super(LDAPBackend, self).__init__(*args, **kwargs)
setting_changed.connect(self._on_setting_changed, dispatch_uid=self._dispatch_uid)
def _on_setting_changed(self, sender, **kwargs):
# If any AUTH_LDAP_* setting changes, force settings to be reloaded for
# this backend instance.
if kwargs.get('setting', '').startswith(self.settings_prefix):
self._settings = None
def _get_settings(self):
if self._settings is None:
self._settings = LDAPSettings(self.settings_prefix)
return self._settings
def _set_settings(self, settings):
self._settings = settings
settings = property(_get_settings, _set_settings)
def authenticate(self, request, username, password):
if self.settings.START_TLS and ldap.OPT_X_TLS_REQUIRE_CERT in self.settings.CONNECTION_OPTIONS:
# with python-ldap, if you want to set connection-specific TLS
# parameters, you must also specify OPT_X_TLS_NEWCTX = 0
# see: https://stackoverflow.com/a/29722445
# see: https://stackoverflow.com/a/38136255
self.settings.CONNECTION_OPTIONS[ldap.OPT_X_TLS_NEWCTX] = 0
if not self.settings.SERVER_URI:
return None
try:
user = User.objects.get(username=username)
if user and (not user.profile or not user.profile.ldap_dn):
return None
except User.DoesNotExist:
pass
try:
for setting_name, type_ in [('GROUP_SEARCH', 'LDAPSearch'), ('GROUP_TYPE', 'LDAPGroupType')]:
if getattr(self.settings, setting_name) is None:
raise ImproperlyConfigured("{} must be an {} instance.".format(setting_name, type_))
ldap_user = super(LDAPBackend, self).authenticate(request, username, password)
# If we have an LDAP user and that user we found has an ldap_user internal object and that object has a bound connection
# Then we can try and force an unbind to close the sticky connection
if ldap_user and ldap_user.ldap_user and ldap_user.ldap_user._connection_bound:
logger.debug("Forcing LDAP connection to close")
try:
ldap_user.ldap_user._connection.unbind_s()
ldap_user.ldap_user._connection_bound = False
except Exception:
logger.exception(f"Got unexpected LDAP exception when forcing LDAP disconnect for user {ldap_user}, login will still proceed")
return ldap_user
except Exception:
logger.exception("Encountered an error authenticating to LDAP")
return None
def get_user(self, user_id):
if not self.settings.SERVER_URI:
return None
return super(LDAPBackend, self).get_user(user_id)
# Disable any LDAP based authorization / permissions checking.
def has_perm(self, user, perm, obj=None):
return False
def has_module_perms(self, user, app_label):
return False
def get_all_permissions(self, user, obj=None):
return set()
def get_group_permissions(self, user, obj=None):
return set()
class LDAPBackend1(LDAPBackend):
settings_prefix = 'AUTH_LDAP_1_'
class LDAPBackend2(LDAPBackend):
settings_prefix = 'AUTH_LDAP_2_'
class LDAPBackend3(LDAPBackend):
settings_prefix = 'AUTH_LDAP_3_'
class LDAPBackend4(LDAPBackend):
settings_prefix = 'AUTH_LDAP_4_'
class LDAPBackend5(LDAPBackend):
settings_prefix = 'AUTH_LDAP_5_'
def _decorate_enterprise_user(user, provider):
user.set_unusable_password()
user.save()
@ -348,122 +203,3 @@ class SAMLAuth(BaseSAMLAuth):
):
return None
return super(SAMLAuth, self).get_user(user_id)
def _update_m2m_from_groups(ldap_user, opts, remove=True):
"""
Hepler function to evaluate the LDAP team/org options to determine if LDAP user should
be a member of the team/org based on their ldap group dns.
Returns:
True - User should be added
False - User should be removed
None - Users membership should not be changed
"""
if opts is None:
return None
elif not opts:
pass
elif isinstance(opts, bool) and opts is True:
return True
else:
if isinstance(opts, str):
opts = [opts]
# If any of the users groups matches any of the list options
for group_dn in opts:
if not isinstance(group_dn, str):
continue
if ldap_user._get_groups().is_member_of(group_dn):
return True
if remove:
return False
return None
@receiver(populate_user, dispatch_uid='populate-ldap-user')
def on_populate_user(sender, **kwargs):
"""
Handle signal from LDAP backend to populate the user object. Update user
organization/team memberships according to their LDAP groups.
"""
user = kwargs['user']
ldap_user = kwargs['ldap_user']
backend = ldap_user.backend
# Boolean to determine if we should force an user update
# to avoid duplicate SQL update statements
force_user_update = False
# Prefetch user's groups to prevent LDAP queries for each org/team when
# checking membership.
ldap_user._get_groups().get_group_dns()
# If the LDAP user has a first or last name > $maxlen chars, truncate it
for field in ('first_name', 'last_name'):
max_len = User._meta.get_field(field).max_length
field_len = len(getattr(user, field))
if field_len > max_len:
setattr(user, field, getattr(user, field)[:max_len])
force_user_update = True
logger.warning('LDAP user {} has {} > max {} characters'.format(user.username, field, max_len))
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
team_map_settings = getattr(backend.settings, 'TEAM_MAP', {})
orgs_list = list(org_map.keys())
team_map = {}
for team_name, team_opts in team_map_settings.items():
if not team_opts.get('organization', None):
# You can't save the LDAP config in the UI w/o an org (or '' or null as the org) so if we somehow got this condition its an error
logger.error("Team named {} in LDAP team map settings is invalid due to missing organization".format(team_name))
continue
team_map[team_name] = team_opts['organization']
create_org_and_teams(orgs_list, team_map, 'LDAP')
# Compute in memory what the state is of the different LDAP orgs
org_roles_and_ldap_attributes = {'admin_role': 'admins', 'auditor_role': 'auditors', 'member_role': 'users'}
desired_org_states = {}
for org_name, org_opts in org_map.items():
remove = bool(org_opts.get('remove', True))
desired_org_states[org_name] = {}
for org_role_name in org_roles_and_ldap_attributes.keys():
ldap_name = org_roles_and_ldap_attributes[org_role_name]
opts = org_opts.get(ldap_name, None)
remove = bool(org_opts.get('remove_{}'.format(ldap_name), remove))
desired_org_states[org_name][org_role_name] = _update_m2m_from_groups(ldap_user, opts, remove)
# If everything returned None (because there was no configuration) we can remove this org from our map
# This will prevent us from loading the org in the next query
if all(desired_org_states[org_name][org_role_name] is None for org_role_name in org_roles_and_ldap_attributes.keys()):
del desired_org_states[org_name]
# Compute in memory what the state is of the different LDAP teams
desired_team_states = {}
for team_name, team_opts in team_map_settings.items():
if 'organization' not in team_opts:
continue
users_opts = team_opts.get('users', None)
remove = bool(team_opts.get('remove', True))
state = _update_m2m_from_groups(ldap_user, users_opts, remove)
if state is not None:
organization = team_opts['organization']
if organization not in desired_team_states:
desired_team_states[organization] = {}
desired_team_states[organization][team_name] = {'member_role': state}
# Check if user.profile is available, otherwise force user.save()
try:
_ = user.profile
except ValueError:
force_user_update = True
finally:
if force_user_update:
user.save()
# Update user profile to store LDAP DN.
profile = user.profile
if profile.ldap_dn != ldap_user.dn:
profile.ldap_dn = ldap_user.dn
profile.save()
reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, 'LDAP')

View File

@ -113,7 +113,7 @@ def create_org_and_teams(org_list, team_map, adapter, can_create=True):
logger.debug(f"Adapter {adapter} is not allowed to create orgs/teams")
return
# Get all of the IDs and names of orgs in the DB and create any new org defined in LDAP that does not exist in the DB
# Get all of the IDs and names of orgs in the DB and create any new org defined in org_list that does not exist in the DB
existing_orgs = get_orgs_by_ids()
# Parse through orgs and teams provided and create a list of unique items we care about creating
@ -174,18 +174,6 @@ def get_or_create_org_with_default_galaxy_cred(**kwargs):
def get_external_account(user):
account_type = None
# Previously this method also checked for active configuration which meant that if a user logged in from LDAP
# and then LDAP was no longer configured it would "convert" the user from an LDAP account_type to none.
# This did have one benefit that if a login type was removed intentionally the user could be given a username password.
# But it had a limitation that the user would have to have an active session (or an admin would have to go set a temp password).
# It also lead to the side affect that if LDAP was ever reconfigured the user would convert back to LDAP but still have a local password.
# That local password could then be used to bypass LDAP authentication.
try:
if user.pk and user.profile.ldap_dn and not user.has_usable_password():
account_type = "ldap"
except AttributeError:
pass
if user.social_auth.all():
account_type = "social"
@ -198,9 +186,8 @@ def get_external_account(user):
def is_remote_auth_enabled():
from django.conf import settings
# Append LDAP, Radius, TACACS+ and SAML options
# Append Radius, TACACS+ and SAML options
settings_that_turn_on_remote_auth = [
'AUTH_LDAP_SERVER_URI',
'SOCIAL_AUTH_SAML_ENABLED_IDPS',
'RADIUS_SERVER',
'TACACSPLUS_HOST',

View File

@ -14,18 +14,6 @@ from rest_framework import serializers
from awx.conf import register, register_validate, fields
from awx.sso.fields import (
AuthenticationBackendsField,
LDAPConnectionOptionsField,
LDAPDNField,
LDAPDNWithUserField,
LDAPGroupTypeField,
LDAPGroupTypeParamsField,
LDAPOrganizationMapField,
LDAPSearchField,
LDAPSearchUnionField,
LDAPServerURIField,
LDAPTeamMapField,
LDAPUserAttrMapField,
LDAPUserFlagsField,
SAMLContactField,
SAMLEnabledIdPsField,
SAMLOrgAttrField,
@ -37,7 +25,7 @@ from awx.sso.fields import (
SocialTeamMapField,
)
from awx.main.validators import validate_private_key, validate_certificate
from awx.sso.validators import validate_ldap_bind_dn, validate_tacacsplus_disallow_nonascii # noqa
from awx.sso.validators import validate_tacacsplus_disallow_nonascii # noqa
class SocialAuthCallbackURL(object):
@ -159,297 +147,6 @@ if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
category_slug='authentication',
)
###############################################################################
# LDAP AUTHENTICATION SETTINGS
###############################################################################
def _register_ldap(append=None):
append_str = '_{}'.format(append) if append else ''
register(
'AUTH_LDAP{}_SERVER_URI'.format(append_str),
field_class=LDAPServerURIField,
allow_blank=True,
default='',
label=_('LDAP Server URI'),
help_text=_(
'URI to connect to LDAP server, such as "ldap://ldap.example.com:389" '
'(non-SSL) or "ldaps://ldap.example.com:636" (SSL). Multiple LDAP '
'servers may be specified by separating with spaces or commas. LDAP '
'authentication is disabled if this parameter is empty.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder='ldaps://ldap.example.com:636',
)
register(
'AUTH_LDAP{}_BIND_DN'.format(append_str),
field_class=fields.CharField,
allow_blank=True,
default='',
validators=[validate_ldap_bind_dn],
label=_('LDAP Bind DN'),
help_text=_(
'DN (Distinguished Name) of user to bind for all search queries. This'
' is the system user account we will use to login to query LDAP for other'
' user information. Refer to the documentation for example syntax.'
),
category=_('LDAP'),
category_slug='ldap',
)
register(
'AUTH_LDAP{}_BIND_PASSWORD'.format(append_str),
field_class=fields.CharField,
allow_blank=True,
default='',
label=_('LDAP Bind Password'),
help_text=_('Password used to bind LDAP user account.'),
category=_('LDAP'),
category_slug='ldap',
encrypted=True,
)
register(
'AUTH_LDAP{}_START_TLS'.format(append_str),
field_class=fields.BooleanField,
default=False,
label=_('LDAP Start TLS'),
help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'),
category=_('LDAP'),
category_slug='ldap',
)
register(
'AUTH_LDAP{}_CONNECTION_OPTIONS'.format(append_str),
field_class=LDAPConnectionOptionsField,
default={'OPT_REFERRALS': 0, 'OPT_NETWORK_TIMEOUT': 30},
label=_('LDAP Connection Options'),
help_text=_(
'Additional options to set for the LDAP connection. LDAP '
'referrals are disabled by default (to prevent certain LDAP '
'queries from hanging with AD). Option names should be strings '
'(e.g. "OPT_REFERRALS"). Refer to '
'https://www.python-ldap.org/doc/html/ldap.html#options for '
'possible options and values that can be set.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder=collections.OrderedDict([('OPT_REFERRALS', 0), ('OPT_NETWORK_TIMEOUT', 30)]),
)
register(
'AUTH_LDAP{}_USER_SEARCH'.format(append_str),
field_class=LDAPSearchUnionField,
default=[],
label=_('LDAP User Search'),
help_text=_(
'LDAP search query to find users. Any user that matches the given '
'pattern will be able to login to the service. The user should also be '
'mapped into an organization (as defined in the '
'AUTH_LDAP_ORGANIZATION_MAP setting). If multiple search queries '
'need to be supported use of "LDAPUnion" is possible. See '
'the documentation for details.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder=('OU=Users,DC=example,DC=com', 'SCOPE_SUBTREE', '(sAMAccountName=%(user)s)'),
)
register(
'AUTH_LDAP{}_USER_DN_TEMPLATE'.format(append_str),
field_class=LDAPDNWithUserField,
allow_blank=True,
allow_null=True,
default=None,
label=_('LDAP User DN Template'),
help_text=_(
'Alternative to user search, if user DNs are all of the same '
'format. This approach is more efficient for user lookups than '
'searching if it is usable in your organizational environment. If '
'this setting has a value it will be used instead of '
'AUTH_LDAP_USER_SEARCH.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder='uid=%(user)s,OU=Users,DC=example,DC=com',
)
register(
'AUTH_LDAP{}_USER_ATTR_MAP'.format(append_str),
field_class=LDAPUserAttrMapField,
default={},
label=_('LDAP User Attribute Map'),
help_text=_(
'Mapping of LDAP user schema to API user attributes. The default'
' setting is valid for ActiveDirectory but users with other LDAP'
' configurations may need to change the values. Refer to the'
' documentation for additional details.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder=collections.OrderedDict([('first_name', 'givenName'), ('last_name', 'sn'), ('email', 'mail')]),
)
register(
'AUTH_LDAP{}_GROUP_SEARCH'.format(append_str),
field_class=LDAPSearchField,
default=[],
label=_('LDAP Group Search'),
help_text=_(
'Users are mapped to organizations based on their membership in LDAP'
' groups. This setting defines the LDAP search query to find groups. '
'Unlike the user search, group search does not support LDAPSearchUnion.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder=('DC=example,DC=com', 'SCOPE_SUBTREE', '(objectClass=group)'),
)
register(
'AUTH_LDAP{}_GROUP_TYPE'.format(append_str),
field_class=LDAPGroupTypeField,
label=_('LDAP Group Type'),
help_text=_(
'The group type may need to be changed based on the type of the '
'LDAP server. Values are listed at: '
'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups'
),
category=_('LDAP'),
category_slug='ldap',
default='MemberDNGroupType',
depends_on=['AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str)],
)
register(
'AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str),
field_class=LDAPGroupTypeParamsField,
label=_('LDAP Group Type Parameters'),
help_text=_('Key value parameters to send the chosen group type init method.'),
category=_('LDAP'),
category_slug='ldap',
default=collections.OrderedDict([('member_attr', 'member'), ('name_attr', 'cn')]),
placeholder=collections.OrderedDict([('ldap_group_user_attr', 'legacyuid'), ('member_attr', 'member'), ('name_attr', 'cn')]),
depends_on=['AUTH_LDAP{}_GROUP_TYPE'.format(append_str)],
)
register(
'AUTH_LDAP{}_REQUIRE_GROUP'.format(append_str),
field_class=LDAPDNField,
allow_blank=True,
allow_null=True,
default=None,
label=_('LDAP Require Group'),
help_text=_(
'Group DN required to login. If specified, user must be a member '
'of this group to login via LDAP. If not set, everyone in LDAP '
'that matches the user search will be able to login to the service. '
'Only one require group is supported.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder='CN=Service Users,OU=Users,DC=example,DC=com',
)
register(
'AUTH_LDAP{}_DENY_GROUP'.format(append_str),
field_class=LDAPDNField,
allow_blank=True,
allow_null=True,
default=None,
label=_('LDAP Deny Group'),
help_text=_(
'Group DN denied from login. If specified, user will not be allowed to login if a member of this group. Only one deny group is supported.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com',
)
register(
'AUTH_LDAP{}_USER_FLAGS_BY_GROUP'.format(append_str),
field_class=LDAPUserFlagsField,
default={},
label=_('LDAP User Flags By Group'),
help_text=_(
'Retrieve users from a given group. At this time, superuser and system'
' auditors are the only groups supported. Refer to the'
' documentation for more detail.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder=collections.OrderedDict(
[('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), ('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com')]
),
)
register(
'AUTH_LDAP{}_ORGANIZATION_MAP'.format(append_str),
field_class=LDAPOrganizationMapField,
default={},
label=_('LDAP Organization Map'),
help_text=_(
'Mapping between organization admins/users and LDAP groups. This '
'controls which users are placed into which organizations '
'relative to their LDAP group memberships. Configuration details '
'are available in the documentation.'
),
category=_('LDAP'),
category_slug='ldap',
placeholder=collections.OrderedDict(
[
(
'Test Org',
collections.OrderedDict(
[
('admins', 'CN=Domain Admins,CN=Users,DC=example,DC=com'),
('auditors', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'),
('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']),
('remove_users', True),
('remove_admins', True),
]
),
),
(
'Test Org 2',
collections.OrderedDict(
[('admins', 'CN=Administrators,CN=Builtin,DC=example,DC=com'), ('users', True), ('remove_users', True), ('remove_admins', True)]
),
),
]
),
)
register(
'AUTH_LDAP{}_TEAM_MAP'.format(append_str),
field_class=LDAPTeamMapField,
default={},
label=_('LDAP Team Map'),
help_text=_('Mapping between team members (users) and LDAP groups. Configuration details are available in the documentation.'),
category=_('LDAP'),
category_slug='ldap',
placeholder=collections.OrderedDict(
[
(
'My Team',
collections.OrderedDict([('organization', 'Test Org'), ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), ('remove', True)]),
),
(
'Other Team',
collections.OrderedDict([('organization', 'Test Org 2'), ('users', 'CN=Other Users,CN=Users,DC=example,DC=com'), ('remove', False)]),
),
]
),
)
_register_ldap()
_register_ldap('1')
_register_ldap('2')
_register_ldap('3')
_register_ldap('4')
_register_ldap('5')
###############################################################################
# RADIUS AUTHENTICATION SETTINGS
###############################################################################

View File

@ -1,39 +1,20 @@
import collections
import copy
import inspect
import json
import re
import six
# Python LDAP
import ldap
import awx
# Django
from django.utils.translation import gettext_lazy as _
# Django Auth LDAP
import django_auth_ldap.config
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty, Field, SkipField
# This must be imported so get_subclasses picks it up
from awx.sso.ldap_group_types import PosixUIDGroupType # noqa
# AWX
from awx.conf import fields
from awx.main.validators import validate_certificate
from awx.sso.validators import ( # noqa
validate_ldap_dn,
validate_ldap_bind_dn,
validate_ldap_dn_with_user,
validate_ldap_filter,
validate_ldap_filter_with_user,
validate_tacacsplus_disallow_nonascii,
)
from awx.sso.validators import validate_tacacsplus_disallow_nonascii # noqa
def get_subclasses(cls):
@ -43,18 +24,6 @@ def get_subclasses(cls):
yield subclass
def find_class_in_modules(class_name):
"""
Used to find ldap subclasses by string
"""
module_search_space = [django_auth_ldap.config, awx.sso.ldap_group_types]
for m in module_search_space:
cls = getattr(m, class_name, None)
if cls:
return cls
return None
class DependsOnMixin:
def get_depends_on(self):
"""
@ -139,12 +108,6 @@ class AuthenticationBackendsField(fields.StringListField):
# authentication backend.
REQUIRED_BACKEND_SETTINGS = collections.OrderedDict(
[
('awx.sso.backends.LDAPBackend', ['AUTH_LDAP_SERVER_URI']),
('awx.sso.backends.LDAPBackend1', ['AUTH_LDAP_1_SERVER_URI']),
('awx.sso.backends.LDAPBackend2', ['AUTH_LDAP_2_SERVER_URI']),
('awx.sso.backends.LDAPBackend3', ['AUTH_LDAP_3_SERVER_URI']),
('awx.sso.backends.LDAPBackend4', ['AUTH_LDAP_4_SERVER_URI']),
('awx.sso.backends.LDAPBackend5', ['AUTH_LDAP_5_SERVER_URI']),
('awx.sso.backends.RADIUSBackend', ['RADIUS_SERVER']),
('social_core.backends.google.GoogleOAuth2', ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET']),
('social_core.backends.github.GithubOAuth2', ['SOCIAL_AUTH_GITHUB_KEY', 'SOCIAL_AUTH_GITHUB_SECRET']),
@ -230,310 +193,6 @@ class AuthenticationBackendsField(fields.StringListField):
return backends
class LDAPServerURIField(fields.URLField):
def __init__(self, **kwargs):
kwargs.setdefault('schemes', ('ldap', 'ldaps'))
kwargs.setdefault('allow_plain_hostname', True)
super(LDAPServerURIField, self).__init__(**kwargs)
def run_validators(self, value):
for url in filter(None, re.split(r'[, ]', (value or ''))):
super(LDAPServerURIField, self).run_validators(url)
return value
class LDAPConnectionOptionsField(fields.DictField):
default_error_messages = {'invalid_options': _('Invalid connection option(s): {invalid_options}.')}
def to_representation(self, value):
value = value or {}
opt_names = ldap.OPT_NAMES_DICT
# Convert integer options to their named constants.
repr_value = {}
for opt, opt_value in value.items():
if opt in opt_names:
repr_value[opt_names[opt]] = opt_value
return repr_value
def to_internal_value(self, data):
data = super(LDAPConnectionOptionsField, self).to_internal_value(data)
valid_options = dict([(v, k) for k, v in ldap.OPT_NAMES_DICT.items()])
invalid_options = set(data.keys()) - set(valid_options.keys())
if invalid_options:
invalid_options = sorted(list(invalid_options))
options_display = json.dumps(invalid_options).lstrip('[').rstrip(']')
self.fail('invalid_options', invalid_options=options_display)
# Convert named options to their integer constants.
internal_data = {}
for opt_name, opt_value in data.items():
internal_data[valid_options[opt_name]] = opt_value
return internal_data
class LDAPDNField(fields.CharField):
def __init__(self, **kwargs):
super(LDAPDNField, self).__init__(**kwargs)
self.validators.append(validate_ldap_dn)
def run_validation(self, data=empty):
value = super(LDAPDNField, self).run_validation(data)
# django-auth-ldap expects DN fields (like AUTH_LDAP_REQUIRE_GROUP)
# to be either a valid string or ``None`` (not an empty string)
return None if value == '' else value
class LDAPDNListField(fields.StringListField):
def __init__(self, **kwargs):
super(LDAPDNListField, self).__init__(**kwargs)
self.validators.append(lambda dn: list(map(validate_ldap_dn, dn)))
def run_validation(self, data=empty):
if not isinstance(data, (list, tuple)):
data = [data]
return super(LDAPDNListField, self).run_validation(data)
class LDAPDNWithUserField(fields.CharField):
def __init__(self, **kwargs):
super(LDAPDNWithUserField, self).__init__(**kwargs)
self.validators.append(validate_ldap_dn_with_user)
def run_validation(self, data=empty):
value = super(LDAPDNWithUserField, self).run_validation(data)
# django-auth-ldap expects DN fields (like AUTH_LDAP_USER_DN_TEMPLATE)
# to be either a valid string or ``None`` (not an empty string)
return None if value == '' else value
class LDAPFilterField(fields.CharField):
def __init__(self, **kwargs):
super(LDAPFilterField, self).__init__(**kwargs)
self.validators.append(validate_ldap_filter)
class LDAPFilterWithUserField(fields.CharField):
def __init__(self, **kwargs):
super(LDAPFilterWithUserField, self).__init__(**kwargs)
self.validators.append(validate_ldap_filter_with_user)
class LDAPScopeField(fields.ChoiceField):
def __init__(self, choices=None, **kwargs):
choices = choices or [('SCOPE_BASE', _('Base')), ('SCOPE_ONELEVEL', _('One Level')), ('SCOPE_SUBTREE', _('Subtree'))]
super(LDAPScopeField, self).__init__(choices, **kwargs)
def to_representation(self, value):
for choice in self.choices.keys():
if value == getattr(ldap, choice):
return choice
return super(LDAPScopeField, self).to_representation(value)
def to_internal_value(self, data):
value = super(LDAPScopeField, self).to_internal_value(data)
return getattr(ldap, value)
class LDAPSearchField(fields.ListField):
default_error_messages = {
'invalid_length': _('Expected a list of three items but got {length} instead.'),
'type_error': _('Expected an instance of LDAPSearch but got {input_type} instead.'),
}
ldap_filter_field_class = LDAPFilterField
def to_representation(self, value):
if not value:
return []
if not isinstance(value, LDAPSearch):
self.fail('type_error', input_type=type(value))
return [
LDAPDNField().to_representation(value.base_dn),
LDAPScopeField().to_representation(value.scope),
self.ldap_filter_field_class().to_representation(value.filterstr),
]
def to_internal_value(self, data):
data = super(LDAPSearchField, self).to_internal_value(data)
if len(data) == 0:
return None
if len(data) != 3:
self.fail('invalid_length', length=len(data))
return LDAPSearch(
LDAPDNField().run_validation(data[0]), LDAPScopeField().run_validation(data[1]), self.ldap_filter_field_class().run_validation(data[2])
)
class LDAPSearchWithUserField(LDAPSearchField):
ldap_filter_field_class = LDAPFilterWithUserField
class LDAPSearchUnionField(fields.ListField):
default_error_messages = {'type_error': _('Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} instead.')}
ldap_search_field_class = LDAPSearchWithUserField
def to_representation(self, value):
if not value:
return []
elif isinstance(value, LDAPSearchUnion):
return [self.ldap_search_field_class().to_representation(s) for s in value.searches]
elif isinstance(value, LDAPSearch):
return self.ldap_search_field_class().to_representation(value)
else:
self.fail('type_error', input_type=type(value))
def to_internal_value(self, data):
data = super(LDAPSearchUnionField, self).to_internal_value(data)
if len(data) == 0:
return None
if len(data) == 3 and isinstance(data[0], str):
return self.ldap_search_field_class().run_validation(data)
else:
search_args = []
for i in range(len(data)):
if not isinstance(data[i], list):
raise ValidationError('In order to ultilize LDAP Union, input element No. %d should be a search query array.' % (i + 1))
try:
search_args.append(self.ldap_search_field_class().run_validation(data[i]))
except Exception as e:
if hasattr(e, 'detail') and isinstance(e.detail, list):
e.detail.insert(0, "Error parsing LDAP Union element No. %d:" % (i + 1))
raise e
return LDAPSearchUnion(*search_args)
class LDAPUserAttrMapField(fields.DictField):
default_error_messages = {'invalid_attrs': _('Invalid user attribute(s): {invalid_attrs}.')}
valid_user_attrs = {'first_name', 'last_name', 'email'}
child = fields.CharField()
def to_internal_value(self, data):
data = super(LDAPUserAttrMapField, self).to_internal_value(data)
invalid_attrs = set(data.keys()) - self.valid_user_attrs
if invalid_attrs:
invalid_attrs = sorted(list(invalid_attrs))
attrs_display = json.dumps(invalid_attrs).lstrip('[').rstrip(']')
self.fail('invalid_attrs', invalid_attrs=attrs_display)
return data
class LDAPGroupTypeField(fields.ChoiceField, DependsOnMixin):
default_error_messages = {
'type_error': _('Expected an instance of LDAPGroupType but got {input_type} instead.'),
'missing_parameters': _('Missing required parameters in {dependency}.'),
'invalid_parameters': _('Invalid group_type parameters. Expected instance of dict but got {parameters_type} instead.'),
}
def __init__(self, choices=None, **kwargs):
group_types = get_subclasses(django_auth_ldap.config.LDAPGroupType)
choices = choices or [(x.__name__, x.__name__) for x in group_types]
super(LDAPGroupTypeField, self).__init__(choices, **kwargs)
def to_representation(self, value):
if not value:
return 'MemberDNGroupType'
if not isinstance(value, django_auth_ldap.config.LDAPGroupType):
self.fail('type_error', input_type=type(value))
return value.__class__.__name__
def to_internal_value(self, data):
data = super(LDAPGroupTypeField, self).to_internal_value(data)
if not data:
return None
cls = find_class_in_modules(data)
if not cls:
return None
# Per-group type parameter validation and handling here
# Backwords compatability. Before AUTH_LDAP_GROUP_TYPE_PARAMS existed
# MemberDNGroupType was the only group type, of the underlying lib, that
# took a parameter.
params = self.get_depends_on() or {}
params_sanitized = dict()
cls_args = inspect.getfullargspec(cls.__init__).args[1:]
if cls_args:
if not isinstance(params, dict):
self.fail('invalid_parameters', parameters_type=type(params))
for attr in cls_args:
if attr in params:
params_sanitized[attr] = params[attr]
try:
return cls(**params_sanitized)
except TypeError:
self.fail('missing_parameters', dependency=list(self.depends_on)[0])
class LDAPGroupTypeParamsField(fields.DictField, DependsOnMixin):
default_error_messages = {'invalid_keys': _('Invalid key(s): {invalid_keys}.')}
def to_internal_value(self, value):
value = super(LDAPGroupTypeParamsField, self).to_internal_value(value)
if not value:
return value
group_type_str = self.get_depends_on()
group_type_str = group_type_str or ''
group_type_cls = find_class_in_modules(group_type_str)
if not group_type_cls:
# Fail safe
return {}
invalid_keys = set(value.keys()) - set(inspect.getfullargspec(group_type_cls.__init__).args[1:])
if invalid_keys:
invalid_keys = sorted(list(invalid_keys))
keys_display = json.dumps(invalid_keys).lstrip('[').rstrip(']')
self.fail('invalid_keys', invalid_keys=keys_display)
return value
class LDAPUserFlagsField(fields.DictField):
default_error_messages = {'invalid_flag': _('Invalid user flag: "{invalid_flag}".')}
valid_user_flags = {'is_superuser', 'is_system_auditor'}
child = LDAPDNListField()
def to_internal_value(self, data):
data = super(LDAPUserFlagsField, self).to_internal_value(data)
invalid_flags = set(data.keys()) - self.valid_user_flags
if invalid_flags:
self.fail('invalid_flag', invalid_flag=list(invalid_flags)[0])
return data
class LDAPDNMapField(fields.StringListBooleanField):
child = LDAPDNField()
class LDAPSingleOrganizationMapField(HybridDictField):
admins = LDAPDNMapField(allow_null=True, required=False)
users = LDAPDNMapField(allow_null=True, required=False)
auditors = LDAPDNMapField(allow_null=True, required=False)
remove_admins = fields.BooleanField(required=False)
remove_users = fields.BooleanField(required=False)
remove_auditors = fields.BooleanField(required=False)
child = _Forbidden()
class LDAPOrganizationMapField(fields.DictField):
child = LDAPSingleOrganizationMapField()
class LDAPSingleTeamMapField(HybridDictField):
organization = fields.CharField()
users = LDAPDNMapField(allow_null=True, required=False)
remove = fields.BooleanField(required=False)
child = _Forbidden()
class LDAPTeamMapField(fields.DictField):
child = LDAPSingleTeamMapField()
class SocialMapStringRegexField(fields.CharField):
def to_representation(self, value):
if isinstance(value, type(re.compile(''))):

View File

@ -1,73 +0,0 @@
# Copyright (c) 2018 Ansible by Red Hat
# All Rights Reserved.
# Python
import ldap
# Django
from django.utils.encoding import force_str
# 3rd party
from django_auth_ldap.config import LDAPGroupType
class PosixUIDGroupType(LDAPGroupType):
def __init__(self, name_attr='cn', ldap_group_user_attr='uid'):
self.ldap_group_user_attr = ldap_group_user_attr
super(PosixUIDGroupType, self).__init__(name_attr)
"""
An LDAPGroupType subclass that handles non-standard DS.
"""
def user_groups(self, ldap_user, group_search):
"""
Searches for any group that is either the user's primary or contains the
user as a member.
"""
groups = []
try:
user_uid = ldap_user.attrs[self.ldap_group_user_attr][0]
if 'gidNumber' in ldap_user.attrs:
user_gid = ldap_user.attrs['gidNumber'][0]
filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % (
self.ldap.filter.escape_filter_chars(user_gid),
self.ldap.filter.escape_filter_chars(user_uid),
)
else:
filterstr = u'(memberUid=%s)' % (self.ldap.filter.escape_filter_chars(user_uid),)
search = group_search.search_with_additional_term_string(filterstr)
search.attrlist = [str(self.name_attr)]
groups = search.execute(ldap_user.connection)
except (KeyError, IndexError):
pass
return groups
def is_member(self, ldap_user, group_dn):
"""
Returns True if the group is the user's primary group or if the user is
listed in the group's memberUid attribute.
"""
is_member = False
try:
user_uid = ldap_user.attrs[self.ldap_group_user_attr][0]
try:
is_member = ldap_user.connection.compare_s(force_str(group_dn), 'memberUid', force_str(user_uid))
except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE):
is_member = False
if not is_member:
try:
user_gid = ldap_user.attrs['gidNumber'][0]
is_member = ldap_user.connection.compare_s(force_str(group_dn), 'gidNumber', force_str(user_gid))
except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE):
is_member = False
except (KeyError, IndexError):
is_member = False
return is_member

View File

@ -1,115 +0,0 @@
import pytest
from awx.sso.backends import _update_m2m_from_groups
class MockLDAPGroups(object):
def is_member_of(self, group_dn):
return bool(group_dn)
class MockLDAPUser(object):
def _get_groups(self):
return MockLDAPGroups()
@pytest.mark.parametrize(
"setting, expected_result",
[
(True, True),
('something', True),
(False, False),
('', False),
],
)
def test_mock_objects(setting, expected_result):
ldap_user = MockLDAPUser()
assert ldap_user._get_groups().is_member_of(setting) == expected_result
@pytest.mark.parametrize(
"opts, remove, expected_result",
[
# In these case we will pass no opts so we should get None as a return in all cases
(
None,
False,
None,
),
(
None,
True,
None,
),
# Next lets test with empty opts ([]) This should return False if remove is True and None otherwise
(
[],
True,
False,
),
(
[],
False,
None,
),
# Next opts is True, this will always return True
(
True,
True,
True,
),
(
True,
False,
True,
),
# If we get only a non-string as an option we hit a continue and will either return None or False depending on the remove flag
(
[32],
False,
None,
),
(
[32],
True,
False,
),
# Finally we need to test whether or not a user should be allowed in or not.
# We use a mock class for ldap_user that simply returns true/false based on the otps
(
['true'],
False,
True,
),
# In this test we are going to pass a string to test the part of the code that coverts strings into array, this should give us True
(
'something',
True,
True,
),
(
[''],
False,
None,
),
(
False,
True,
False,
),
# Empty strings are considered opts == None and will result in None or False based on the remove flag
(
'',
True,
False,
),
(
'',
False,
None,
),
],
)
@pytest.mark.django_db
def test__update_m2m_from_groups(opts, remove, expected_result):
ldap_user = MockLDAPUser()
assert expected_result == _update_m2m_from_groups(ldap_user, opts, remove)

View File

@ -293,18 +293,17 @@ class TestCommonFunctions:
assert o.galaxy_credentials.count() == 0
@pytest.mark.parametrize(
"enable_ldap, enable_social, enable_enterprise, expected_results",
"enable_social, enable_enterprise, expected_results",
[
(False, False, False, None),
(True, False, False, 'ldap'),
(True, True, False, 'social'),
(True, True, True, 'enterprise'),
(False, True, True, 'enterprise'),
(False, False, True, 'enterprise'),
(False, True, False, 'social'),
(False, False, None),
(True, False, 'social'),
(True, True, 'enterprise'),
(True, True, 'enterprise'),
(False, True, 'enterprise'),
(True, False, 'social'),
],
)
def test_get_external_account(self, enable_ldap, enable_social, enable_enterprise, expected_results):
def test_get_external_account(self, enable_social, enable_enterprise, expected_results):
try:
user = User.objects.get(username="external_tester")
except User.DoesNotExist:
@ -312,8 +311,6 @@ class TestCommonFunctions:
user.set_unusable_password()
user.save()
if enable_ldap:
user.profile.ldap_dn = 'test.dn'
if enable_social:
from social_django.models import UserSocialAuth
@ -337,8 +334,6 @@ class TestCommonFunctions:
[
# Set none of the social auth settings
('JUNK_SETTING', False),
# Set the hard coded settings
('AUTH_LDAP_SERVER_URI', True),
('SOCIAL_AUTH_SAML_ENABLED_IDPS', True),
('RADIUS_SERVER', True),
('TACACSPLUS_HOST', True),
@ -366,9 +361,8 @@ class TestCommonFunctions:
"key_one, key_one_value, key_two, key_two_value, expected",
[
('JUNK_SETTING', True, 'JUNK2_SETTING', True, False),
('AUTH_LDAP_SERVER_URI', True, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True, True),
('JUNK_SETTING', True, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True, True),
('AUTH_LDAP_SERVER_URI', False, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', False, False),
('JUNK_SETTING', True, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', False, False),
],
)
def test_is_remote_auth_enabled_multiple_keys(self, key_one, key_one_value, key_two, key_two_value, expected):

View File

@ -1,19 +0,0 @@
from django.test.utils import override_settings
import ldap
import pytest
from awx.sso.backends import LDAPSettings
@override_settings(AUTH_LDAP_CONNECTION_OPTIONS={ldap.OPT_NETWORK_TIMEOUT: 60})
@pytest.mark.django_db
def test_ldap_with_custom_timeout():
settings = LDAPSettings()
assert settings.CONNECTION_OPTIONS == {ldap.OPT_NETWORK_TIMEOUT: 60}
@override_settings(AUTH_LDAP_CONNECTION_OPTIONS={ldap.OPT_REFERRALS: 0})
@pytest.mark.django_db
def test_ldap_with_missing_timeout():
settings = LDAPSettings()
assert settings.CONNECTION_OPTIONS == {ldap.OPT_REFERRALS: 0, ldap.OPT_NETWORK_TIMEOUT: 30}

View File

@ -1,9 +1,8 @@
import pytest
from unittest import mock
from rest_framework.exceptions import ValidationError
from awx.sso.fields import SAMLOrgAttrField, SAMLTeamAttrField, SAMLUserFlagsAttrField, LDAPGroupTypeParamsField, LDAPServerURIField
from awx.sso.fields import SAMLOrgAttrField, SAMLTeamAttrField, SAMLUserFlagsAttrField
class TestSAMLOrgAttrField:
@ -192,44 +191,3 @@ class TestSAMLUserFlagsAttrField:
field.to_internal_value(data)
print(e.value.detail)
assert e.value.detail == expected
class TestLDAPGroupTypeParamsField:
@pytest.mark.parametrize(
"group_type, data, expected",
[
('LDAPGroupType', {'name_attr': 'user', 'bob': ['a', 'b'], 'scooter': 'hello'}, ['Invalid key(s): "bob", "scooter".']),
('MemberDNGroupType', {'name_attr': 'user', 'member_attr': 'west', 'bob': ['a', 'b'], 'scooter': 'hello'}, ['Invalid key(s): "bob", "scooter".']),
(
'PosixUIDGroupType',
{'name_attr': 'user', 'member_attr': 'west', 'ldap_group_user_attr': 'legacyThing', 'bob': ['a', 'b'], 'scooter': 'hello'},
['Invalid key(s): "bob", "member_attr", "scooter".'],
),
],
)
def test_internal_value_invalid(self, group_type, data, expected):
field = LDAPGroupTypeParamsField()
field.get_depends_on = mock.MagicMock(return_value=group_type)
with pytest.raises(ValidationError) as e:
field.to_internal_value(data)
assert e.value.detail == expected
class TestLDAPServerURIField:
@pytest.mark.parametrize(
"ldap_uri, exception, expected",
[
(r'ldap://servername.com:444', None, r'ldap://servername.com:444'),
(r'ldap://servername.so3:444', None, r'ldap://servername.so3:444'),
(r'ldaps://servername3.s300:344', None, r'ldaps://servername3.s300:344'),
(r'ldap://servername.-so3:444', ValidationError, None),
],
)
def test_run_validators_valid(self, ldap_uri, exception, expected):
field = LDAPServerURIField()
if exception is None:
assert field.run_validators(ldap_uri) == expected
else:
with pytest.raises(exception):
field.run_validators(ldap_uri)

View File

@ -1,25 +0,0 @@
import ldap
from awx.sso.backends import LDAPSettings
from awx.sso.validators import validate_ldap_filter
from django.core.cache import cache
def test_ldap_default_settings(mocker):
from_db = mocker.Mock(**{'order_by.return_value': []})
mocker.patch('awx.conf.models.Setting.objects.filter', return_value=from_db)
settings = LDAPSettings()
assert settings.ORGANIZATION_MAP == {}
assert settings.TEAM_MAP == {}
def test_ldap_default_network_timeout(mocker):
cache.clear() # clearing cache avoids picking up stray default for OPT_REFERRALS
from_db = mocker.Mock(**{'order_by.return_value': []})
mocker.patch('awx.conf.models.Setting.objects.filter', return_value=from_db)
settings = LDAPSettings()
assert settings.CONNECTION_OPTIONS[ldap.OPT_NETWORK_TIMEOUT] == 30
def test_ldap_filter_validator():
validate_ldap_filter('(test-uid=%(user)s)', with_user=True)

View File

@ -1,72 +1,12 @@
# Python
import re
# Python-LDAP
import ldap
# Django
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
__all__ = [
'validate_ldap_dn',
'validate_ldap_dn_with_user',
'validate_ldap_bind_dn',
'validate_ldap_filter',
'validate_ldap_filter_with_user',
'validate_tacacsplus_disallow_nonascii',
]
def validate_ldap_dn(value, with_user=False):
if with_user:
if '%(user)s' not in value:
raise ValidationError(_('DN must include "%%(user)s" placeholder for username: %s') % value)
dn_value = value.replace('%(user)s', 'USER')
else:
dn_value = value
try:
ldap.dn.str2dn(dn_value.encode('utf-8'))
except ldap.DECODING_ERROR:
raise ValidationError(_('Invalid DN: %s') % value)
def validate_ldap_dn_with_user(value):
validate_ldap_dn(value, with_user=True)
def validate_ldap_bind_dn(value):
if not re.match(r'^[A-Za-z][A-Za-z0-9._-]*?\\[A-Za-z0-9 ._-]+?$', value.strip()) and not re.match(
r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', value.strip()
):
validate_ldap_dn(value)
def validate_ldap_filter(value, with_user=False):
value = value.strip()
if not value:
return
if with_user:
if '%(user)s' not in value:
raise ValidationError(_('DN must include "%%(user)s" placeholder for username: %s') % value)
dn_value = value.replace('%(user)s', 'USER')
else:
dn_value = value
if re.match(r'^\([A-Za-z0-9-]+?=[^()]+?\)$', dn_value):
return
elif re.match(r'^\([&|!]\(.*?\)\)$', dn_value):
try:
map(validate_ldap_filter, ['(%s)' % x for x in dn_value[3:-2].split(')(')])
return
except ValidationError:
pass
raise ValidationError(_('Invalid filter: %s') % value)
def validate_ldap_filter_with_user(value):
validate_ldap_filter(value, with_user=True)
def validate_tacacsplus_disallow_nonascii(value):
try:
value.encode('ascii')

View File

@ -52,21 +52,6 @@ EXAMPLES = '''
name: "AWX_ISOLATION_SHOW_PATHS"
value: "'/var/lib/awx/projects/', '/tmp'"
register: testing_settings
- name: Set the LDAP Auth Bind Password
settings:
name: "AUTH_LDAP_BIND_PASSWORD"
value: "Password"
no_log: true
- name: Set all the LDAP Auth Bind Params
settings:
settings:
AUTH_LDAP_BIND_PASSWORD: "password"
AUTH_LDAP_USER_ATTR_MAP:
email: "mail"
first_name: "givenName"
last_name: "surname"
'''
from ..module_utils.controller_api import ControllerAPIModule

View File

@ -7,36 +7,6 @@ import pytest
from awx.conf.models import Setting
@pytest.mark.django_db
def test_setting_flat_value(run_module, admin_user):
the_value = 'CN=service_account,OU=ServiceAccounts,DC=domain,DC=company,DC=org'
result = run_module('settings', dict(name='AUTH_LDAP_BIND_DN', value=the_value), admin_user)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
assert Setting.objects.get(key='AUTH_LDAP_BIND_DN').value == the_value
@pytest.mark.django_db
def test_setting_dict_value(run_module, admin_user):
the_value = {'email': 'mail', 'first_name': 'givenName', 'last_name': 'surname'}
result = run_module('settings', dict(name='AUTH_LDAP_USER_ATTR_MAP', value=the_value), admin_user)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
assert Setting.objects.get(key='AUTH_LDAP_USER_ATTR_MAP').value == the_value
@pytest.mark.django_db
def test_setting_nested_type(run_module, admin_user):
the_value = {'email': 'mail', 'first_name': 'givenName', 'last_name': 'surname'}
result = run_module('settings', dict(settings={'AUTH_LDAP_USER_ATTR_MAP': the_value}), admin_user)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
assert Setting.objects.get(key='AUTH_LDAP_USER_ATTR_MAP').value == the_value
@pytest.mark.django_db
def test_setting_bool_value(run_module, admin_user):
for the_value in (True, False):

View File

@ -18,7 +18,6 @@ page.register_page(
resources.settings_github_team,
resources.settings_google_oauth2,
resources.settings_jobs,
resources.settings_ldap,
resources.settings_radius,
resources.settings_saml,
resources.settings_system,

View File

@ -215,7 +215,6 @@ class Resources(object):
_settings_github_team = 'settings/github-team/'
_settings_google_oauth2 = 'settings/google-oauth2/'
_settings_jobs = 'settings/jobs/'
_settings_ldap = 'settings/ldap/'
_settings_logging = 'settings/logging/'
_settings_named_url = 'settings/named-url/'
_settings_radius = 'settings/radius/'

View File

@ -11,12 +11,11 @@ When a user wants to log into AWX, she can explicitly choose some of the support
* Microsoft Azure Active Directory (AD) OAuth2
On the other hand, the other authentication methods use the same types of login info (username and password), but authenticate using external auth systems rather than AWX's own database. If some of these methods are enabled, AWX will try authenticating using the enabled methods *before AWX's own authentication method*. The order of precedence is:
* LDAP
* RADIUS
* TACACS+
* SAML
AWX will try authenticating against each enabled authentication method *in the specified order*, meaning if the same username and password is valid in multiple enabled auth methods (*e.g.*, both LDAP and TACACS+), AWX will only use the first positive match (in the above example, log a user in via LDAP and skip TACACS+).
AWX will try authenticating against each enabled authentication method *in the specified order*, meaning if the same username and password is valid in multiple enabled auth methods (*e.g.*, both RADIUS and TACACS+), AWX will only use the first positive match (in the above example, log a user in via RADIUS and skip TACACS+).
## Notes:
SAML users, RADIUS users and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users:

View File

@ -1,68 +0,0 @@
# LDAP
The Lightweight Directory Access Protocol (LDAP) is an open, vendor-neutral, industry-standard application protocol for accessing and maintaining distributed directory information services over an Internet Protocol (IP) network. Directory services play an important role in developing intranet and Internet applications by allowing the sharing of information about users, systems, networks, services, and applications throughout the network.
# Configure LDAP Authentication
Please see the [AWX documentation](https://ansible.readthedocs.io/projects/awx/en/latest/administration/ldap_auth.html) for basic LDAP configuration.
LDAP Authentication provides duplicate sets of configuration fields for authentication with up to six different LDAP servers.
The default set of configuration fields take the form `AUTH_LDAP_<field name>`. Configuration fields for additional LDAP servers are numbered `AUTH_LDAP_<n>_<field name>`.
## Test Environment Setup
Please see `README.md` of this repository: https://github.com/ansible/deploy_ldap
# Basic Setup for FreeIPA
LDAP Server URI (append if you have multiple LDAPs)
`ldaps://{{serverip1}}:636`
LDAP BIND DN (How to create a bind account in [FreeIPA](https://www.freeipa.org/page/Creating_a_binddn_for_Foreman)
`uid=awx-bind,cn=sysaccounts,cn=etc,dc=example,dc=com`
LDAP BIND PASSWORD
`{{yourbindaccountpassword}}`
LDAP USER DN TEMPLATE
`uid=%(user)s,cn=users,cn=accounts,dc=example,dc=com`
LDAP GROUP TYPE
`NestedMemberDNGroupType`
LDAP GROUP SEARCH
```
[
"cn=groups,cn=accounts,dc=example,dc=com",
"SCOPE_SUBTREE",
"(objectClass=groupOfNames)"
]
```
LDAP USER ATTRIBUTE MAP
```
{
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
```
LDAP USER FLAGS BY GROUP
```
{
"is_superuser": "cn={{superusergroupname}},cn=groups,cn=accounts,dc=example,dc=com"
}
```
LDAP ORGANIZATION MAP
```
{
"{{yourorganizationname}}": {
"admins": "cn={{admingroupname}},cn=groups,cn=accounts,dc=example,dc=com",
"remove_admins": false
}
}
```

View File

@ -1,4 +1,4 @@
Through the AWX user interface, you can set up a simplified login through various authentication types: GitHub, Google, LDAP, and RADIUS. After you create and register your developer application with the appropriate service, you can set up authorizations for them.
Through the AWX user interface, you can set up a simplified login through various authentication types: GitHub, Google, and RADIUS. After you create and register your developer application with the appropriate service, you can set up authorizations for them.
1. From the left navigation bar, click **Settings**.
@ -7,8 +7,7 @@ Through the AWX user interface, you can set up a simplified login through variou
- :ref:`ag_auth_azure`
- :ref:`ag_auth_github`
- :ref:`ag_auth_google_oauth2`
- :ref:`LDAP settings <ag_auth_ldap>`
- :ref:`ag_auth_radius`
- :ref:`ag_auth_radius`
- :ref:`ag_auth_tacacs`
Different authentication types require you to enter different information. Be sure to include all the information as required.

View File

@ -13,10 +13,6 @@ This section describes setting up authentication for the following enterprise sy
.. contents::
:local:
.. note::
For LDAP authentication, see :ref:`ag_auth_ldap`.
Azure, RADIUS, and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users:
- Enterprise users can only be created via the first successful login attempt from remote authentication backend.
@ -62,13 +58,6 @@ For application registering basics in Azure AD, refer to the `Azure AD Identity
.. _`Azure AD Identity Platform (v2)`: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-overview
LDAP Authentication
---------------------
Refer to the :ref:`ag_auth_ldap` section.
.. _ag_auth_radius:
RADIUS settings

View File

@ -41,7 +41,6 @@ Need help or want to discuss AWX including the documentation? See the :ref:`Comm
oauth2_token_auth
social_auth
ent_auth
ldap_auth
authentication_timeout
kerberos_auth
session_limits

View File

@ -1,361 +0,0 @@
.. _ag_auth_ldap:
Setting up LDAP Authentication
================================
.. index::
single: LDAP
pair: authentication; LDAP
This chapter describes how to integrate LDAP authentication with AWX.
.. note::
If the LDAP server you want to connect to has a certificate that is self-signed or signed by a corporate internal certificate authority (CA), the CA certificate must be added to the system's trusted CAs. Otherwise, connection to the LDAP server will result in an error that the certificate issuer is not recognized.
Administrators use LDAP as a source for account authentication information for AWX users. User authentication is provided, but not the synchronization of user permissions and credentials. Organization membership (as well as the organization admin) and team memberships can be synchronized.
When so configured, a user who logs in with an LDAP username and password automatically gets an AWX account created for them and they can be automatically placed into organizations as either regular users or organization administrators.
Users created locally in the user interface, take precedence over those logging into controller for their first time with an alternative authentication solution. You must delete the local user if you want to re-use it with another authentication method, such as LDAP.
Users created through an LDAP login cannot change their username, given name, surname, or set a local password for themselves. You can also configure this to restrict editing of other field names.
To configure LDAP integration for AWX:
1. First, create a user in LDAP that has access to read the entire LDAP structure.
2. Test if you can make successful queries to the LDAP server, use the ``ldapsearch`` command, which is a command line tool that can be installed on AWX command line as well as on other Linux and OSX systems. Use the following command to query the ldap server, where *josie* and *Josie4Cloud* are replaced by attributes that work for your setup:
::
ldapsearch -x -H ldap://win -D "CN=josie,CN=Users,DC=website,DC=com" -b "dc=website,dc=com" -w Josie4Cloud
Here ``CN=josie,CN=users,DC=website,DC=com`` is the Distinguished Name of the connecting user.
.. note::
The ``ldapsearch`` utility is not automatically pre-installed with AWX, however, you can install it from the ``openldap-clients`` package.
3. In the AWX User Interface, click **Settings** from the left navigation and click to select **LDAP settings** from the list of Authentication options.
Multiple LDAP configurations are not needed per LDAP server, but you can configure multiple LDAP servers from this page, otherwise, leave the server at **Default**:
.. image:: ../common/images/configure-awx-auth-ldap-servers.png
|
The equivalent API endpoints will show ``AUTH_LDAP_*`` repeated: ``AUTH_LDAP_1_*``, ``AUTH_LDAP_2_*``, ..., ``AUTH_LDAP_5_*`` to denote server designations.
4. To enter or modify the LDAP server address to connect to, click **Edit** and enter in the **LDAP Server URI** field using the same format as the one prepopulated in the text field:
.. image:: ../common/images/configure-awx-auth-ldap-server-uri.png
.. note::
Multiple LDAP servers may be specified by separating each with spaces or commas. Click the |help| icon to comply with proper syntax and rules.
.. |help| image:: ../common/images/tooltips-icon.png
5. Enter the password to use for the Binding user in the **LDAP Bind Password** text field. In this example, the password is 'passme':
.. image:: ../common/images/configure-awx-auth-ldap-bind-pwd.png
6. Click to select a group type from the **LDAP Group Type** drop-down menu list.
LDAP Group Types include:
- ``PosixGroupType``
- ``GroupOfNamesType``
- ``GroupOfUniqueNamesType``
- ``ActiveDirectoryGroupType``
- ``OrganizationalRoleGroupType``
- ``MemberDNGroupType``
- ``NISGroupType``
- ``NestedGroupOfNamesType``
- ``NestedGroupOfUniqueNamesType``
- ``NestedActiveDirectoryGroupType``
- ``NestedOrganizationalRoleGroupType``
- ``NestedMemberDNGroupType``
- ``PosixUIDGroupType``
The LDAP Group Types that are supported by leveraging the underlying `django-auth-ldap library`_. To specify the parameters for the selected group type, see :ref:`Step 15 <ldap_grp_params>` below.
.. _`django-auth-ldap library`: https://django-auth-ldap.readthedocs.io/en/latest/groups.html#types-of-groups
7. The **LDAP Start TLS** is disabled by default. To enable TLS when the LDAP connection is not using SSL/TLS, click the toggle to **ON**.
.. image:: ../common/images/configure-awx-auth-ldap-start-tls.png
8. Enter the Distinguished Name in the **LDAP Bind DN** text field to specify the user that AWX uses to connect (Bind) to the LDAP server. Below uses the example, ``CN=josie,CN=users,DC=website,DC=com``:
.. image:: ../common/images/configure-awx-auth-ldap-bind-dn.png
9. If that name is stored in key ``sAMAccountName``, the **LDAP User DN Template** populates with ``(sAMAccountName=%(user)s)``. Active Directory stores the username to ``sAMAccountName``. Similarly, for OpenLDAP, the key is ``uid``--hence the line becomes ``(uid=%(user)s)``.
10. Enter the group distinguish name to allow users within that group to access AWX in the **LDAP Require Group** field, using the same format as the one shown in the text field, ``CN=awx Users,OU=Users,DC=website,DC=com``.
.. image:: ../common/images/configure-awx-auth-ldap-req-group.png
11. Enter the group distinguish name to prevent users within that group to access AWX in the **LDAP Deny Group** field, using the same format as the one shown in the text field. In this example, leave the field blank.
12. Enter where to search for users while authenticating in the **LDAP User Search** field using the same format as the one shown in the text field. In this example, use:
::
[
"OU=Users,DC=website,DC=com",
"SCOPE_SUBTREE",
"(cn=%(user)s)"
]
The first line specifies where to search for users in the LDAP tree. In the above example, the users are searched recursively starting from ``DC=website,DC=com``.
The second line specifies the scope where the users should be searched:
- SCOPE_BASE: This value is used to indicate searching only the entry at the base DN, resulting in only that entry being returned
- SCOPE_ONELEVEL: This value is used to indicate searching all entries one level under the base DN - but not including the base DN and not including any entries under that one level under the base DN.
- SCOPE_SUBTREE: This value is used to indicate searching of all entries at all levels under and including the specified base DN.
The third line specifies the key name where the user name is stored.
.. image:: ../common/images/configure-awx-authen-ldap-user-search.png
.. note::
For multiple search queries, the proper syntax is:
::
[
[
"OU=Users,DC=northamerica,DC=acme,DC=com",
"SCOPE_SUBTREE",
"(sAMAccountName=%(user)s)"
],
[
"OU=Users,DC=apac,DC=corp,DC=com",
"SCOPE_SUBTREE",
"(sAMAccountName=%(user)s)"
],
[
"OU=Users,DC=emea,DC=corp,DC=com",
"SCOPE_SUBTREE",
"(sAMAccountName=%(user)s)"
]
]
13. In the **LDAP Group Search** text field, specify which groups should be searched and how to search them. In this example, use:
::
[
"dc=example,dc=com",
"SCOPE_SUBTREE",
"(objectClass=group)"
]
- The first line specifies the BASE DN where the groups should be searched.
- The second lines specifies the scope and is the same as that for the user directive.
- The third line specifies what the ``objectclass`` of a group object is in the LDAP you are using.
.. image:: ../common/images/configure-awx-authen-ldap-group-search.png
14. Enter the user attributes in the **LDAP User Attribute Map** the text field. In this example, use:
::
{
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
The above example retrieves users by last name from the key ``sn``. You can use the same LDAP query for the user to figure out what keys they are stored under.
.. image:: ../common/images/configure-awx-auth-ldap-user-attrb-map.png
.. _ldap_grp_params:
15. Depending on the selected **LDAP Group Type**, different parameters are available in the **LDAP Group Type Parameters** field to account for this. ``LDAP_GROUP_TYPE_PARAMS`` is a dictionary, which will be converted by AWX to kwargs and passed to the LDAP Group Type class selected. There are two common parameters used by any of the LDAP Group Type; ``name_attr`` and ``member_attr``. Where ``name_attr`` defaults to ``cn`` and ``member_attr`` defaults to ``member``:
::
{"name_attr": "cn", "member_attr": "member"}
To determine what parameters a specific LDAP Group Type expects. refer to the `django_auth_ldap`_ documentation around the classes ``init`` parameters.
.. _`django_auth_ldap`: https://django-auth-ldap.readthedocs.io/en/latest/reference.html#django_auth_ldap.config.LDAPGroupType
16. Enter the user profile flags in the **LDAP User Flags by Group** the text field. In this example, use the following syntax to set LDAP users as "Superusers" and "Auditors":
::
{
"is_superuser": "cn=superusers,ou=groups,dc=website,dc=com",
"is_system_auditor": "cn=auditors,ou=groups,dc=website,dc=com"
}
The above example retrieves users who are flagged as superusers or as auditor in their profile.
.. image:: ../common/images/configure-awx-auth-ldap-user-flags.png
17. For details on completing the mapping fields, see :ref:`ag_ldap_org_team_maps`.
.. image:: ../common/images/configure-ldap-orgs-teams-mapping.png
18. Click **Save** when done.
With these values entered on this form, you can now make a successful authentication with LDAP.
.. note::
AWX does not actively sync users, but they are created during their initial login.
To improve performance associated with LDAP authentication, see :ref:`ldap_auth_perf_tips` at the end of this chapter.
.. _ag_ldap_org_team_maps:
LDAP Organization and Team Mapping
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. index::
single: organization mapping
single: LDAP mapping
pair: authentication; LDAP mapping
pair: authentication; organization mapping
pair: authentication; LDAP team mapping
pair: authentication; team mapping
single: team mapping
You can control which users are placed into which organizations based on LDAP attributes (mapping out between your organization admins/users and LDAP groups).
Keys are organization names. Organizations will be created if not present. Values are dictionaries defining the options for each organization's membership. For each organization, it is possible to specify what groups are automatically users of the organization and also what groups can administer the organization.
**admins**: None, True/False, string or list/tuple of strings.
- If **None**, organization admins will not be updated based on LDAP values.
- If **True**, all users in LDAP will automatically be added as admins of the organization.
- If **False**, no LDAP users will be automatically added as admins of the organization.
- If a string or list of strings, specifies the group DN(s) that will be added of the organization if they match any of the specified groups.
**remove_admins**: True/False. Defaults to **False**.
- When **True**, a user who is not an member of the given groups will be removed from the organization's administrative list.
**users**: None, True/False, string or list/tuple of strings. Same rules apply as for **admins**.
**remove_users**: True/False. Defaults to **False**. Same rules apply as **remove_admins**.
::
{
"LDAP Organization": {
"admins": "cn=engineering_admins,ou=groups,dc=example,dc=com",
"remove_admins": false,
"users": [
"cn=engineering,ou=groups,dc=example,dc=com",
"cn=sales,ou=groups,dc=example,dc=com",
"cn=it,ou=groups,dc=example,dc=com"
],
"remove_users": false
},
"LDAP Organization 2": {
"admins": [
"cn=Administrators,cn=Builtin,dc=example,dc=com"
],
"remove_admins": false,
"users": true,
"remove_users": false
}
}
Mapping between team members (users) and LDAP groups. Keys are team names (will be created if not present). Values are dictionaries of options for each team's membership, where each can contain the following parameters:
**organization**: string. The name of the organization to which the team belongs. The team will be created if the combination of organization and team name does not exist. The organization will first be created if it does not exist.
**users**: None, True/False, string or list/tuple of strings.
- If **None**, team members will not be updated.
- If **True/False**, all LDAP users will be added/removed as team members.
- If a string or list of strings, specifies the group DN(s). User will be added as a team member if the user is a member of ANY of these groups.
**remove**: True/False. Defaults to **False**. When **True**, a user who is not a member of the given groups will be removed from the team.
::
{
"LDAP Engineering": {
"organization": "LDAP Organization",
"users": "cn=engineering,ou=groups,dc=example,dc=com",
"remove": true
},
"LDAP IT": {
"organization": "LDAP Organization",
"users": "cn=it,ou=groups,dc=example,dc=com",
"remove": true
},
"LDAP Sales": {
"organization": "LDAP Organization",
"users": "cn=sales,ou=groups,dc=example,dc=com",
"remove": true
}
}
.. _ldap_logging:
Enabling Logging for LDAP
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. index::
single: LDAP
pair: authentication; LDAP
To enable logging for LDAP, you must set the level to ``DEBUG`` in the Settings configuration window:
1. Click **Settings** from the left navigation pane and click to select **Logging settings** from the System list of options.
2. Click **Edit**.
3. Set the **Logging Aggregator Level Threshold** field to **Debug**.
.. image:: ../common/images/settings-system-logging-debug.png
4. Click **Save** to save your changes.
Referrals
~~~~~~~~~~~
.. index::
pair: LDAP; referrals
pair: troubleshooting; LDAP referrals
Active Directory uses "referrals" in case the queried object is not available in its database. It has been noted that this does not work properly with the django LDAP client and, most of the time, it helps to disable referrals. Disable LDAP referrals by adding the following lines to your ``/etc/awx/conf.d/custom.py`` file:
.. code-block:: bash
AUTH_LDAP_GLOBAL_OPTIONS = {
ldap.OPT_REFERRALS: False,
}
.. _ldap_auth_perf_tips:
LDAP authentication performance tips
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. index::
pair: best practices; ldap
When an LDAP user authenticates, by default, all user-related attributes will be updated in the database on each log in. In some environments, this operation can be skipped due to performance issues. To avoid it, you can disable the option `AUTH_LDAP_ALWAYS_UPDATE_USER`.
.. warning::
With this option set to False, no changes to LDAP user's attributes will be updated. Attributes will only be updated the first time the user is created.

View File

@ -363,11 +363,3 @@ Troubleshoot Logging
API 4XX Errors
~~~~~~~~~~~~~~~~~~~~
You can include the API error message for 4XX errors by modifying the log format for those messages. Refer to the :ref:`logging-api-400-error-config` section for more detail.
LDAP
~~~~~~
You can enable logging messages for the LDAP adapter. Refer to the :ref:`ldap_logging` section for more detail.
SAML
~~~~~~~
You can enable logging messages for the SAML adapter the same way you can enable logging for LDAP. Refer to the :ref:`ldap_logging` section for more detail.

View File

@ -451,7 +451,7 @@ Revoking an access token by this method is the same as deleting the token resour
.. note::
The **Allow External Users to Create Oauth2 Tokens** (``ALLOW_OAUTH2_FOR_EXTERNAL_USERS`` in the API) setting is disabled by default. External users refer to users authenticated externally with a service like LDAP, or any of the other SSO services. This setting ensures external users cannot *create* their own tokens. If you enable then disable it, any tokens created by external users in the meantime will still exist, and are not automatically revoked.
The **Allow External Users to Create Oauth2 Tokens** (``ALLOW_OAUTH2_FOR_EXTERNAL_USERS`` in the API) setting is disabled by default. External users refer to users authenticated externally with services like SSO services. This setting ensures external users cannot *create* their own tokens. If you enable then disable it, any tokens created by external users in the meantime will still exist, and are not automatically revoked.
Alternatively, you can use the ``manage`` utility, :ref:`ag_manage_utility_revoke_tokens`, to revoke tokens as described in the :ref:`ag_token_utility` section.

View File

@ -80,15 +80,6 @@ Metrics added in this release to track:
- **callback_receiver_event_processing_avg_seconds** - Proxy for “how far behind the callback receiver workers are in processing output". If this number stays large, consider horizontally scaling the control plane and reducing the ``capacity_adjustment`` value on the node.
LDAP login and basic authentication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. index::
pair: improvements; LDAP
pair: improvements; basic auth
Enhancements were made to the authentication backend that syncs LDAP configuration with the organizations and teams in the AWX. Logging in with large mappings between LDAP groups and organizations and teams is now up to 10 times faster than in previous versions.
Capacity Planning
------------------
.. index::
@ -382,4 +373,4 @@ For workloads with high levels of API interaction, best practices include:
- Use dynamic inventory sources instead of individually creating inventory hosts via the API
- Use webhook notifications instead of polling for job status
Since the published blog, additional observations have been made in the field regarding authentication methods. For automation clients that will make many requests in rapid succession, using tokens is a best practice, because depending on the type of user, there may be additional overhead when using basic authentication. For example, LDAP users using basic authentication trigger a process to reconcile if the LDAP user is correctly mapped to particular organizations, teams and roles. Refer to :ref:`ag_oauth2_token_auth` for detail on how to generate and use tokens.
Since the published blog, additional observations have been made in the field regarding authentication methods. For automation clients that will make many requests in rapid succession, using tokens is a best practice, because depending on the type of user, there may be additional overhead when using basic authentication. Refer to :ref:`ag_oauth2_token_auth` for detail on how to generate and use tokens.

View File

@ -24,7 +24,7 @@ User passwords for local users
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
AWX hashes local AWX user passwords with the PBKDF2 algorithm using a SHA256 hash. Users who authenticate via external
account mechanisms (LDAP, SAML, OAuth, and others) do not have any password or secret stored.
account mechanisms (SAML, OAuth, and others) do not have any password or secret stored.
Secret handling for operational use
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -82,7 +82,7 @@ Do not disable SELinux, and do not disable AWXs existing multi-tenant contain
External account stores
^^^^^^^^^^^^^^^^^^^^^^^^^
Maintaining a full set of users just in AWX can be a time-consuming task in a large organization, prone to error. AWX supports connecting to external account sources via :ref:`LDAP <ag_auth_ldap>` and certain :ref:`OAuth providers <ag_social_auth>`. Using this eliminates a source of error when working with permissions.
Maintaining a full set of users just in AWX can be a time-consuming task in a large organization, prone to error. AWX supports connecting to external account sources via certain :ref:`OAuth providers <ag_social_auth>`. Using this eliminates a source of error when working with permissions.
.. _ag_security_django_password:

View File

@ -11,7 +11,7 @@ Authentication methods help simplify logins for end users--offering single sign-
Account authentication can be configured in the AWX User Interface and saved to the PostgreSQL database. For instructions, refer to the :ref:`ag_configure_awx` section.
Account authentication in AWX can be configured to centrally use OAuth2, while enterprise-level account authentication can be configured for :ref:`Azure <ag_auth_azure>`, :ref:`RADIUS <ag_auth_radius>`, or even :ref:`LDAP <ag_auth_ldap>` as a source for authentication information. See :ref:`ag_ent_auth` for more detail.
Account authentication in AWX can be configured to centrally use OAuth2, while enterprise-level account authentication can be configured for :ref:`Azure <ag_auth_azure>`, :ref:`RADIUS <ag_auth_radius>` as a source for authentication information. See :ref:`ag_ent_auth` for more detail.
For websites, such as Microsoft Azure, Google or GitHub, that provide account information, account information is often implemented using the OAuth standard. OAuth is a secure authorization protocol which is commonly used in conjunction with account authentication to grant 3rd party applications a "session token" allowing them to make API calls to providers on the users behalf.

View File

@ -52,9 +52,6 @@ Example configuration of ``extra_settings`` parameter:
- setting: MAX_PAGE_SIZE
value: "500"
- setting: AUTH_LDAP_BIND_DN
value: "cn=admin,dc=example,dc=com"
- setting: LOG_AGGREGATOR_LEVEL
value: "'DEBUG'"

View File

@ -14,7 +14,6 @@ Known Issues
pair: known issues; live event statuses
pair: live event statuses; green dot
pair: live event statuses; red dot
pair: known issues; LDAP authentication
pair: known issues; lost isolated jobs
pair: known issues; sosreport
pair: known issues; local management
@ -97,13 +96,6 @@ Misuse of job slicing can cause errors in job scheduling
.. include:: ../common/job-slicing-rule.rst
Default LDAP directory must be configured to use LDAP Authentication
======================================================================
The ability to configure up to six LDAP directories for authentication requires a value. On the settings page for LDAP, there is a "Default" LDAP configuration followed by five-numbered configuration slots. If the "Default" is not populated, AWX will not try to authenticate using the other directory configurations.
Potential security issue using ``X_FORWARDED_FOR`` in ``REMOTE_HOST_HEADERS``
=================================================================================

View File

@ -189,7 +189,7 @@ Authentication Enhancements
pair: features; authentication
pair: features; OAuth 2 token
AWX supports LDAP, SAML, token-based authentication. Enhanced LDAP and SAML support allows you to integrate your account information in a more flexible manner. Token-based Authentication allows for easily authentication of third-party tools and services with AWX via integrated OAuth 2 token support.
AWX supports SAML, token-based authentication. Enhanced SAML support allows you to integrate your account information in a more flexible manner. Token-based Authentication allows for easily authentication of third-party tools and services with AWX via integrated OAuth 2 token support.
Cluster Management
~~~~~~~~~~~~~~~~~~~~

View File

@ -248,7 +248,7 @@ Often, you will have many Roles in a system and you will want some roles to incl
.. |rbac-heirarchy-morecomplex| image:: ../common/images/rbac-heirarchy-morecomplex.png
RBAC controls also give you the capability to explicitly permit User and Teams of Users to run playbooks against certain sets of hosts. Users and teams are restricted to just the sets of playbooks and hosts to which they are granted capabilities. And, with AWX, you can create or import as many Users and Teams as you require--create users and teams manually or import them from LDAP or Active Directory.
RBAC controls also give you the capability to explicitly permit User and Teams of Users to run playbooks against certain sets of hosts. Users and teams are restricted to just the sets of playbooks and hosts to which they are granted capabilities. And, with AWX, you can create or import as many Users and Teams as you require--create users and teams manually or import them from Active Directory.
RBACs are easiest to think of in terms of who or what can see, change, or delete an "object" for which a specific capability is being determined.

View File

@ -1,23 +0,0 @@
Copyright (c) 2009, Peter Sagerson
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,73 +0,0 @@
The MIT License applies to contributions committed after July 1st, 2021, and
to all contributions by the following authors:
* A. Karl Kornel
* Alex Willmer
* Aymeric Augustin
* Bernhard M. Wiedemann
* Bradley Baetz
* Christian Heimes
* Éloi Rivard
* Eyal Cherevatzki
* Florian Best
* Fred Thomsen
* Ivan A. Melnikov
* johnthagen
* Jonathon Reinhart
* Jon Dufresne
* Martin Basti
* Marti Raudsepp
* Miro Hrončok
* Paul Aurich
* Petr Viktorin
* Pieterjan De Potter
* Raphaël Barrois
* Robert Kuska
* Stanislav Láznička
* Tobias Bräutigam
* Tom van Dijk
* Wentao Han
* William Brown
-------------------------------------------------------------------------------
MIT License
Copyright (c) 2021 python-ldap contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Previous license:
The python-ldap package is distributed under Python-style license.
Standard disclaimer:
This software is made available by the author(s) to the public for free
and "as is". All users of this free software are solely and entirely
responsible for their own choice and use of this software for their
own purposes. By using this software, each user agrees that the
author(s) shall not be liable for damages of any kind in relation to
its use or performance. The author(s) do not warrant that this software
is fit for any purpose.
$Id: LICENCE,v 1.1 2002/09/18 18:51:22 stroeder Exp $

View File

@ -13,7 +13,6 @@ Cython<3 # due to https://github.com/yaml/pyyaml/pull/702
daphne
distro
django==4.2.10 # CVE-2024-24680
django-auth-ldap
django-cors-headers
django-crum
django-extensions
@ -52,7 +51,6 @@ pyparsing==2.4.6 # Upgrading to v3 of pyparsing introduce errors on smart host
python-daemon>3.0.0
python-dsv-sdk>=1.0.4
python-tss-sdk>=1.2.1
python-ldap
pyyaml>=6.0.1
pyzstd # otel collector log file compression library
receptorctl

View File

@ -129,7 +129,6 @@ django==4.2.10
# -r /awx_devel/requirements/requirements.in
# channels
# django-ansible-base
# django-auth-ldap
# django-cors-headers
# django-crum
# django-extensions
@ -140,8 +139,6 @@ django==4.2.10
# djangorestframework
# social-auth-app-django
# via -r /awx_devel/requirements/requirements_git.txt
django-auth-ldap==4.6.0
# via -r /awx_devel/requirements/requirements.in
django-cors-headers==4.3.1
# via -r /awx_devel/requirements/requirements.in
django-crum==0.7.9
@ -373,13 +370,11 @@ pyasn1==0.5.1
# via
# pyasn1-modules
# python-jose
# python-ldap
# rsa
# service-identity
pyasn1-modules==0.3.0
# via
# google-auth
# python-ldap
# service-identity
pycparser==2.21
# via cffi
@ -418,10 +413,6 @@ python-dsv-sdk==1.0.4
# via -r /awx_devel/requirements/requirements.in
python-jose==3.3.0
# via social-auth-core
python-ldap==3.4.4
# via
# -r /awx_devel/requirements/requirements.in
# django-auth-ldap
python-string-utils==1.0.0
# via openshift
python-tss-sdk==1.2.2

View File

@ -19,7 +19,6 @@ logutils
jupyter
# matplotlib - Caused issues when bumping to setuptools 58
backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirectory
git+https://github.com/artefactual-labs/mockldap.git@master#egg=mockldap
gprof2dot
atomicwrites
flake8

View File

@ -44,7 +44,6 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
libtool-ltdl-devel \
make \
nss \
openldap-devel \
patch \
postgresql \
postgresql-devel \
@ -127,7 +126,6 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
glibc-langpack-en \
krb5-workstation \
nginx \
"openldap >= 2.6.2-3" \
postgresql \
python3.11 \
"python3.11-devel" \

View File

@ -272,7 +272,6 @@ $ make docker-compose
- [Start a Cluster](#start-a-cluster)
- [Start with Minikube](#start-with-minikube)
- [SAML and OIDC Integration](#saml-and-oidc-integration)
- [OpenLDAP Integration](#openldap-integration)
- [Splunk Integration](#splunk-integration)
- [tacacs+ Integration](#tacacs+-integration)
@ -436,41 +435,6 @@ Note: The OIDC adapter performs authentication only, not authorization. So any u
If you Keycloak configuration is not working and you need to rerun the playbook to try a different `container_reference` or `oidc_reference` you can log into the Keycloak admin console on port 8443 and select the AWX realm in the upper left drop down. Then make sure you are on "Ream Settings" in the Configure menu option and click the trash can next to AWX in the main page window pane. This will completely remove the AWX ream (which has both SAML and OIDC settings) enabling you to re-run the plumb playbook.
### OpenLDAP Integration
OpenLDAP is an LDAP provider that can be used to test AWX with LDAP integration. This section describes how to build a reference OpenLDAP instance and plumb it with your AWX for testing purposes.
First, be sure that you have the awx.awx collection installed by running `make install_collection`.
Anytime you want to run an OpenLDAP instance alongside AWX we can start docker-compose with the LDAP option to get an LDAP instance with the command:
```bash
LDAP=true make docker-compose
```
Once the containers come up two new ports (389, 636) should be exposed and the LDAP server should be running on those ports. The first port (389) is non-SSL and the second port (636) is SSL enabled.
Now we are ready to configure and plumb OpenLDAP with AWX. To do this we have provided a playbook which will:
* Backup and configure the LDAP adapter in AWX. NOTE: this will back up your existing settings but the password fields can not be backed up through the API, you need a DB backup to recover this.
Note: The default configuration will utilize the non-tls connection. If you want to use the tls configuration you will need to work through TLS negotiation issues because the LDAP server is using a self signed certificate.
You can run the playbook like:
```bash
export CONTROLLER_USERNAME=<your username>
export CONTROLLER_PASSWORD=<your password>
ansible-playbook tools/docker-compose/ansible/plumb_ldap.yml
```
Once the playbook is done running LDAP should now be setup in your development environment. This realm has four users with the following username/passwords:
1. awx_ldap_unpriv:unpriv123
2. awx_ldap_admin:admin123
3. awx_ldap_auditor:audit123
4. awx_ldap_org_admin:orgadmin123
The first account is a normal user. The second account will be a super user in AWX. The third account will be a system auditor in AWX. The fourth account is an org admin. All users belong to an org called "LDAP Organization". To log in with one of these users go to the AWX login screen enter the username/password.
### Splunk Integration
Splunk is a log aggregation tool that can be used to test AWX with external logging integration. This section describes how to build a reference Splunk instance and plumb it with your AWX for testing purposes.
@ -550,7 +514,7 @@ To create a secret connected to this vault in AWX you can run the following play
```bash
export CONTROLLER_USERNAME=<your username>
export CONTROLLER_PASSWORD=<your password>
ansible-playbook tools/docker-compose/ansible/plumb_vault.yml -e enable_ldap=false
ansible-playbook tools/docker-compose/ansible/plumb_vault.yml
```
This will create the following items in your AWX instance:
@ -575,53 +539,6 @@ If you have a playbook like:
And run it through AWX with the credential `Credential From Vault via Token Auth` tied to it, the debug should result in `this_is_the_secret_value`. If you run it through AWX with the credential `Credential From Vault via Userpass Auth`, the debug should result in `this_is_the_userpass_secret_value`.
### HashiVault with LDAP
If you wish to have your OpenLDAP container connected to the Vault container, you will first need to have the OpenLDAP container running alongside AWX and Vault.
```bash
VAULT=true LDAP=true make docker-compose
```
Similar to the above, you will need to unseal the vault before we can run the other needed playbooks.
```bash
ansible-playbook tools/docker-compose/ansible/unseal_vault.yml
```
Now that the vault is unsealed, we can plumb the vault container now while passing true to enable_ldap extra var.
```bash
export CONTROLLER_USERNAME=<your username>
export CONTROLLER_PASSWORD=<your password>
ansible-playbook tools/docker-compose/ansible/plumb_vault.yml -e enable_ldap=true
```
This will populate your AWX instance with LDAP specific items.
- A vault LDAP Lookup Cred tied to the LDAP `awx_ldap_vault` user called `Vault LDAP Lookup Cred`
- A credential called `Credential From HashiCorp Vault via LDAP Auth` which is of the created type using the `Vault LDAP Lookup Cred` to get the secret.
And run it through AWX with the credential `Credential From HashiCorp Vault via LDAP Auth` tied to it, the debug should result in `this_is_the_ldap_secret_value`.
The extremely non-obvious input is the fact that the fact prefixes "data/" unexpectedly.
This was discovered by inspecting the secret with the vault CLI, which may help with future troubleshooting.
```
docker exec -it -e VAULT_TOKEN=<token> tools_vault_1 vault kv get --address=http://127.0.0.1:1234 my_engine/my_root/my_folder
```
### Prometheus and Grafana integration
See docs at https://github.com/ansible/awx/blob/devel/tools/grafana/README.md

View File

@ -1,32 +0,0 @@
---
- name: Plumb an ldap instance
hosts: localhost
connection: local
gather_facts: False
vars:
awx_host: "https://localhost:8043"
tasks:
- name: Load existing and new LDAP settings
ansible.builtin.set_fact:
existing_ldap: "{{ lookup('awx.awx.controller_api', 'settings/ldap', host=awx_host, verify_ssl=false) }}"
new_ldap: "{{ lookup('template', 'ldap_settings.json.j2') }}"
- name: Display existing LDAP configuration
ansible.builtin.debug:
msg:
- "Here is your existing LDAP configuration for reference:"
- "{{ existing_ldap }}"
- ansible.builtin.pause:
prompt: "Continuing to run this will replace your existing ldap settings (displayed above). They will all be captured. Be sure that is backed up before continuing"
- name: Write out the existing content
ansible.builtin.copy:
dest: "../_sources/existing_ldap_adapter_settings.json"
content: "{{ existing_ldap }}"
- name: Configure AWX LDAP adapter
awx.awx.settings:
settings: "{{ new_ldap }}"
controller_host: "{{ awx_host }}"
validate_certs: False

View File

@ -23,15 +23,6 @@ work_sign_public_keyfile: "{{ work_sign_key_dir }}/work_public_key.pem"
# SSO variables
enable_keycloak: false
enable_ldap: false
ldap_public_key_file_name: 'ldap.cert'
ldap_private_key_file_name: 'ldap.key'
ldap_cert_dir: '{{ sources_dest }}/ldap_certs'
ldap_diff_dir: '{{ sources_dest }}/ldap_diffs'
ldap_public_key_file: '{{ ldap_cert_dir }}/{{ ldap_public_key_file_name }}'
ldap_private_key_file: '{{ ldap_cert_dir }}/{{ ldap_private_key_file_name }}'
ldap_cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN="
# Hashicorp Vault
enable_vault: false
vault_tls: false

View File

@ -1,21 +0,0 @@
---
- name: Create LDAP cert directory
file:
path: "{{ item }}"
state: directory
loop:
- "{{ ldap_cert_dir }}"
- "{{ ldap_diff_dir }}"
- name: include vault vars
include_vars: "{{ hashivault_vars_file }}"
- name: General LDAP cert
command: 'openssl req -new -x509 -days 365 -nodes -out {{ ldap_public_key_file }} -keyout {{ ldap_private_key_file }} -subj "{{ ldap_cert_subject }}"'
args:
creates: "{{ ldap_public_key_file }}"
- name: Copy ldap.diff
ansible.builtin.template:
src: "ldap.ldif.j2"
dest: "{{ ldap_diff_dir }}/ldap.ldif"

View File

@ -97,10 +97,6 @@
creates: "{{ work_sign_public_keyfile }}"
when: sign_work | bool
- name: Include LDAP tasks if enabled
include_tasks: ldap.yml
when: enable_ldap | bool
- name: Include vault TLS tasks if enabled
include_tasks: vault_tls.yml
when: enable_vault | bool

View File

@ -146,31 +146,6 @@ services:
depends_on:
- postgres
{% endif %}
{% if enable_ldap|bool %}
ldap:
image: bitnami/openldap:2
container_name: tools_ldap_1
hostname: ldap
user: "{{ ansible_user_uid }}"
networks:
- awx
ports:
- "389:1389"
- "636:1636"
environment:
LDAP_ADMIN_USERNAME: admin
LDAP_ADMIN_PASSWORD: admin
LDAP_CUSTOM_LDIF_DIR: /opt/bitnami/openldap/ldiffs
LDAP_ENABLE_TLS: "yes"
LDAP_LDAPS_PORT_NUMBER: 1636
LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/certs/{{ ldap_public_key_file_name }}
LDAP_TLS_CA_FILE: /opt/bitnami/openldap/certs/{{ ldap_public_key_file_name }}
LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/certs/{{ ldap_private_key_file_name }}
volumes:
- 'openldap_data:/bitnami/openldap'
- '../../docker-compose/_sources/ldap_certs:/opt/bitnami/openldap/certs'
- '../../docker-compose/_sources/ldap_diffs:/opt/bitnami/openldap/ldiffs'
{% endif %}
{% if enable_splunk|bool %}
splunk:
image: splunk/splunk:latest
@ -376,11 +351,6 @@ volumes:
redis_socket_{{ container_postfix }}:
name: tools_redis_socket_{{ container_postfix }}
{% endfor -%}
{% if enable_ldap|bool %}
openldap_data:
name: tools_ldap_1
driver: local
{% endif %}
{% if enable_vault|bool %}
hashicorp_vault_data:
name: tools_vault_1

View File

@ -1,99 +0,0 @@
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
dc: example
o: example
dn: ou=users,dc=example,dc=org
ou: users
objectClass: organizationalUnit
dn: cn=awx_ldap_admin,ou=users,dc=example,dc=org
mail: admin@example.org
sn: LdapAdmin
cn: awx_ldap_admin
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
userPassword: admin123
givenName: awx
dn: cn=awx_ldap_auditor,ou=users,dc=example,dc=org
mail: auditor@example.org
sn: LdapAuditor
cn: awx_ldap_auditor
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
userPassword: audit123
givenName: awx
dn: cn=awx_ldap_unpriv,ou=users,dc=example,dc=org
mail: unpriv@example.org
sn: LdapUnpriv
cn: awx_ldap_unpriv
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: awx
userPassword: unpriv123
dn: ou=groups,dc=example,dc=org
ou: groups
objectClass: top
objectClass: organizationalUnit
dn: cn=awx_users,ou=groups,dc=example,dc=org
cn: awx_users
objectClass: top
objectClass: groupOfNames
member: cn=awx_ldap_admin,ou=users,dc=example,dc=org
member: cn=awx_ldap_auditor,ou=users,dc=example,dc=org
member: cn=awx_ldap_unpriv,ou=users,dc=example,dc=org
member: cn=awx_ldap_org_admin,ou=users,dc=example,dc=org
dn: cn=awx_admins,ou=groups,dc=example,dc=org
cn: awx_admins
objectClass: top
objectClass: groupOfNames
member: cn=awx_ldap_admin,ou=users,dc=example,dc=org
dn: cn=awx_auditors,ou=groups,dc=example,dc=org
cn: awx_auditors
objectClass: top
objectClass: groupOfNames
member: cn=awx_ldap_auditor,ou=users,dc=example,dc=org
dn: cn=awx_ldap_org_admin,ou=users,dc=example,dc=org
mail: org.admin@example.org
sn: LdapOrgAdmin
cn: awx_ldap_org_admin
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: awx
userPassword: orgadmin123
dn: cn=awx_org_admins,ou=groups,dc=example,dc=org
cn: awx_org_admins
objectClass: top
objectClass: groupOfNames
member: cn=awx_ldap_org_admin,ou=users,dc=example,dc=org
{% if enable_ldap|bool and enable_vault|bool %}
dn: cn={{ vault_ldap_username }},ou=users,dc=example,dc=org
changetype: add
mail: vault@example.org
sn: LdapVaultAdmin
cn: {{ vault_ldap_username }}
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
userPassword: {{ vault_ldap_password }}
givenName: awx
{% endif %}

View File

@ -42,10 +42,6 @@ OPTIONAL_API_URLPATTERN_PREFIX = '{{ api_urlpattern_prefix }}'
# Enable the following line to turn on database settings logging.
# LOGGING['loggers']['awx.conf']['level'] = 'DEBUG'
# Enable the following lines to turn on LDAP auth logging.
# LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console']
# LOGGING['loggers']['django_auth_ldap']['level'] = 'DEBUG'
{% if enable_otel|bool %}
LOGGING['handlers']['otel'] |= {
'class': 'awx.main.utils.handlers.OTLPHandler',

View File

@ -5,8 +5,5 @@ vault_cert_dir: "{{ sources_dest }}/vault_certs"
vault_server_cert: "{{ vault_cert_dir }}/server.crt"
vault_client_cert: "{{ vault_cert_dir }}/client.crt"
vault_client_key: "{{ vault_cert_dir }}/client.key"
ldap_ldif: "{{ sources_dest }}/ldap.ldifs/ldap.ldif"
vault_ldap_username: "awx_ldap_vault"
vault_ldap_password: "vault123"
vault_userpass_username: "awx_userpass_admin"
vault_userpass_password: "userpass123"

View File

@ -92,74 +92,6 @@
validate_certs: false
token: "{{ Initial_Root_Token }}"
- name: Configure the vault ldap auth
block:
- name: Create ldap auth mount
flowerysong.hvault.write:
path: "sys/auth/ldap"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
data:
type: "ldap"
register: vault_auth_ldap
changed_when: vault_auth_ldap.result.errors | default([]) | length == 0
failed_when:
- vault_auth_ldap.result.errors | default([]) | length > 0
- "'path is already in use at ldap/' not in vault_auth_ldap.result.errors | default([])"
- name: Create ldap engine
flowerysong.hvault.engine:
path: "ldap_engine"
type: "kv"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
- name: Create a ldap secret
flowerysong.hvault.kv:
mount_point: "ldap_engine/ldaps_root"
key: "ldap_secret"
value:
my_key: "this_is_the_ldap_secret_value"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
- name: Configure ldap auth
flowerysong.hvault.ldap_config:
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
url: "ldap://ldap:1389"
binddn: "cn=awx_ldap_vault,ou=users,dc=example,dc=org"
bindpass: "vault123"
userdn: "ou=users,dc=example,dc=org"
deny_null_bind: "false"
discoverdn: "true"
- name: Create ldap access policy
flowerysong.hvault.policy:
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
name: "ldap_engine"
policy:
ldap_engine/*: [create, read, update, delete, list]
sys/mounts:/*: [create, read, update, delete, list]
sys/mounts: [read]
- name: Add awx_ldap_vault user to auth_method
flowerysong.hvault.ldap_user:
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
state: present
name: "{{ vault_ldap_username }}"
policies:
- "ldap_engine"
when: enable_ldap | bool
- name: Create userpass engine
flowerysong.hvault.engine:
path: "userpass_engine"

View File

@ -78,56 +78,6 @@
secret_path: "/my_root/my_folder"
secret_version: ""
- name: Create a HashiCorp Vault Credential for LDAP
awx.awx.credential:
credential_type: HashiCorp Vault Secret Lookup
name: Vault LDAP Lookup Cred
organization: Default
controller_host: "{{ awx_host }}"
controller_username: admin
controller_password: "{{ admin_password }}"
validate_certs: false
inputs:
api_version: "v1"
default_auth_path: "ldap"
kubernetes_role: ""
namespace: ""
url: "{{ vault_addr_from_container }}"
username: "{{ vault_ldap_username }}"
password: "{{ vault_ldap_password }}"
register: vault_ldap_cred
when: enable_ldap | bool
- name: Create a credential from the Vault LDAP Custom Cred Type
awx.awx.credential:
credential_type: "{{ custom_vault_cred_type.id }}"
controller_host: "{{ awx_host }}"
controller_username: admin
controller_password: "{{ admin_password }}"
validate_certs: false
name: Credential From HashiCorp Vault via LDAP Auth
inputs: {}
organization: Default
register: custom_credential_via_ldap
when: enable_ldap | bool
- name: Use the Vault LDAP Credential the new credential
awx.awx.credential_input_source:
input_field_name: password
target_credential: "{{ custom_credential_via_ldap.id }}"
source_credential: "{{ vault_ldap_cred.id }}"
controller_host: "{{ awx_host }}"
controller_username: admin
controller_password: "{{ admin_password }}"
validate_certs: false
metadata:
auth_path: ""
secret_backend: "ldap_engine"
secret_key: "my_key"
secret_path: "ldaps_root/ldap_secret"
secret_version: ""
when: enable_ldap | bool
- name: Create a HashiCorp Vault Credential for UserPass
awx.awx.credential:
credential_type: HashiCorp Vault Secret Lookup

View File

@ -1,52 +0,0 @@
{
"AUTH_LDAP_1_SERVER_URI": "ldap://ldap:1389",
"AUTH_LDAP_1_BIND_DN": "cn=admin,dc=example,dc=org",
"AUTH_LDAP_1_BIND_PASSWORD": "admin",
"AUTH_LDAP_1_START_TLS": false,
"AUTH_LDAP_1_CONNECTION_OPTIONS": {
"OPT_REFERRALS": 0,
"OPT_NETWORK_TIMEOUT": 30
},
"AUTH_LDAP_1_USER_SEARCH": [
"ou=users,dc=example,dc=org",
"SCOPE_SUBTREE",
"(cn=%(user)s)"
],
"AUTH_LDAP_1_USER_DN_TEMPLATE": "cn=%(user)s,ou=users,dc=example,dc=org",
"AUTH_LDAP_1_USER_ATTR_MAP": {
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
},
"AUTH_LDAP_1_GROUP_SEARCH": [
"ou=groups,dc=example,dc=org",
"SCOPE_SUBTREE",
"(objectClass=groupOfNames)"
],
"AUTH_LDAP_1_GROUP_TYPE": "MemberDNGroupType",
"AUTH_LDAP_1_GROUP_TYPE_PARAMS": {
"member_attr": "member",
"name_attr": "cn"
},
"AUTH_LDAP_1_REQUIRE_GROUP": "cn=awx_users,ou=groups,dc=example,dc=org",
"AUTH_LDAP_1_DENY_GROUP": null,
"AUTH_LDAP_1_USER_FLAGS_BY_GROUP": {
"is_superuser": [
"cn=awx_admins,ou=groups,dc=example,dc=org"
],
"is_system_auditor": [
"cn=awx_auditors,ou=groups,dc=example,dc=org"
]
},
"AUTH_LDAP_1_ORGANIZATION_MAP": {
"LDAP Organization": {
"users": true,
"remove_admins": false,
"remove_users": true,
"admins": [
"cn=awx_org_admins,ou=groups,dc=example,dc=org"
]
}
},
"AUTH_LDAP_1_TEAM_MAP": {}
}