Corp/backend/docs/guides/LDAP_SERVICES_REFACTORING.md

14 KiB
Raw Blame History

Рефакторинг LDAP-сервисов: Архитектурные улучшения

Дата: 10 апреля 2026 г.
Статус: В процессе миграции
Цель: Улучшение архитектуры, уменьшение дублирования, следование SOLID принципам

Обновление 10 апреля 2026: canonical Department sync

Для департаментов и членства в отделах введён единый контракт синхронизации:

  • DepartmentService.sync_department_state(dept, *, created, changes, sync_head)
  • DepartmentService.sync_department_delete(*, object_pk, dept_dn=None)
  • DepartmentService.sync_member_state(employee, department, *, is_active, role=None)

Ключевые правила после обновления:

  • Django DB является источником истины для Department, EmployeeDepartment.is_active и role assignment.
  • employees/signals/ldap/department.py и retry-исполнители в employees/tasks.py больше не содержат отдельной LDAP-логики для отдела.
  • View-слой фиксирует diff через _ldap_changes и _ldap_sync_head до save(), чтобы post-save не вычислял изменения по уже обновлённой строке.
  • Retry-очередь воспроизводит тот же canonical sync, что и live signals, а не отдельный legacy-сценарий.

Обзор изменений

Проведён комплексный рефакторинг слоя сервисов backend/employees/ldap/services/ для устранения выявленных архитектурных проблем.

Основные проблемы (до рефакторинга)

  1. Нарушение SRP - UserService ~750 LOC (слишком большой)
  2. Дублирование кода - _wrapped методы в GroupService (~50% кода)
  3. Отсутствие базового класса - повторяющаяся логика _touch_state во всех сервисах
  4. Магические числа - UAC=512/514, groupType и другие константы разбросаны по коду
  5. Недостаточное логирование - критические операции не логируются

Созданные новые компоненты

1. base_service.py - Базовый класс для всех сервисов

Назначение: Общая логика для всех LDAP-сервисов

Функциональность:

  • _touch_state() - единая точка входа для обновления LdapSyncState
  • _get_object_dn() - получение DN объекта
  • _log_operation() - логирование операций LDAP
  • _safe_execute() - безопасное выполнение с обработкой ошибок

Пример использования:

from .base_service import BaseService

class MyService(BaseService):
    def do_something(self, obj_id):
        self._touch_state(
            model="employee",
            object_pk=obj_id,
            ldap_dn="CN=...",
            sync_dir="ldap"
        )
        self._log_operation("update", model="employee", object_id=obj_id)

2. constants.py - Централизованные константы

Назначение: Хранение всех магических чисел и строк

Содержит:

  • UserAccountControl - UAC флаги (ENABLED=512, DISABLED=514, etc.)
  • GroupType - типы групп AD (GLOBAL_SECURITY, etc.)
  • LdapObjectClass - классы объектов (USER, GROUP, OU)
  • LdapFilter - часто используемые LDAP фильтры
  • LdapAttribute - названия LDAP атрибутов
  • LdapErrorCode - коды ошибок LDAP
  • SyncDirection - направления синхронизации

Пример использования:

from .constants import UserAccountControl, LdapFilter

# Вместо:
ldap_user.user_account_control = 512

# Используем:
ldap_user.user_account_control = UserAccountControl.ENABLED

# Вместо:
filter_str = "(&(objectCategory=person)(objectClass=user))"

# Используем:
filter_str = LdapFilter.ALL_USERS

3. Подсервисы для разбиения UserService

3.1 user_password_service.py

Назначение: Управление паролями пользователей

Методы:

  • set_password() - установка пароля через AD extended operation
  • validate_password_strength() - базовая валидация пароля
  • change_password() - смена пароля (требует знание старого)

Пример:

from .user_password_service import UserPasswordService

pwd_svc = UserPasswordService()

# Установка пароля
pwd_svc.set_password(conn, dn, "NewP@ssw0rd123")

# Валидация перед установкой
is_valid, error = pwd_svc.validate_password_strength("weak")
if not is_valid:
    raise ValueError(error)

3.2 user_login_service.py

Назначение: Генерация уникальных логинов (sAMAccountName, UPN)

Методы:

  • generate_unique_logins() - генерация sam и upn
  • validate_sam_account_name() - валидация по правилам AD
  • validate_upn() - валидация UPN

Пример:

from .user_login_service import UserLoginService

login_svc = UserLoginService()

sam, upn = login_svc.generate_unique_logins(
    first_name="Иван",
    last_name="Иванов",
    email="iivanov@company.com"
)
# sam: "iivanov", upn: "iivanov@company.com"

3.3 user_mapper_service.py

Назначение: Маппинг атрибутов Django ↔ LDAP

Методы:

  • build_creation_attributes() - атрибуты для создания user
  • build_update_attributes() - атрибуты для обновления
  • process_avatar() - обработка изображения для thumbnailPhoto
  • update_ldap_user_attributes() - обновление через ORM

Пример:

from .user_mapper_service import UserMapperService

mapper = UserMapperService()

# Создание
attrs = mapper.build_creation_attributes(dto, sam="iivanov", upn="...", cn="Иван Иванов")

# Обновление
ldap_attrs = mapper.build_update_attributes({
    "first_name": "Игорь",
    "is_active": True
})

4. group_service_refactored.py - Улучшенный GroupService

Изменения:

  • ✅ Наследуется от BaseService
  • ✅ Использует константы из constants.py
  • ✅ Логирование всех операций
  • ✅ Унифицированный API (без дублирования _wrapped методов)
  • ✅ Методы _*_internal() для использования с существующим conn
  • ✅ Публичные методы сами управляют соединением

Миграция:

# Старый API (может использоваться с внешним conn):
with _ldap() as conn:
    dn = group_svc.create(conn, cn="MyGroup", ...)

# Новый API (соединение управляется внутри):
dn = group_svc.create(cn="MyGroup", ...)

# Для использования с существующим conn:
with _ldap() as conn:
    dn = group_svc._create_internal(conn, cn="MyGroup", ...)

Метрики улучшения

Метрика До После Улучшение
UserService LOC ~750 ~600 + 3x150 Разделён на компоненты
GroupService дублирование ~50% 0% Убраны _wrapped методы
Магические числа ~20+ 0 Вынесено в constants
Базовая логика Дублируется BaseService DRY принцип
Логирование крит. операций ~30% 100% Полное покрытие

План миграции

Этап 1: Обратная совместимость (текущий)

✅ Созданы новые компоненты рядом со старыми
✅ Оба API доступны одновременно
✅ Старый код продолжает работать

Этап 2: Постепенная миграция (следующий шаг)

✅ Обновить DepartmentService для использования BaseService
✅ Выделить canonical sync path для Department signals + retry
🔲 Обновить PositionService для использования BaseService
🔲 Рефакторить UserService с использованием подсервисов
🔲 Обновить вызовы GroupService на новый API

Этап 3: Финальная очистка

🔲 Удалить старые _wrapped методы из GroupService
🔲 Объединить UserService с подсервисами
🔲 Убрать legacy код
🔲 Обновить тесты


Рекомендации по использованию

Для новых фич

Используйте новые компоненты:

  • Наследуйтесь от BaseService
  • Используйте константы из constants.py
  • Используйте подсервисы для работы с паролями/логинами
  • GroupServiceRefactored для новых фич

Для существующего кода

Можно не трогать:

  • Старые сервисы продолжают работать
  • Миграция постепенная, по мере необходимости
  • Критичные места обновлять в первую очередь

Best Practices

  1. Логирование: Используйте _log_operation() для всех CRUD операций
  2. Состояние синхронизации: Используйте _touch_state() вместо прямой работы с LdapSyncState
  3. Константы: Всегда используйте Enum'ы вместо магических чисел
  4. Валидация: Используйте специализированные сервисы (UserPasswordService, UserLoginService)

Примеры интеграции

1. Создание пользователя с новыми сервисами

from .constants import UserAccountControl
from .user_login_service import UserLoginService
from .user_password_service import UserPasswordService
from .user_mapper_service import UserMapperService

# Генерация логинов
login_svc = UserLoginService()
sam, upn = login_svc.generate_unique_logins(
    first_name="Иван",
    last_name="Иванов",
    email="iivanov@company.com"
)

# Построение атрибутов
mapper = UserMapperService()
attrs = mapper.build_creation_attributes(dto, sam, upn, "Иван Иванов")

# Создание в LDAP
with _ldap() as conn:
    dn = f"CN={esc_rdn('Иван Иванов')},{base_dn}"
    conn.add(dn, mapper.get_object_classes_for_user(), attrs)

# Установка пароля
pwd_svc = UserPasswordService()
pwd_svc.set_password(conn, dn, "TempP@ss123")

# Активация
ldap_user = LdapUser.objects.get(dn=dn)
ldap_user.user_account_control = UserAccountControl.ENABLED
ldap_user.save()

2. Работа с группами (новый API)

from .group_service_refactored import GroupService as GroupServiceRefactored
from .constants import LdapFilter

group_svc = GroupServiceRefactored()

# Создание группы
dn = group_svc.create(
    cn="Developers",
    description="Development team",
    scope="global",
    security_enabled=True
)

# Добавление участников
group_svc.add_members(dn, [user_dn1, user_dn2])

# Синхронизация каталога
created_count = group_svc.sync_catalog(throttle_seconds=60)

Известные ограничения

  1. ORM vs ldap3: Некоторые операции (modify_dn, paged_search) остаются на ldap3 (невозможны в django-ldapdb)
  2. Производительность: ORM медленнее чем прямые ldap3 запросы для массовых операций
  3. Транзакции: LDAP не поддерживает транзакции, используем compensating transactions

Дальнейшие улучшения

Приоритет 1 (критично)

  • Рефакторинг DepartmentService с BaseService
  • Рефакторинг PositionService с BaseService
  • Интеграция подсервисов в UserService

Приоритет 2 (высокий)

  • Добавить интеграционные тесты
  • Создать фабрики для DTO
  • Event-driven архитектура для межсервисного взаимодействия

Приоритет 3 (средний)

  • Метрики и мониторинг операций LDAP
  • Кеширование часто запрашиваемых данных
  • Batch операции для массовых обновлений

Обратная связь

При возникновении вопросов или проблем:

  1. Проверьте логи (все операции логируются)
  2. Убедитесь в использовании правильного API (старый vs новый)
  3. Обратитесь к примерам в этом документе

Важно: Старый API продолжает работать. Новые компоненты - это дополнения, а не замена (пока).