184 lines
11 KiB
Markdown
184 lines
11 KiB
Markdown
# 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.
|