Corp/docs/guides/LDAP.md

173 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_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 принимает email или телефон:
```text
POST /api/auth/token/
```
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.