feat(ldap): добавлена интеграция с LdapSyncState

Добавлена полная поддержка связи Employee ↔ LdapUser через LdapSyncState.

Ключевые изменения:
- orm_services.py теперь автоматически создает/обновляет/удаляет LdapSyncState
- create_user() создает LdapSyncState при указании employee_pk
- update_user() обновляет last_django_modify_ts в LdapSyncState
- delete_user() удаляет LdapSyncState при hard delete
- Добавлен метод get_user_dn_by_employee_pk() для быстрого получения DN
- Улучшен метод get_user_by_employee_pk() - сначала ищет через LdapSyncState

Архитектура связи:
Employee (pk=123, PostgreSQL)
    ↓ object_pk
LdapSyncState (ldap_dn='CN=...', PostgreSQL)
    ↓ ldap_dn
LdapUser (dn='CN=...', employee_number='123', LDAP server)

Обновлена документация:
- ORM_INTEGRATION.md теперь объясняет связь моделей
- Добавлена диаграмма архитектуры
- Описаны роли каждой модели
- Объяснено почему нет ForeignKey (разные БД)
This commit is contained in:
lizerk 2026-03-18 13:53:22 +03:00
parent 2eb2da4c5a
commit 8cdfbfb6ef
2 changed files with 178 additions and 1 deletions

View File

@ -11,6 +11,106 @@ GET запросы → Employee (PostgreSQL) - быстро ⚡
POST/PUT/DELETE → LdapUser (django-ldapdb ORM) - просто ✨
```
## Связь моделей
### Архитектура связи
```
┌─────────────────────────┐
│ Employee │ (PostgreSQL, default database)
│ - pk: 123 │
│ - first_name: "John" │
│ - last_name: "Doe" │
│ - email: "jdoe@..." │
└─────────────────────────┘
↓ связь через object_pk
┌─────────────────────────┐
│ LdapSyncState │ (PostgreSQL, default database)
│ - model: 'employee' │ ← тип модели
│ - object_pk: '123' │ ← Employee.pk (строка)
│ - ldap_dn: 'CN=John...'│ ← DN в LDAP
│ - ldap_guid: '{UUID}' │ ← objectGUID из AD
│ - last_django_modify_ts│ ← метка времени
│ - last_sync_dir │ ← направление синхронизации
└─────────────────────────┘
↓ связь через ldap_dn
┌─────────────────────────┐
│ LdapUser │ (LDAP server, ldap database)
│ - dn: 'CN=John...' │ ← Distinguished Name (первичный ключ)
│ - cn: "John Doe" │
│ - sam_account_name: "jdoe"
│ - employee_number: '123'│ ← Employee.pk (опционально, для быстрого поиска)
│ - given_name: "John" │
│ - sn: "Doe" │
│ - mail: "jdoe@..." │
└─────────────────────────┘
```
### Роли моделей
**Employee** (employees/models.py):
- Основная модель сотрудника в PostgreSQL
- Используется для всех GET запросов (быстро)
- Хранит все данные для отображения в UI
**LdapSyncState** (employees/models.py):
- Промежуточная таблица маппинга
- Связывает Employee.pk ↔ LDAP DN
- Хранит метки времени для синхронизации
- Позволяет отследить изменения
**LdapUser** (employees/ldap/orm_models.py):
- ORM модель для записи в LDAP через django-ldapdb
- Используется ТОЛЬКО для POST/PUT/DELETE операций
- Автоматически записывает изменения в Active Directory
### Методы связи в ORM сервисах
```python
from employees.ldap.orm_services import LdapOrmUserService
svc = LdapOrmUserService()
# 1. Создание: автоматически создает LdapSyncState
user = svc.create_user(
sam_account_name="jdoe",
first_name="John",
last_name="Doe",
email="jdoe@example.com",
employee_pk=123, # ← связь с Employee
)
# Создает LdapSyncState(model='employee', object_pk='123', ldap_dn=user.dn)
# 2. Получение DN по Employee.pk (быстро, без LDAP запроса)
dn = svc.get_user_dn_by_employee_pk(employee_pk=123)
# Читает из LdapSyncState.ldap_dn
# 3. Получение LdapUser по Employee.pk
ldap_user = svc.get_user_by_employee_pk(employee_pk=123)
# Сначала ищет DN через LdapSyncState, затем делает LDAP запрос
# 4. Обновление: автоматически обновляет LdapSyncState
svc.update_user(dn, first_name="Johnny")
# Обновляет LdapSyncState.last_django_modify_ts
# 5. Удаление: автоматически удаляет LdapSyncState (при hard delete)
svc.delete_user(dn, soft=False)
# Удаляет LdapSyncState для этого employee_pk
```
### Почему нет ForeignKey?
**LdapUser** и **Employee** находятся в **разных базах данных**:
- Employee → PostgreSQL (database='default')
- LdapUser → LDAP server (database='ldap')
Django **не поддерживает ForeignKey между разными базами данных**.
Поэтому используется:
- `LdapSyncState.object_pk` (CharField) - хранит str(Employee.pk)
- `LdapUser.employee_number` (CharField) - хранит str(Employee.pk) в LDAP
- Связь **слабая** (через строки), но это **нормально** для multi-database setup
## Компоненты
### 1. LDAP ORM Модели

View File

@ -18,7 +18,9 @@ import logging
from typing import Optional
from django.conf import settings
from django.utils import timezone
from employees.models import LdapSyncState
from .orm_models import LdapUser, LdapGroup, LdapOrganizationalUnit
from .utils.text_utils import esc_rdn
@ -99,6 +101,23 @@ class LdapOrmUserService:
# Сохранение в LDAP
user.save()
# Создание/обновление LdapSyncState для связи Employee ↔ LDAP
if employee_pk:
# Получаем objectGUID после создания (если доступен)
guid_str = str(user.object_guid) if hasattr(user, 'object_guid') and user.object_guid else None
LdapSyncState.objects.update_or_create(
model='employee',
object_pk=str(employee_pk),
defaults={
'ldap_dn': dn,
'ldap_guid': guid_str,
'last_django_modify_ts': timezone.now(),
'last_sync_dir': 'django',
}
)
logger.info(f"Created LdapSyncState for Employee.pk={employee_pk}, DN={dn}")
logger.info(f"Created LDAP user: {dn} (sAMAccountName={sam_account_name})")
return user
@ -167,6 +186,20 @@ class LdapOrmUserService:
if updated:
user.save()
# Обновление LdapSyncState (если есть employee_number)
if user.employee_number:
try:
sync_state = LdapSyncState.objects.get(
model='employee',
object_pk=user.employee_number
)
sync_state.last_django_modify_ts = timezone.now()
sync_state.last_sync_dir = 'django'
sync_state.save(update_fields=['last_django_modify_ts', 'last_sync_dir', 'updated_at'])
except LdapSyncState.DoesNotExist:
logger.warning(f"LdapSyncState not found for employee_pk={user.employee_number}")
logger.info(f"Updated LDAP user: {user_dn}")
return user
@ -193,7 +226,19 @@ class LdapOrmUserService:
logger.info(f"Soft deleted (disabled) LDAP user: {user_dn}")
else:
# Hard delete - удаление из LDAP
employee_pk = user.employee_number if hasattr(user, 'employee_number') else None
user.delete()
# Удаление LdapSyncState при полном удалении
if employee_pk:
deleted_count = LdapSyncState.objects.filter(
model='employee',
object_pk=employee_pk
).delete()[0]
if deleted_count:
logger.info(f"Deleted LdapSyncState for employee_pk={employee_pk}")
logger.info(f"Deleted LDAP user: {user_dn}")
def get_user_by_dn(self, dn: str) -> Optional[LdapUser]:
@ -204,11 +249,43 @@ class LdapOrmUserService:
return None
def get_user_by_employee_pk(self, employee_pk: int) -> Optional[LdapUser]:
"""Получает пользователя из LDAP по Employee.pk."""
"""
Получает пользователя из LDAP по Employee.pk.
Сначала пытается найти через LdapSyncState (более надежно),
затем через employeeNumber атрибут в LDAP.
"""
# Способ 1: через LdapSyncState (рекомендуется)
try:
sync_state = LdapSyncState.objects.get(
model='employee',
object_pk=str(employee_pk)
)
if sync_state.ldap_dn:
return self.get_user_by_dn(sync_state.ldap_dn)
except LdapSyncState.DoesNotExist:
pass
# Способ 2: через employeeNumber в LDAP (fallback)
try:
return LdapUser.objects.get(employee_number=str(employee_pk))
except LdapUser.DoesNotExist:
return None
def get_user_dn_by_employee_pk(self, employee_pk: int) -> Optional[str]:
"""
Получает DN пользователя в LDAP по Employee.pk через LdapSyncState.
Это быстрый метод без запроса к LDAP серверу.
"""
try:
sync_state = LdapSyncState.objects.get(
model='employee',
object_pk=str(employee_pk)
)
return sync_state.ldap_dn or None
except LdapSyncState.DoesNotExist:
return None
class LdapOrmGroupService: