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:
parent
2eb2da4c5a
commit
8cdfbfb6ef
@ -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 Модели
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user