11 KiB
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 автоматически. Для полной интеграции задавайте обе переменные явно.
Минимальная конфигурация
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 сохранены для
обратной совместимости:
POST /api/auth/token/
{
"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-операция завершается инфраструктурной ошибкой:
- создаётся
LdapSyncQueue; - система пытается немедленно поставить
process_ldap_queue_itemв Celery; - если Celery недоступен, periodic task
employees.tasks.process_ldap_queueподберёт запись; - Celery Beat проверяет очередь каждую минуту;
- после
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.
Пароли
Пользователь меняет пароль через:
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.
Безопасный порядок ручной операции:
- выберите объект и откройте diff;
- определите фактический источник истины;
- синхронизируйте в одном направлении;
- повторно проверьте diff и
LdapSyncState; - при ошибке проверьте
LdapSyncQueue, Celery worker и LDAP connection; - delete action используйте только если удаление объекта каталога действительно требуется.
Проверка
Unit и mocked integration tests не требуют живого AD:
cd backend
pytest employees/ldap/tests tests/employees -q
API fallback без LDAP:
cd backend
pytest tests/api/v1/employees/test_ldap_optional_*.py -q
Live roundtrip tests запускаются только против специально подготовленного тестового каталога:
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_dnservice account.