# LDAP и Active Directory Этот guide описывает текущую интеграцию каталога сотрудников. LDAP-аутентификация, синхронизация и ORM write-back — связанные, но отдельные режимы. ## Источники истины - настройки: `backend/eusrr_backend/settings.py` - authentication backend: `backend/eusrr_backend/auth_backends.py` - публичный Python API: `backend/employees/ldap/__init__.py` - сервисы: `backend/employees/ldap/services/` - retry queue: `backend/employees/tasks.py` и `backend/employees/models.py` - сигналы: `backend/employees/signals/ldap/` - admin: `backend/employees/ldap/admin.py` - tests: `backend/employees/ldap/tests/`, `backend/tests/employees/`, `backend/tests/manual/ldap_integration/` ## Режимы работы | `LDAP_ENABLED` | `LDAP_WRITE_ENABLED` | Поведение | | --- | --- | --- | | `false` | `false` | Полностью локальный режим: Django authentication и PostgreSQL | | `true` | `false` | LDAP login и ldap3 services включены; django-ldapdb alias не создаётся | | `true` | `true` | Полная интеграция, включая LDAP ORM/write-back | `LDAP_WRITE_ENABLED` не включает authentication автоматически. Для полной интеграции задавайте обе переменные явно. ## Минимальная конфигурация ```dotenv LDAP_ENABLED=true LDAP_WRITE_ENABLED=true LDAP_URI=ldaps://ad.example.com:636 LDAP_BIND_DN=CN=eusrr-service,OU=Service Accounts,DC=example,DC=com LDAP_BIND_PASSWORD=change-me LDAP_CA_CERTS=/etc/ssl/certs/company-ca.pem LDAP_USER_BASE=OU=Users,DC=example,DC=com LDAP_USERS_BASE=OU=Users,DC=example,DC=com LDAP_BASE_DN=OU=Users,DC=example,DC=com LDAP_DEPARTMENTS_BASE=OU=Departments,DC=example,DC=com LDAP_GROUPS_BASE=OU=Groups,DC=example,DC=com LDAP_POSITIONS_BASE=OU=Positions,DC=example,DC=com LDAP_DISMISSED_BASE=OU=Dismissed,DC=example,DC=com LDAP_UPN_SUFFIX=example.com LDAP_EMPLOYEE_ID_ATTR=employeeNumber ``` Дополнительные настройки: | Переменная | Назначение | | --- | --- | | `LDAP_USER_FILTER` | object filter для поиска пользователей | | `LDAP_LOGIN_UID_ATTRS` | атрибуты входа по короткому логину; по умолчанию `uid,sAMAccountName` | | `LDAP_LOGIN_MAIL_ATTRS` | атрибуты входа по адресу; по умолчанию `mail,userPrincipalName` | | `LDAP_PHONE_ATTRS` | comma-separated атрибуты для login по телефону | | `LDAP_ATTR_MAIL`, `LDAP_ATTR_GIVENNAME`, `LDAP_ATTR_SN`, `LDAP_ATTR_PHONE` | mapping основных полей | | `LDAP_CONNECT_TIMEOUT` | timeout подключения ldap3 | | `LDAP_OPERATION_TIMEOUT` | timeout LDAP-операции | | `LDAP_WRITE_DN`, `LDAP_WRITE_PASSWORD` | отдельная service account для записи, если нужна | | `LDAP_GUESTS_BASE`, `LDAP_GUESTS_ACTIVE_BASE`, `LDAP_GUESTS_DEACTIVATED_BASE` | контейнеры гостевых учёток | В production используйте `ldaps://` и проверяемый CA. Секреты не должны попадать в Git, логи или Markdown-примеры. ## Authentication `LDAP3Backend` стоит первым в `AUTHENTICATION_BACKENDS`, но делает fast exit при `LDAP_ENABLED=false`. Далее доступны локальный email/phone backend, аварийный superuser backend, permissions по должности, django-rules и стандартный Django backend. Login API принимает LDAP-логин (`uid`/`sAMAccountName`), email или телефон через единое поле `login`. Старые поля `email`, `phone` и `phone_number` сохранены для обратной совместимости: ```text POST /api/auth/token/ ``` ```json { "login": "ivanov", "password": "секретный-пароль" } ``` LDAP-managed пользователь хранит локальный пароль в unusable-состоянии. Проверка пароля выполняется каталогом, а выданный JWT всё равно привязывается к `UserAuthSession`. ## Модель данных и направление синхронизации PostgreSQL-модели `Employee`, `Department`, `DepartmentRole`, `Position` и Django `Group` используются приложением для чтения и permissions. LDAP-модели из `orm_models.py` предназначены для операций каталога. Связь хранится в `LdapSyncState`: model name, Django PK, LDAP DN/GUID, timestamps, последнее направление и optional data hash. Не ищите соответствие объектов по отображаемому имени, если доступен sync state. Высокоуровневые сервисы: - `UserService` — создание, обновление, удаление и mapping пользователей; - `DepartmentService` — OU, department group, руководители, участники и department roles; - `GroupService` — CRUD и membership глобальных групп; - `PositionService` — POS-группы, membership и nesting; - `SyncService` — bulk import/export пользователей и отделов. Для нового кода используйте эти сервисы и DTO из `employees.ldap.domain.dtos`. Прямые `ldap3.modify` и ручное конструирование DN допустимы только внутри infrastructure/repository слоя. ## Автоматическая синхронизация и retry Сигналы синхронизируют изменения существующих LDAP-managed объектов. Создание LDAP-пользователя при регистрации выполняет `LdapUserCreationMixin`, а не employee `post_save`. Если LDAP-операция завершается инфраструктурной ошибкой: 1. создаётся `LdapSyncQueue`; 2. система пытается немедленно поставить `process_ldap_queue_item` в Celery; 3. если Celery недоступен, periodic task `employees.tasks.process_ldap_queue` подберёт запись; 4. Celery Beat проверяет очередь каждую минуту; 5. после `max_attempts` запись получает статус `failed`. Повторная операция вызывает тот же canonical service method, поэтому retry не должен содержать отдельную бизнес-логику. ## Перемещение и переименование LDAP-объектов `ModifyDnMixin` обрабатывает: - смену `base_dn` через `modify_dn(..., new_superior=...)`; - смену RDN (`cn`/`ou`) через LDAP rename; - синхронизацию `_saved_dn`, `dn` и исходных значений после успеха. Не меняйте DN строковыми операциями вне сервиса. Значения RDN должны проходить escaping из `employees.ldap.utils`. `LdapSyncStateMixin` обновляет или удаляет `LdapSyncState` после успешного ORM save/delete. Для модели должны быть заданы `_sync_model_name` и `_sync_pk_field`. ## Пароли Пользователь меняет пароль через: ```text POST /api/auth/change-password/ ``` Endpoint сначала проверяет текущий пароль через настроенную authentication chain и применяет Django password validators (минимум 8 символов, common/numeric/similarity checks). Для LDAP-managed пользователя новый пароль затем проверяется политикой каталога и записывается через `UserService`; локальный пароль снова становится unusable. Для локального пользователя применяется обычный `set_password`. Низкоуровневый `UserPasswordService` используется внутри LDAP service layer. Не сохраняйте пароль в DTO, queue payload, log или `LdapSyncState`. ## Admin workflow LDAP admin регистрирует пользователей, OU, department OU-groups и группы. Доступны actions LDAP → Django, Django → LDAP, просмотр diff, изменение пароля и явное удаление из LDAP. Безопасный порядок ручной операции: 1. выберите объект и откройте diff; 2. определите фактический источник истины; 3. синхронизируйте в одном направлении; 4. повторно проверьте diff и `LdapSyncState`; 5. при ошибке проверьте `LdapSyncQueue`, Celery worker и LDAP connection; 6. delete action используйте только если удаление объекта каталога действительно требуется. ## Проверка Unit и mocked integration tests не требуют живого AD: ```bash cd backend pytest employees/ldap/tests tests/employees -q ``` API fallback без LDAP: ```bash cd backend pytest tests/api/v1/employees/test_ldap_optional_*.py -q ``` Live roundtrip tests запускаются только против специально подготовленного тестового каталога: ```bash cd backend pytest tests/manual/ldap_integration/test_entities_roundtrip.py -q ``` Никогда не запускайте manual roundtrip suite против production OU: тесты создают и изменяют реальные LDAP-объекты. ## Диагностика - Login не использует LDAP: проверьте `LDAP_ENABLED`, URI, user base и filter. - ORM сообщает об отсутствии alias `ldap`: проверьте `LDAP_WRITE_ENABLED` до запуска Django. - `502 ldap_error` при регистрации/смене пароля: проверьте bind account, TLS/CA и права на нужный OU. - Django изменился, LDAP нет: найдите запись в `LdapSyncQueue` и проверьте Celery worker/Beat. - Объект найден дважды: исправьте `LdapSyncState` и employee id attribute, не связывайте по CN вручную. - Перемещение отклонено AD: проверьте escaped RDN, target OU и право `modify_dn` service account.