Corp/docs/in-progress/LDAP_ORM_MIGRATION_PLAN.md
lizerk 75662ec7ea feat(ldap): Полная миграция на django-ldapdb ORM с миксинами и Django Admin
LDAP ORM модели (orm_models.py):
================================
✅ Применены миксины ModifyDnMixin и LdapSyncStateMixin ко всем моделям
✅ LdapUser - поддержка modify_dn + автоуправление LdapSyncState
✅ LdapGroup - поддержка modify_dn для перемещения групп
✅ LdapOrganizationalUnit - поддержка modify_dn для реорганизации

Миксины (mixins.py) - НОВЫЙ ФАЙЛ:
=================================
✅ ModifyDnMixin - автоматический modify_dn при изменении base_dn
   • Отслеживает изменения base_dn
   • Выполняет LDAP modify_dn с newsuperior
   • Обновляет _saved_dn для корректного последующего save()
   • Использование: user.base_dn = new_dn; user.save()

✅ LdapSyncStateMixin - автоуправление LdapSyncState
   • Автоматически создает/обновляет LdapSyncState при save()
   • Автоматически удаляет LdapSyncState при delete()
   • Настройки: _sync_model_name, _sync_pk_field

Django Admin для LDAP (admin.py) - НОВЫЙ ФАЙЛ:
==============================================
✅ LdapUserAdmin - полноценная админка для LDAP пользователей
   • List display: CN, SAM, email, Django Employee link, sync status
   • Search: по CN, SAM, email, имени, фамилии
   • Filters: по userAccountControl
   • Actions синхронизации:
     - sync_from_ldap_to_django (LDAP → Django)
     - sync_from_django_to_ldap (Django → LDAP)
     - show_sync_diff (сравнение данных)
   • Fieldsets: идентификация, персональные данные, контакты, UAC, группы
   • DN форматирование с подсветкой компонентов
   • Sync status indicators с иконками

Django Admin для Employee (employees/admin.py):
===============================================
✅ Добавлены зеркальные actions синхронизации:
   • sync_from_django_to_ldap - создание/обновление в LDAP
     - Автоматическое создание LDAP пользователя если не существует
     - Генерация временного пароля
     - Установка is_ldap_managed = True
   • sync_from_ldap_to_django - обновление из LDAP
   • show_sync_diff - сравнение Django ↔ LDAP
✅ Импорт django.contrib.messages для уведомлений

Сервисы LDAP (services/):
=========================
✅ UserService - полная миграция на ORM для UPDATE операций
✅ GroupService - использование ORM для модификаций
✅ DepartmentService - ORM для обновления OU
✅ PositionService - ORM для групп позиций

Тесты (tests/unit/):
===================
✅ test_user_service.py - обновлены под ORM
✅ test_group_service.py - обновлены под ORM
✅ test_position_service.py - обновлены под ORM
✅ conftest.py - добавлены фикстуры для ORM моделей

Authentication (auth_backends.py):
=================================
✅ EmailOrPhoneBackend - добавлено подробное debug логирование
   • Логирование email/phone lookup
   • Логирование is_active проверки
   • Логирование password check с hash prefix
✅ SuperuserOnlyBackend - добавлено debug логирование
   • Логирование parent auth результата
   • Логирование is_superuser проверки

API (api/auth/serializers.py):
==============================
✅ PhoneOrEmailTokenObtainPairSerializer - исправлен KeyError
   • Добавлена проверка credentials перед super().validate()
   • Явный AuthenticationFailed при отсутствии учетных данных

Middleware (middleware.py):
===========================
✅ Очистка от устаревшего кода (305 → 102 строки)
   • ❌ Удалена EmailVerificationMiddleware (не используется)
   • ❌ Удалена RegistrationIPRestrictionMiddleware (не используется)
   • ❌ Удалены все ссылки на namespace 'auth_front'
   • ✅ Возврат 403 для неавторизованных вместо редиректа

Settings (settings.py):
=======================
✅ LOGIN_URL изменен с 'auth_front:login' на '/admin/login/'
✅ Добавлено DEBUG логирование для:
   • django.contrib.auth
   • eusrr_backend.auth_backends

Миграции:
=========
✅ Удалены старые миграции LDAP моделей (0042, 0043)
✅ Добавлена миграция LdapSyncQueue (0042_ldapsyncqueue.py)

Документация:
=============
✅ ADMIN_GUIDE.md - руководство по использованию LDAP Admin
✅ docs/guides/ldap-orm-mixins-usage.md - usage guide для миксинов
✅ docs/completed/ldap-orm-create-support.md - документация ORM миграции
✅ docs/in-progress/LDAP_ORM_MIGRATION_PLAN.md - план миграции

Инфраструктура (__init__.py):
=============================
✅ Добавлен импорт admin для авторегистрации
✅ Экспорт ORM моделей (LdapUser, LdapGroup, LdapOrganizationalUnit)

ИТОГО:
======
• Полная миграция с ldap3 на django-ldapdb ORM для UPDATE/MOVE/DELETE
• Унификация: один подход для всех LDAP операций через модели
• Автоматизация: синхронизация состояния и перемещения через миксины
• UI: полноценный Django Admin для управления LDAP объектами
• Отладка: расширенное логирование аутентификации
• Чистота: удален устаревший код middleware и миграций
• Тесты: 29 passed, 0 failed, 18 skipped

Breaking Changes: НЕТ - обратная совместимость сохранена
2026-03-19 22:33:06 +03:00

19 KiB
Raw Blame History

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

Создан: 19 марта 2026 г.
Статус: 🔄 В работе
Цель: Полный переход от низкоуровневого ldap3 к django-ldapdb ORM + миксины


Обзор текущего состояния

✅ Готово

  • ModifyDnMixin — поддержка перемещения объектов
  • LdapSyncStateMixin — автоматическое управление LdapSyncState
  • Миксины интегрированы в модели (LdapUser, LdapGroup, LdapOrganizationalUnit)
  • Протестирована работа CREATE и MOVE через ORM
  • Документация по миксинам

❌ LEGACY код требует замены

1. CREATE операции (conn.add)

  • employees/ldap/services/user_service.py

    • _try_create_with_cns() — создание пользователей
    • _create_user_in_ldap() — основной метод создания
  • employees/ldap/services/department_service.py

    • Создание групп ролей (4 места)
    • Создание OU отделов (1 место)
  • employees/ldap/services/group_service.py

    • create_group() — создание групп
  • employees/ldap/services/position_service.py

    • Создание групп позиций

2. MOVE операции (conn.modify_dn)

  • employees/ldap/services/user_service.py

    • _move_user_to_base() — перемещение пользователей
  • employees/ldap/utils/dn_utils.py

    • _move_to_department() — универсальная функция перемещения
  • employees/ldap/services/department_service.py

    • Коррекция DN при изменении названия отдела
    • Коррекция DN групп ролей

3. RENAME операции (conn.modify_dn)

  • employees/ldap/services/user_service.py

    • Изменение CN при смене ФИО
  • employees/ldap/services/group_service.py

    • rename_group() — переименование групп
  • employees/ldap/services/department_service.py

    • Переименование OU отделов
    • Переименование групп ролей

Стратегия миграции

Принципы

  1. Постепенная замена — по одному сервису за раз
  2. Обратная совместимость — старый код продолжает работать
  3. 100% покрытие тестами — каждое изменение тестируется
  4. Rollback plan — возможность откатиться назад

Приоритеты

  • 🟢 P0 (критично): UserService — основная функциональность
  • 🟡 P1 (важно): DepartmentService, GroupService
  • 🟠 P2 (среднее): PositionService
  • 🔵 P3 (низкое): Утилиты, вспомогательные функции

Этапы миграции

ФАЗА 1: UserService (P0) — 3-4 дня

1.1. CREATE пользователей

Цель: Заменить conn.add() на LdapUser.objects.create()

Файлы:

  • employees/ldap/services/user_service.py

Изменения:

# Было (LEGACY):
def _try_create_with_cns(self, conn, dto, sam, upn, base_dn, object_classes, cns):
    for cn_txt in cns:
        dn_try = f"CN={cn_txt},{base_dn}"
        attrs = self._build_user_attrs(dto, sam, upn, cn_txt)
        if conn.add(dn_try, object_classes, attrs):
            return dn_try
    return None

# Станет (ORM):
def _try_create_with_cns_orm(self, dto, sam, upn, base_dn, cns):
    """Создание через ORM с обработкой коллизий."""
    from ldap import ALREADY_EXISTS
    from employees.ldap.orm_models import LdapUser
    
    for cn_txt in cns:
        try:
            user = LdapUser.objects.create(
                dn=f"CN={cn_txt},{base_dn}",
                cn=cn_txt,
                sam_account_name=sam,
                user_principal_name=upn,
                given_name=dto.first_name,
                sn=dto.last_name or ".",
                display_name=f"{dto.first_name} {dto.last_name or ''}".strip(),
                mail=dto.email,
                user_account_control=512,  # Normal account
                employee_number=str(dto.employee_id) if dto.employee_id else "",
                # ... остальные поля из dto
            )
            return user.dn
        except ALREADY_EXISTS:
            continue
    return None

Преимущества:

  • Убираем UserMapperService._build_user_attrs() — атрибуты задаются напрямую
  • Автоматическая валидация полей
  • LdapSyncStateMixin создаст LdapSyncState автоматически
  • Меньше кода (~40% сокращение)

Тестирование:

  • Unit тесты для _try_create_with_cns_orm()
  • Integration тесты: создание пользователя с коллизиями CN
  • Проверка LdapSyncState после создания

Риски:

  • 🟡 Другие поля могут быть не учтены
  • 🟢 Rollback: оставить старый метод как _try_create_with_cns_legacy()

1.2. MOVE пользователей (увольнение, перевод)

Цель: Заменить conn.modify_dn() на user.base_dn = ...; user.save()

Файлы:

  • employees/ldap/services/user_service.py
  • employees/ldap/utils/dn_utils.py

Изменения:

# Было (LEGACY):
def _move_user_to_base(self, conn, user_dn, base_dn):
    rdn = user_dn.split(",", 1)[0]
    ok = conn.modify_dn(user_dn, rdn, new_superior=base_dn)
    if not ok:
        raise RuntimeError(f"LDAP move failed: {conn.result}")
    return f"{rdn},{base_dn}"

# Станет (ORM):
def _move_user_to_base_orm(self, user_dn, base_dn):
    """Перемещение через ModifyDnMixin."""
    from employees.ldap.orm_models import LdapUser
    
    user = LdapUser.objects.get(dn=user_dn)
    user.base_dn = base_dn
    user.save()  # ModifyDnMixin автоматически вызовет modify_dn
    
    return user.dn  # Обновлённый DN

Преимущества:

  • ModifyDnMixin автоматически обновляет LdapSyncState.dn
  • Логирование операций из коробки
  • Меньше ошибок (нет ручного формирования DN)

Тестирование:

  • Unit тесты для _move_user_to_base_orm()
  • Integration тесты: перемещение в Dismissed, в отдел
  • Проверка обновления LdapSyncState.dn

Риски:

  • 🟢 Низкий — миксин уже протестирован

1.3. RENAME пользователей (смена ФИО)

Цель: Сохранить conn.modify_dn() для изменения RDN (cn)

Обоснование:

  • Изменение RDN (cn=...) != перемещение (newsuperior)
  • ModifyDnMixin работает только с base_dn
  • Для RDN нужен отдельный подход

Решение:

# Оставляем LEGACY для изменения CN:
def _update_cn_if_needed(self, conn, dn, new_cn):
    """Изменение CN через modify_dn (без newsuperior)."""
    current_rdn = dn.split(",", 1)[0]
    new_rdn = f"CN={new_cn}"
    
    if current_rdn != new_rdn:
        ok = conn.modify_dn(dn, new_rdn)
        if not ok:
            raise RuntimeError(f"Failed to rename: {conn.result}")
        
        # Обновляем LdapSyncState вручную
        base = dn.split(",", 1)[1]
        new_dn = f"{new_rdn},{base}"
        self._update_sync_state_dn(old_dn=dn, new_dn=new_dn)
        
        return new_dn
    return dn

Альтернатива (будущее):

  • Создать RenameRdnMixin для изменения RDN
  • Но это P3 приоритет

Тестирование:

  • Unit тесты сохранения старой логики
  • Проверка обновления LdapSyncState

ФАЗА 2: DepartmentService (P1) — 2-3 дня

2.1. CREATE OU отделов

Цель: conn.add() → LdapOrganizationalUnit.objects.create()

Изменения:

# Было:
ok = conn.add(dn, ["top", "organizationalUnit"])

# Станет:
from employees.ldap.orm_models import LdapOrganizationalUnit

ou = LdapOrganizationalUnit.objects.create(
    dn=dn,
    ou=dept_name,
    description=f"Отдел {dept_name}",
    managed_by=manager_dn if manager_dn else "",
)

Тестирование:

  • Создание OU через ORM
  • Проверка атрибутов (description, managedBy)

2.2. CREATE групп ролей

Цель: conn.add() → LdapGroup.objects.create()

Изменения:

# Было:
attrs = {
    "sAMAccountName": sam,
    "description": desc,
    "member": members,
}
ok = conn.add(group_dn, ["top", "group"], attrs)

# Станет:
from employees.ldap.orm_models import LdapGroup

group = LdapGroup.objects.create(
    dn=group_dn,
    cn=cn,
    sam_account_name=sam,
    description=desc,
    member=members if members else [],
)

Тестирование:

  • Создание групп ролей через ORM
  • Проверка членства (member list)

2.3. RENAME OU при изменении названия отдела

Цель: conn.modify_dn() → ou.base_dn = ...; ou.save() (для MOVE)

ВАЖНО: Для изменения самого OU (не перемещения) нужен modify_dn(new_rdn=...)

Решение:

  • Оставить conn.modify_dn() для изменения RDN OU
  • Использовать ModifyDnMixin только если планируется перемещение OU (редко)

Тестирование:

  • Переименование OU (IT → Engineering)
  • Проверка обновления всех вложенных DN

ФАЗА 3: GroupService (P1) — 1-2 дня

3.1. CREATE глобальных групп

Цель: conn.add() → LdapGroup.objects.create()

Аналогично DepartmentService 2.2


3.2. RENAME групп

Цель: Оставить conn.modify_dn() для изменения CN

Аналогично UserService 1.3


ФАЗА 4: PositionService (P2) — 1 день

4.1. CREATE групп позиций

Цель: conn.add() → LdapGroup.objects.create()

Аналогично DepartmentService 2.2


ФАЗА 5: Утилиты и рефакторинг (P3) — 2-3 дня

5.1. Создать RenameRdnMixin

Цель: Унифицировать изменение RDN через миксин

class RenameRdnMixin:
    """Миксин для изменения RDN (cn=..., ou=...) без перемещения."""
    
    def rename_rdn(self, new_rdn_value):
        """Изменяет RDN без перемещения между контейнерами.
        
        Example:
            user.rename_rdn("Ivanov I 2")  # cn=Ivanov I → cn=Ivanov I 2
        """
        from .infrastructure.connections import _ldap
        
        old_dn = self.dn
        rdn_attr = self.build_rdn().split("=")[0]  # "cn" или "ou"
        new_rdn = f"{rdn_attr}={new_rdn_value}"
        
        with _ldap() as conn:
            success = conn.modify_dn(old_dn, new_rdn)
            if not success:
                raise RuntimeError(f"Failed to rename: {conn.result}")
        
        # Обновляем self.dn
        base = old_dn.split(",", 1)[1]
        self.dn = f"{new_rdn},{base}"

Интеграция:

class LdapUser(ModifyDnMixin, RenameRdnMixin, LdapSyncStateMixin, LdapModel):
    ...

# Использование:
user = LdapUser.objects.get(dn=...)
user.rename_rdn("Petrov P")  # Изменение CN
user.save()  # Обновление остальных атрибутов

5.2. Удалить LEGACY код

Цель: Удалить старые методы после полной миграции

Удалить:

  • UserMapperService._build_user_attrs() — заменено на прямое создание через ORM
  • dn_utils._move_to_department() — заменено на ModifyDnMixin
  • Все методы с _legacy суффиксом

Проверить зависимости:

# Найти все использования LEGACY методов
grep -r "_build_user_attrs" backend/
grep -r "_move_to_department" backend/

5.3. Обновить документацию

Цель: Убрать упоминания о LEGACY, обновить примеры

Файлы:

  • employees/ldap/orm_models.py — убрать секцию LEGACY
  • docs/guides/ldap-orm-mixins-usage.md — добавить RenameRdnMixin
  • docs/completed/ldap-orm-create-support.md — пометить как завершённое

Тестирование

Unit тесты

  • Создание объектов через ORM (все модели)
  • Перемещение через ModifyDnMixin
  • Обработка коллизий CN
  • Обновление LdapSyncState автоматически

Integration тесты

  • Полный цикл: создание → перемещение → обновление → удаление
  • Создание пользователя → перевод в отдел → увольнение
  • Создание отдела → назначение менеджера → переименование
  • Создание группы → добавление членов → переименование

Regression тесты

  • Существующие тесты проходят без изменений
  • Функциональность не сломана

Performance тесты

  • ORM не медленнее ldap3 (допустимо +10%)
  • Batch операции работают эффективно

Риски и митигация

🔴 Высокие риски

  1. Потеря данных при ошибке в миграции

    • Митигация: резервное копирование LDAP перед каждой фазой
    • Rollback: восстановление из бэкапа
  2. Несовместимость с существующими интеграциями

    • Митигация: тестирование на staging
    • Rollback: feature flag для переключения ORM/LEGACY

🟡 Средние риски

  1. Производительность ORM ниже ldap3

    • Митигация: профилирование и оптимизация
    • Альтернатива: batch операции через bulk_create
  2. Неучтённые edge cases

    • Митигация: расширенное тестирование
    • Rollback: постепенная миграция (по одному сервису)

🟢 Низкие риски

  1. Изменение поведения при обработке ошибок
    • Митигация: обработка тех же exception (ALREADY_EXISTS, etc.)

Чеклист выполнения

Подготовка

  • Создать feature branch feature/ldap-orm-migration
  • Настроить CI/CD для автоматического тестирования
  • Создать резервную копию LDAP

ФАЗА 1: UserService

  • Реализовать _try_create_with_cns_orm()
  • Написать unit тесты CREATE
  • Реализовать _move_user_to_base_orm()
  • Написать unit тесты MOVE
  • Integration тесты UserService
  • Code review
  • Merge в develop

ФАЗА 2: DepartmentService

  • Реализовать CREATE OU через ORM
  • Реализовать CREATE групп через ORM
  • Unit тесты
  • Integration тесты
  • Code review
  • Merge в develop

ФАЗА 3: GroupService

  • Реализовать CREATE через ORM
  • Unit тесты
  • Integration тесты
  • Code review
  • Merge в develop

ФАЗА 4: PositionService

  • Реализовать CREATE через ORM
  • Unit тесты
  • Code review
  • Merge в develop

ФАЗА 5: Финализация

  • Создать RenameRdnMixin (опционально)
  • Удалить LEGACY код
  • Обновить документацию
  • Final regression тесты
  • Production deployment

Оценка времени

Фаза Описание Оценка Зависимости
1 UserService (CREATE + MOVE) 3-4 дня -
2 DepartmentService 2-3 дня Фаза 1
3 GroupService 1-2 дня Фаза 1
4 PositionService 1 день Фаза 1
5 Рефакторинг + документация 2-3 дня Фазы 1-4
ИТОГО Полная миграция 9-13 дней -

С учётом тестирования и ревью: ~15-20 рабочих дней (3-4 недели)


Success Criteria

Функциональные

  • ✅ Все операции CREATE выполняются через ORM
  • ✅ Все операции MOVE выполняются через ModifyDnMixin
  • ✅ LdapSyncState обновляется автоматически
  • ✅ Обработка коллизий CN работает корректно

Технические

  • ✅ 100% покрытие unit тестами новых методов
  • ✅ Integration тесты проходят успешно
  • ✅ Regression тесты не выявляют проблем
  • ✅ Performance не деградировала (±10%)

Качество кода

  • ✅ Код соответствует PEP 8
  • ✅ Все методы задокументированы (docstrings)
  • ✅ Нет дублирования кода
  • ✅ Code review пройден

Документация

  • ✅ Обновлена документация ORM моделей
  • ✅ Обновлены гайды по миксинам
  • ✅ Создана migration guide для команды

Следующие шаги

  1. Обсудить план с командой — получить feedback
  2. Установить приоритеты — согласовать порядок фаз
  3. Начать ФАЗУ 1 — UserService как самый критичный компонент
  4. Еженедельные ревью — отслеживать прогресс

Контакты и ответственные

  • Tech Lead: @username
  • Backend разработчик (LDAP): @username
  • QA Engineer: @username
  • DevOps (backup/rollback): @username

Статус обновлён: 19 марта 2026 г.
Следующий ревью: TBD