diff --git a/backend/employees/ldap/ORM_INTEGRATION.md b/backend/employees/ldap/ORM_INTEGRATION.md index 95a58954..deb9bcfa 100644 --- a/backend/employees/ldap/ORM_INTEGRATION.md +++ b/backend/employees/ldap/ORM_INTEGRATION.md @@ -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 Модели diff --git a/backend/employees/ldap/orm_services.py b/backend/employees/ldap/orm_services.py index 7239a5f5..38a97f37 100644 --- a/backend/employees/ldap/orm_services.py +++ b/backend/employees/ldap/orm_services.py @@ -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: