From c6ae00d85ae6f0390003e61e8e5c512fccc7f553 Mon Sep 17 00:00:00 2001 From: Djebran Lezzoum Date: Tue, 27 Aug 2024 10:15:41 +0200 Subject: [PATCH] Remove LDAP authentication Remove LDAP authentication from AWX --- Makefile | 8 +- awx/api/serializers.py | 39 +- awx/api/views/root.py | 9 - .../migrations/0006_v331_ldap_group_type.py | 6 +- .../migrations/0011_remove_ldap_auth_conf.py | 115 ++++++ awx/conf/migrations/_ldap_group_type.py | 31 -- awx/conf/signals.py | 2 +- awx/conf/tests/functional/test_migrations.py | 25 -- awx/main/access.py | 5 +- .../management/commands/dump_auth_config.py | 69 +--- awx/main/middleware.py | 2 +- awx/main/migrations/0196_delete_profile.py | 16 + awx/main/models/__init__.py | 3 +- awx/main/models/organization.py | 22 +- .../tests/functional/api/test_settings.py | 76 ---- awx/main/tests/functional/test_ldap.py | 103 ----- .../unit/commands/test_dump_auth_config.py | 43 --- awx/settings/defaults.py | 17 - awx/sso/backends.py | 264 ------------- awx/sso/common.py | 17 +- awx/sso/conf.py | 305 +-------------- awx/sso/fields.py | 343 +---------------- awx/sso/ldap_group_types.py | 73 ---- awx/sso/tests/functional/test_backends.py | 115 ------ awx/sso/tests/functional/test_common.py | 24 +- awx/sso/tests/functional/test_ldap.py | 19 - awx/sso/tests/unit/test_fields.py | 44 +-- awx/sso/tests/unit/test_ldap.py | 25 -- awx/sso/validators.py | 60 --- awx_collection/plugins/modules/settings.py | 15 - awx_collection/test/awx/test_settings.py | 30 -- awxkit/awxkit/api/pages/settings.py | 1 - awxkit/awxkit/api/resources.py | 1 - docs/auth/README.md | 3 +- docs/auth/ldap.md | 68 ---- .../configure_awx_authentication.rst | 5 +- docs/docsite/rst/administration/ent_auth.rst | 11 - docs/docsite/rst/administration/index.rst | 1 - docs/docsite/rst/administration/ldap_auth.rst | 361 ------------------ docs/docsite/rst/administration/logging.rst | 8 - .../rst/administration/oauth2_token_auth.rst | 2 +- .../rst/administration/performance.rst | 11 +- .../rst/administration/secret_handling.rst | 2 +- .../security_best_practices.rst | 2 +- .../rst/administration/social_auth.rst | 2 +- .../rst/administration/troubleshooting.rst | 3 - .../rst/release_notes/known_issues.rst | 8 - docs/docsite/rst/userguide/overview.rst | 2 +- docs/docsite/rst/userguide/rbac.rst | 2 +- licenses/django-auth-ldap.txt | 23 -- licenses/python-ldap.txt | 73 ---- requirements/requirements.in | 2 - requirements/requirements.txt | 9 - requirements/requirements_dev.txt | 1 - .../roles/dockerfile/templates/Dockerfile.j2 | 2 - tools/docker-compose/README.md | 85 +---- tools/docker-compose/ansible/plumb_ldap.yml | 32 -- .../ansible/roles/sources/defaults/main.yml | 9 - .../ansible/roles/sources/tasks/ldap.yml | 21 - .../ansible/roles/sources/tasks/main.yml | 4 - .../sources/templates/docker-compose.yml.j2 | 30 -- .../roles/sources/templates/ldap.ldif.j2 | 99 ----- .../sources/templates/local_settings.py.j2 | 4 - .../ansible/roles/vault/defaults/main.yml | 3 - .../ansible/roles/vault/tasks/initialize.yml | 68 ---- .../ansible/roles/vault/tasks/plumb.yml | 50 --- .../ansible/templates/ldap_settings.json.j2 | 52 --- 67 files changed, 172 insertions(+), 2813 deletions(-) create mode 100644 awx/conf/migrations/0011_remove_ldap_auth_conf.py delete mode 100644 awx/conf/migrations/_ldap_group_type.py delete mode 100644 awx/conf/tests/functional/test_migrations.py create mode 100644 awx/main/migrations/0196_delete_profile.py delete mode 100644 awx/main/tests/functional/test_ldap.py delete mode 100644 awx/sso/ldap_group_types.py delete mode 100644 awx/sso/tests/functional/test_backends.py delete mode 100644 awx/sso/tests/functional/test_ldap.py delete mode 100644 awx/sso/tests/unit/test_ldap.py delete mode 100644 docs/auth/ldap.md delete mode 100644 docs/docsite/rst/administration/ldap_auth.rst delete mode 100644 licenses/django-auth-ldap.txt delete mode 100644 licenses/python-ldap.txt delete mode 100644 tools/docker-compose/ansible/plumb_ldap.yml delete mode 100644 tools/docker-compose/ansible/roles/sources/tasks/ldap.yml delete mode 100644 tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 delete mode 100644 tools/docker-compose/ansible/templates/ldap_settings.json.j2 diff --git a/Makefile b/Makefile index 60aae0395cb0..257590f091c8 100644 --- a/Makefile +++ b/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 diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 231bf3bbcda0..e167f463ec5f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -961,7 +961,6 @@ def get_types(self): 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 Meta: 'is_superuser', 'is_system_auditor', 'password', - 'ldap_dn', 'last_login', 'external_account', ) @@ -1028,8 +1026,10 @@ def validate_password(self, value): 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 @@ def get_related(self, obj): ) 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, diff --git a/awx/api/views/root.py b/awx/api/views/root.py index e55461923e8b..d0f3a88e4c21 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -295,15 +295,6 @@ def get(self, request, format=None): 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 diff --git a/awx/conf/migrations/0006_v331_ldap_group_type.py b/awx/conf/migrations/0006_v331_ldap_group_type.py index 9b7bf4e6ecc1..f70b3db273c1 100644 --- a/awx/conf/migrations/0006_v331_ldap_group_type.py +++ b/awx/conf/migrations/0006_v331_ldap_group_type.py @@ -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 = [] diff --git a/awx/conf/migrations/0011_remove_ldap_auth_conf.py b/awx/conf/migrations/0011_remove_ldap_auth_conf.py new file mode 100644 index 000000000000..e8955635af63 --- /dev/null +++ b/awx/conf/migrations/0011_remove_ldap_auth_conf.py @@ -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), + ] diff --git a/awx/conf/migrations/_ldap_group_type.py b/awx/conf/migrations/_ldap_group_type.py deleted file mode 100644 index 378f934342fb..000000000000 --- a/awx/conf/migrations/_ldap_group_type.py +++ /dev/null @@ -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() diff --git a/awx/conf/signals.py b/awx/conf/signals.py index d8297becb40e..d7868e4faa4f 100644 --- a/awx/conf/signals.py +++ b/awx/conf/signals.py @@ -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)) diff --git a/awx/conf/tests/functional/test_migrations.py b/awx/conf/tests/functional/test_migrations.py deleted file mode 100644 index d3fddb292bd1..000000000000 --- a/awx/conf/tests/functional/test_migrations.py +++ /dev/null @@ -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 diff --git a/awx/main/access.py b/awx/main/access.py index 3a217fe2afa4..74c604436807 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -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()): diff --git a/awx/main/management/commands/dump_auth_config.py b/awx/main/management/commands/dump_auth_config.py index 45afc9b41d41..6434b01db939 100644 --- a/awx/main/management/commands/dump_auth_config.py +++ b/awx/main/management/commands/dump_auth_config.py @@ -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 @@ 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 @@ def is_enabled(self, settings, keys): 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 @@ def handle(self, *args, **options): 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 diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 433ade596fe4..5b0b99c5e48a 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -93,7 +93,7 @@ def process_request(self, request): 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) diff --git a/awx/main/migrations/0196_delete_profile.py b/awx/main/migrations/0196_delete_profile.py new file mode 100644 index 000000000000..a2179870bd81 --- /dev/null +++ b/awx/main/migrations/0196_delete_profile.py @@ -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', + ), + ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index a799b077f30a..d371ec23927c 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -18,7 +18,7 @@ # 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 @@ def o_auth2_token_get_absolute_url(self, request=None): 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) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 939595ea9e9c..23ce7598a296 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -15,8 +15,8 @@ # 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.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 @@ def get_absolute_url(self, request=None): 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 diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index a84a6f7f6acd..6f6c2a5e09af 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -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'}) diff --git a/awx/main/tests/functional/test_ldap.py b/awx/main/tests/functional/test_ldap.py deleted file mode 100644 index 2467ff52e38f..000000000000 --- a/awx/main/tests/functional/test_ldap.py +++ /dev/null @@ -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) diff --git a/awx/main/tests/unit/commands/test_dump_auth_config.py b/awx/main/tests/unit/commands/test_dump_auth_config.py index 48024ff5e425..3b1c6e67d03d 100644 --- a/awx/main/tests/unit/commands/test_dump_auth_config.py +++ b/awx/main/tests/unit/commands/test_dump_auth_config.py @@ -28,21 +28,6 @@ } }, "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 @@ def setUp(self): "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 @@ def test_json_returned_from_cmd(self): # 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'] diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d51818d7222e..a1fa3ce479a7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -9,8 +9,6 @@ import socket from datetime import timedelta -# python-ldap -import ldap from split_settings.tools import include @@ -389,12 +387,6 @@ } 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', @@ -420,14 +412,6 @@ 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. @@ -927,7 +911,6 @@ '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'}, diff --git a/awx/sso/backends.py b/awx/sso/backends.py index 572afc3ef04b..3b60cb223e67 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -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 @@ # 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 @@ def get_user(self, user_id): ): 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') diff --git a/awx/sso/common.py b/awx/sso/common.py index 99abc51d5a03..b57506f67dac 100644 --- a/awx/sso/common.py +++ b/awx/sso/common.py @@ -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', diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 03640fccd8ae..332815deb6e7 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -14,18 +14,6 @@ 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 @@ 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 @@ def __call__(self): 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 ############################################################################### diff --git a/awx/sso/fields.py b/awx/sso/fields.py index a81cb1cf34d4..d0ee30316992 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -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 @@ def _default_from_required_settings(self): 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(''))): diff --git a/awx/sso/ldap_group_types.py b/awx/sso/ldap_group_types.py deleted file mode 100644 index 2a5434c15440..000000000000 --- a/awx/sso/ldap_group_types.py +++ /dev/null @@ -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 diff --git a/awx/sso/tests/functional/test_backends.py b/awx/sso/tests/functional/test_backends.py deleted file mode 100644 index a0d2c31da3e2..000000000000 --- a/awx/sso/tests/functional/test_backends.py +++ /dev/null @@ -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) diff --git a/awx/sso/tests/functional/test_common.py b/awx/sso/tests/functional/test_common.py index f2b3e5781d90..18e485c6a10c 100644 --- a/awx/sso/tests/functional/test_common.py +++ b/awx/sso/tests/functional/test_common.py @@ -293,18 +293,17 @@ def test_get_or_create_org_with_default_galaxy_cred_no_galaxy_cred(self, galaxy_ 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 @@ def test_get_external_account(self, enable_ldap, enable_social, enable_enterpris 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 @@ def test_get_external_account(self, enable_ldap, enable_social, enable_enterpris [ # 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 @@ def test_is_remote_auth_enabled(self, setting, expected): "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): diff --git a/awx/sso/tests/functional/test_ldap.py b/awx/sso/tests/functional/test_ldap.py deleted file mode 100644 index 881ab29e2b4f..000000000000 --- a/awx/sso/tests/functional/test_ldap.py +++ /dev/null @@ -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} diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py index 35ab58d07fab..14f91d2f4218 100644 --- a/awx/sso/tests/unit/test_fields.py +++ b/awx/sso/tests/unit/test_fields.py @@ -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 @@ def test_internal_value_invalid(self, data, expected): 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) diff --git a/awx/sso/tests/unit/test_ldap.py b/awx/sso/tests/unit/test_ldap.py deleted file mode 100644 index aa54aaa49dbe..000000000000 --- a/awx/sso/tests/unit/test_ldap.py +++ /dev/null @@ -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) diff --git a/awx/sso/validators.py b/awx/sso/validators.py index 478b86b36fc9..a93f22efb8f3 100644 --- a/awx/sso/validators.py +++ b/awx/sso/validators.py @@ -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') diff --git a/awx_collection/plugins/modules/settings.py b/awx_collection/plugins/modules/settings.py index c911f77fcc6b..7314257463b2 100644 --- a/awx_collection/plugins/modules/settings.py +++ b/awx_collection/plugins/modules/settings.py @@ -52,21 +52,6 @@ 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 diff --git a/awx_collection/test/awx/test_settings.py b/awx_collection/test/awx/test_settings.py index 69e823b3b9cf..2e0de9d2e04d 100644 --- a/awx_collection/test/awx/test_settings.py +++ b/awx_collection/test/awx/test_settings.py @@ -7,36 +7,6 @@ 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): diff --git a/awxkit/awxkit/api/pages/settings.py b/awxkit/awxkit/api/pages/settings.py index 12fa7e2910ca..bb29612ee81c 100644 --- a/awxkit/awxkit/api/pages/settings.py +++ b/awxkit/awxkit/api/pages/settings.py @@ -18,7 +18,6 @@ class Setting(base.Base): resources.settings_github_team, resources.settings_google_oauth2, resources.settings_jobs, - resources.settings_ldap, resources.settings_radius, resources.settings_saml, resources.settings_system, diff --git a/awxkit/awxkit/api/resources.py b/awxkit/awxkit/api/resources.py index 4ffb70a9b563..7b73734e2a97 100644 --- a/awxkit/awxkit/api/resources.py +++ b/awxkit/awxkit/api/resources.py @@ -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/' diff --git a/docs/auth/README.md b/docs/auth/README.md index eb23268747a3..62be30a69358 100644 --- a/docs/auth/README.md +++ b/docs/auth/README.md @@ -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: diff --git a/docs/auth/ldap.md b/docs/auth/ldap.md deleted file mode 100644 index d212fa7ca853..000000000000 --- a/docs/auth/ldap.md +++ /dev/null @@ -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_`. Configuration fields for additional LDAP servers are numbered `AUTH_LDAP__`. - - -## 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 -} -} -``` diff --git a/docs/docsite/rst/administration/configure_awx_authentication.rst b/docs/docsite/rst/administration/configure_awx_authentication.rst index f90576f918d5..c56dfc5937f5 100644 --- a/docs/docsite/rst/administration/configure_awx_authentication.rst +++ b/docs/docsite/rst/administration/configure_awx_authentication.rst @@ -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 ` -- :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. diff --git a/docs/docsite/rst/administration/ent_auth.rst b/docs/docsite/rst/administration/ent_auth.rst index 73039f46779c..238893ecee3e 100644 --- a/docs/docsite/rst/administration/ent_auth.rst +++ b/docs/docsite/rst/administration/ent_auth.rst @@ -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 diff --git a/docs/docsite/rst/administration/index.rst b/docs/docsite/rst/administration/index.rst index 5ba867d856f0..247bc6db4abb 100644 --- a/docs/docsite/rst/administration/index.rst +++ b/docs/docsite/rst/administration/index.rst @@ -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 diff --git a/docs/docsite/rst/administration/ldap_auth.rst b/docs/docsite/rst/administration/ldap_auth.rst deleted file mode 100644 index 62372d949b4a..000000000000 --- a/docs/docsite/rst/administration/ldap_auth.rst +++ /dev/null @@ -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 ` 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. - diff --git a/docs/docsite/rst/administration/logging.rst b/docs/docsite/rst/administration/logging.rst index ff5453839a54..bf484ab9cd41 100644 --- a/docs/docsite/rst/administration/logging.rst +++ b/docs/docsite/rst/administration/logging.rst @@ -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. diff --git a/docs/docsite/rst/administration/oauth2_token_auth.rst b/docs/docsite/rst/administration/oauth2_token_auth.rst index e6a3497f5ec0..7ab83a16e6df 100644 --- a/docs/docsite/rst/administration/oauth2_token_auth.rst +++ b/docs/docsite/rst/administration/oauth2_token_auth.rst @@ -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. diff --git a/docs/docsite/rst/administration/performance.rst b/docs/docsite/rst/administration/performance.rst index 32d3c96cd0b8..05b1c0f62be5 100644 --- a/docs/docsite/rst/administration/performance.rst +++ b/docs/docsite/rst/administration/performance.rst @@ -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. diff --git a/docs/docsite/rst/administration/secret_handling.rst b/docs/docsite/rst/administration/secret_handling.rst index 41b35d731008..fb5eb86e8c13 100644 --- a/docs/docsite/rst/administration/secret_handling.rst +++ b/docs/docsite/rst/administration/secret_handling.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/docsite/rst/administration/security_best_practices.rst b/docs/docsite/rst/administration/security_best_practices.rst index 8695082fbf86..e5d739559325 100644 --- a/docs/docsite/rst/administration/security_best_practices.rst +++ b/docs/docsite/rst/administration/security_best_practices.rst @@ -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 ` and certain :ref:`OAuth providers `. 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 `. Using this eliminates a source of error when working with permissions. .. _ag_security_django_password: diff --git a/docs/docsite/rst/administration/social_auth.rst b/docs/docsite/rst/administration/social_auth.rst index 9979fb0d3934..05a62f02019c 100644 --- a/docs/docsite/rst/administration/social_auth.rst +++ b/docs/docsite/rst/administration/social_auth.rst @@ -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 `, :ref:`RADIUS `, or even :ref:`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 `, :ref:`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. diff --git a/docs/docsite/rst/administration/troubleshooting.rst b/docs/docsite/rst/administration/troubleshooting.rst index 43c9bfa8d909..f363695265b5 100644 --- a/docs/docsite/rst/administration/troubleshooting.rst +++ b/docs/docsite/rst/administration/troubleshooting.rst @@ -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'" diff --git a/docs/docsite/rst/release_notes/known_issues.rst b/docs/docsite/rst/release_notes/known_issues.rst index ae685f152c2e..d92590a02ba2 100644 --- a/docs/docsite/rst/release_notes/known_issues.rst +++ b/docs/docsite/rst/release_notes/known_issues.rst @@ -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`` ================================================================================= diff --git a/docs/docsite/rst/userguide/overview.rst b/docs/docsite/rst/userguide/overview.rst index 59f44063f9a8..87390a3910d8 100644 --- a/docs/docsite/rst/userguide/overview.rst +++ b/docs/docsite/rst/userguide/overview.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/docsite/rst/userguide/rbac.rst b/docs/docsite/rst/userguide/rbac.rst index f94e95763721..1eb31e9f94ac 100644 --- a/docs/docsite/rst/userguide/rbac.rst +++ b/docs/docsite/rst/userguide/rbac.rst @@ -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. diff --git a/licenses/django-auth-ldap.txt b/licenses/django-auth-ldap.txt deleted file mode 100644 index b16b2c01ff6a..000000000000 --- a/licenses/django-auth-ldap.txt +++ /dev/null @@ -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. diff --git a/licenses/python-ldap.txt b/licenses/python-ldap.txt deleted file mode 100644 index cece5f74cb92..000000000000 --- a/licenses/python-ldap.txt +++ /dev/null @@ -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 $ diff --git a/requirements/requirements.in b/requirements/requirements.in index 8a2bd21ae664..a162b671e2d5 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -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 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2838fb6e15f1..b23b18880480 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -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 diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 0d88a477fed5..7dfc0f67a942 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -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 diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index dbfc821e89fe..0ab08ca6d108 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -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" \ diff --git a/tools/docker-compose/README.md b/tools/docker-compose/README.md index 34db342021ff..77e10233bcdb 100644 --- a/tools/docker-compose/README.md +++ b/tools/docker-compose/README.md @@ -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= -export CONTROLLER_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= export CONTROLLER_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= - -export CONTROLLER_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= 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 diff --git a/tools/docker-compose/ansible/plumb_ldap.yml b/tools/docker-compose/ansible/plumb_ldap.yml deleted file mode 100644 index 56b3dcdbabf2..000000000000 --- a/tools/docker-compose/ansible/plumb_ldap.yml +++ /dev/null @@ -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 diff --git a/tools/docker-compose/ansible/roles/sources/defaults/main.yml b/tools/docker-compose/ansible/roles/sources/defaults/main.yml index 669f2cfe2002..6bc110758056 100644 --- a/tools/docker-compose/ansible/roles/sources/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/sources/defaults/main.yml @@ -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 diff --git a/tools/docker-compose/ansible/roles/sources/tasks/ldap.yml b/tools/docker-compose/ansible/roles/sources/tasks/ldap.yml deleted file mode 100644 index 1e0185a0885f..000000000000 --- a/tools/docker-compose/ansible/roles/sources/tasks/ldap.yml +++ /dev/null @@ -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" diff --git a/tools/docker-compose/ansible/roles/sources/tasks/main.yml b/tools/docker-compose/ansible/roles/sources/tasks/main.yml index 0f1149053ebd..5637f6254601 100644 --- a/tools/docker-compose/ansible/roles/sources/tasks/main.yml +++ b/tools/docker-compose/ansible/roles/sources/tasks/main.yml @@ -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 diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index a56f861fda57..80f075ab4140 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -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 diff --git a/tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 b/tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 deleted file mode 100644 index 9deaf836cd61..000000000000 --- a/tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 +++ /dev/null @@ -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 %} diff --git a/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 b/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 index 42a5d56366f4..1be38f43e28a 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 @@ -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', diff --git a/tools/docker-compose/ansible/roles/vault/defaults/main.yml b/tools/docker-compose/ansible/roles/vault/defaults/main.yml index 58e0153b7f1e..36feeb28684a 100644 --- a/tools/docker-compose/ansible/roles/vault/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/vault/defaults/main.yml @@ -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" diff --git a/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml b/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml index 8c7230c6d146..ac7d60b8ecfd 100644 --- a/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml +++ b/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml @@ -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" diff --git a/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml b/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml index 0e87daef6fa5..f3fc709b84d5 100644 --- a/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml +++ b/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml @@ -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 diff --git a/tools/docker-compose/ansible/templates/ldap_settings.json.j2 b/tools/docker-compose/ansible/templates/ldap_settings.json.j2 deleted file mode 100644 index 793270d7c93c..000000000000 --- a/tools/docker-compose/ansible/templates/ldap_settings.json.j2 +++ /dev/null @@ -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": {} -}