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:
parent
6dea7bfe17
commit
f22b192fb4
8
Makefile
8
Makefile
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
115
awx/conf/migrations/0011_remove_ldap_auth_conf.py
Normal file
115
awx/conf/migrations/0011_remove_ldap_auth_conf.py
Normal 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),
|
||||
]
|
@ -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()
|
@ -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))
|
||||
|
@ -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
|
@ -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()):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
16
awx/main/migrations/0196_delete_profile.py
Normal file
16
awx/main/migrations/0196_delete_profile.py
Normal 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',
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'})
|
||||
|
@ -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)
|
@ -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']
|
||||
|
@ -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'},
|
||||
|
@ -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')
|
||||
|
@ -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',
|
||||
|
305
awx/sso/conf.py
305
awx/sso/conf.py
@ -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
|
||||
###############################################################################
|
||||
|
@ -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(''))):
|
||||
|
@ -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
|
@ -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)
|
@ -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):
|
||||
|
@ -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}
|
@ -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)
|
||||
|
@ -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)
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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/'
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -82,7 +82,7 @@ Do not disable SELinux, and do not disable AWX’s 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:
|
||||
|
@ -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 user’s behalf.
|
||||
|
||||
|
@ -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'"
|
||||
|
||||
|
@ -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``
|
||||
=================================================================================
|
||||
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
@ -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 $
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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" \
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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"
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
@ -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',
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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": {}
|
||||
}
|
Loading…
Reference in New Issue
Block a user