""" LDAP authentication with ldap3, replacing django-auth-ldap and python-ldap. Same behaviour as the old django-auth-ldap setup: the uid is searched (as the admin DN) in every base of LDAPSEARCH, exactly one match is required, the user logs in with their own DN, and a local Django user mirrors the names and email from LDAP (AUTH_LDAP_USER_ATTR_MAP). """ import logging from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend from ldap3.core.exceptions import LDAPException from ldap3.utils.conv import escape_filter_chars from .ungleich_ldap import LdapManager logger = logging.getLogger(__name__) class LdapBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): # An empty password is an anonymous bind in LDAP and would succeed. if not username or not password: return None ldap_manager = LdapManager() try: entry = self._find_user(ldap_manager, username) if entry is None: return None conn = ldap_manager.get_conn(user=entry.entry_dn, password=password) try: if not conn.bind(): return None finally: conn.unbind() except LDAPException as exc: logger.error("LDAP authentication for %s failed: %s", username, exc) return None user = self._sync_user(username, entry) return user if self.user_can_authenticate(user) else None def _find_user(self, ldap_manager, username): attributes = list(settings.AUTH_LDAP_USER_ATTR_MAP.values()) search_filter = "(uid={})".format(escape_filter_chars(username)) matches = [] conn = ldap_manager.get_admin_conn() try: for base in settings.LDAP_SEARCH_BASES: conn.search(base, search_filter, attributes=attributes) matches.extend(conn.entries) finally: conn.unbind() if len(matches) != 1: if matches: logger.warning("uid %s matched %d entries; refusing login", username, len(matches)) return None return matches[0] def _sync_user(self, username, entry): user, created = get_user_model().objects.get_or_create(username=username) for field, attr in settings.AUTH_LDAP_USER_ATTR_MAP.items(): value = entry[attr].value if attr in entry else None setattr(user, field, value if isinstance(value, str) else "") if created: user.set_unusable_password() user.save() return user