feat: add directory adapter
LDAPS directory adapter with certificate verification, allow-listed reads and writes, escaped filters, password verification and change, password policy and account state, plus an in-memory fake. Adds the LDAP_* configuration, python-ldap with its image libraries, the CA file mount and an opt-in live test for the test OU. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
This commit is contained in:
+41
-1
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
@@ -31,6 +31,17 @@ class ConfigError(Exception):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DirectoryConfig:
|
||||
server: str # host name, optionally host:port; always reached over LDAPS
|
||||
user_ou_dn: str
|
||||
bind_dn: str # distinguished name or user principal name of the service account
|
||||
bind_password: str = field(repr=False)
|
||||
ca_file: Path
|
||||
publisher_group_dn: str
|
||||
admin_group_dn: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
secret_key: str
|
||||
@@ -40,6 +51,7 @@ class Config:
|
||||
trusted_proxies: list[str]
|
||||
log_level: str
|
||||
default_timezone: str
|
||||
directory: DirectoryConfig
|
||||
|
||||
|
||||
def load_config(environ: Mapping[str, str]) -> Config:
|
||||
@@ -104,6 +116,25 @@ def load_config(environ: Mapping[str, str]) -> Config:
|
||||
except (ZoneInfoNotFoundError, ValueError, OSError):
|
||||
problems.append("DEFAULT_TIMEZONE: must be a valid IANA time zone name")
|
||||
|
||||
def required(name: str, rule: str) -> str:
|
||||
value = raw(name)
|
||||
if value is None:
|
||||
problems.append(f"{name}: required, {rule}")
|
||||
return ""
|
||||
return value
|
||||
|
||||
server = required("LDAP_SERVER", "host name of the directory server, with optional :port")
|
||||
if "://" in server or "/" in server:
|
||||
problems.append("LDAP_SERVER: must be a host name, not a URL (LDAPS is always used)")
|
||||
user_ou_dn = required("LDAP_USER_OU_DN", "distinguished name of the OU that holds the users")
|
||||
bind_dn = required("LDAP_BIND_DN", "service account distinguished name or user principal name")
|
||||
bind_password = required("LDAP_BIND_PASSWORD", "password of the service account")
|
||||
publisher_group_dn = required("LDAP_PUBLISHER_GROUP_DN", "distinguished name of the group")
|
||||
admin_group_dn = required("LDAP_ADMIN_GROUP_DN", "distinguished name of the group")
|
||||
ca_file = Path(required("LDAP_CA_FILE", "path of the PEM file with the CA chain"))
|
||||
if raw("LDAP_CA_FILE") is not None and not ca_file.is_file():
|
||||
problems.append("LDAP_CA_FILE: must be an existing file")
|
||||
|
||||
if problems:
|
||||
raise ConfigError(problems)
|
||||
|
||||
@@ -115,4 +146,13 @@ def load_config(environ: Mapping[str, str]) -> Config:
|
||||
trusted_proxies=proxies,
|
||||
log_level=log_level,
|
||||
default_timezone=timezone,
|
||||
directory=DirectoryConfig(
|
||||
server=server,
|
||||
user_ou_dn=user_ou_dn,
|
||||
bind_dn=bind_dn,
|
||||
bind_password=bind_password,
|
||||
ca_file=ca_file,
|
||||
publisher_group_dn=publisher_group_dn,
|
||||
admin_group_dn=admin_group_dn,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ ALLOWED_HOSTS = _config.allowed_hosts
|
||||
TRUSTED_PROXIES = _config.trusted_proxies
|
||||
DATA_DIR = _config.data_dir
|
||||
DISPLAY_TIME_ZONE = _config.default_timezone
|
||||
DIRECTORY = _config.directory
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.contenttypes",
|
||||
|
||||
Reference in New Issue
Block a user