Corp/docs/guides/LDAP.md

11 KiB
Raw Blame History

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-операция завершается инфраструктурной ошибкой:

  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.

Пароли

Пользователь меняет пароль через:

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:

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_dn service account.