Corp/backend/employees/ldap/sync_service.py
Игорь Надеин 4d04aafdb9 refactor: Replace inline CSS with components (Phase 3)
- Replaced feed-* inline styles in base.html with feed-cards.css component (~60 lines)
- Replaced ios-search inline styles in 3 templates with ios-search.css component
  * employees/employees_list.html
  * search/results.html
  * employees/department_list.html
- Eliminated ~150 lines of duplicate CSS across templates
- All 17 tests passing
2025-10-28 08:54:38 +03:00

349 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""DEPRECATED: Этот модуль сохранён для обратной совместимости.
Используйте вместо него:
from employees.ldap.services.sync_service import SyncService
Функции в этом модуле являются обёртками над новым SyncService.
"""
from __future__ import annotations
import logging
from typing import Iterable, Tuple
from django.conf import settings
from ldap3 import BASE
from ..models import Employee, EmployeeDepartment, LdapSyncState
from .config import SyncConfig
from .infrastructure.connections import _ldap
from .utils.dn_utils import (
_ensure_user_dn,
_move_to_department,
_target_department_ou_dn,
extract_department_from_dn,
)
from .utils.group_utils import (
_desired_group_cns_for_employee,
sync_user_groups_by_cns,
)
from .repositories.ldap_repository import (
is_taken,
read_attrs,
modify_user_attrs,
)
from .utils.ldap_utils import (
build_logins_for_user,
)
from .utils.image_utils import normalize_avatar_to_jpeg
from .services.sync_service import SyncService
logger = logging.getLogger(__name__)
# Создаём глобальный экземпляр для backward compatibility
_sync_service = SyncService()
# ==================== WRAPPER FUNCTIONS ====================
def import_departments(*, cfg: SyncConfig) -> Tuple[int, int, int]:
"""DEPRECATED: Используйте SyncService().import_departments().
Импорт OU отделов из LDAP в Django.
Args:
cfg: Конфигурация синхронизации.
Returns:
Tuple[int, int, int]: (created, updated, deleted).
"""
return _sync_service.import_departments(cfg)
def import_users(*, cfg: SyncConfig) -> Tuple[int, int, int]:
"""DEPRECATED: Используйте SyncService().import_users().
Импорт пользователей из LDAP в Django.
Args:
cfg: Конфигурация синхронизации.
Returns:
Tuple[int, int, int]: (created, updated, deleted).
"""
return _sync_service.import_users(cfg)
def export_users(*, cfg: SyncConfig) -> Tuple[int, int, int, int, int]:
"""DEPRECATED: Используйте SyncService().export_users().
Полный экспорт пользователей из Django в LDAP.
Args:
cfg: Конфигурация синхронизации.
Returns:
Tuple[int, int, int, int, int]:
(logins_set, moved, avatars_set, groups_added, groups_removed).
"""
return _export_users_legacy(cfg=cfg)
def export_users_delete(
*, cfg: SyncConfig, employees: Iterable[Employee]
) -> int:
"""DEPRECATED: Используйте SyncService().export_users_delete().
Удаляет учётные записи пользователей в LDAP.
Args:
cfg: Конфигурация синхронизации.
employees: Итератор пользователей для удаления.
Returns:
int: Количество успешно удалённых DN.
"""
return _sync_service.export_users_delete(cfg, employees)
# ==================== LEGACY IMPLEMENTATIONS ====================
# Эти функции сохранены для сложной логики export_users,
# которая ещё не полностью перенесена в новые сервисы
def _employees_with_dn_qs():
"""Возвращает QuerySet сотрудников с LDAP DN."""
emp_ids = (
LdapSyncState.objects.filter(model="employee")
.exclude(ldap_dn="")
.values_list("object_pk", flat=True)
)
return Employee.objects.filter(pk__in=list(emp_ids)).select_related()
def _ensure_and_persist_user_dn(conn, emp, *, do_write):
"""Обеспечивает наличие DN и сохраняет в LdapSyncState."""
user_dn = _ensure_user_dn(conn, emp)
if do_write:
st, _ = LdapSyncState.objects.get_or_create(
model="employee", object_pk=str(emp.pk)
)
if (st.ldap_dn or "") != user_dn:
st.touch(ldap_dn=user_dn, sync_dir="ldap")
return user_dn
def _active_dept_name(emp: Employee) -> str | None:
"""Имя активного отдела сотрудника."""
link = (
EmployeeDepartment.objects.filter(employee=emp, is_active=True)
.select_related("department")
.first()
)
return link.department.name if link and link.department_id else None
def export_users_create_attrs(*, cfg: SyncConfig) -> int:
"""Создаёт недостающие sAMAccountName/UPN и базовые ФИО-атрибуты."""
upn_suffix: str = getattr(settings, "LDAP_UPN_SUFFIX", "robotail.local")
do_write = not cfg.dry_run
assigned = 0
with _ldap() as conn:
for emp in _employees_with_dn_qs():
try:
user_dn = _ensure_and_persist_user_dn(
conn, emp, do_write=do_write
)
except RuntimeError as e:
logger.warning("[WARN] %s", e)
continue
cur = read_attrs(
conn, user_dn, ["sAMAccountName", "userPrincipalName"]
)
if cur["sAMAccountName"] and cur["userPrincipalName"]:
continue
guid = (
LdapSyncState.objects.filter(
model="employee", object_pk=str(emp.pk)
)
.values_list("ldap_guid", flat=True)
.first()
)
sam, upn = build_logins_for_user(
first_name=emp.first_name or "",
last_name=emp.last_name or "",
email=emp.email or "",
upn_suffix=upn_suffix,
is_taken_sam=lambda s: is_taken(
conn, attributes={"sAMAccountName": s}
),
is_taken_upn=lambda u: is_taken(
conn, attributes={"userPrincipalName": u}
),
guid=guid,
)
modify_user_attrs(
conn,
user_dn,
{
"sAMAccountName": sam,
"userPrincipalName": upn,
"givenName": emp.first_name or None,
"sn": emp.last_name or None,
"displayName": (
f"{emp.first_name} {emp.last_name}".strip() or None
),
},
do_write=do_write,
)
assigned += 1
return assigned
def export_users_update_profile(
*, cfg: SyncConfig
) -> Tuple[int, int]:
"""Обновляет профильные данные: перемещение между отделами и аватары."""
do_write = not cfg.dry_run
moved = avatars_set = 0
with _ldap() as conn:
for emp in _employees_with_dn_qs():
try:
user_dn = _ensure_and_persist_user_dn(
conn, emp, do_write=do_write
)
except RuntimeError as e:
logger.warning("[WARN] %s", e)
continue
# MOVE между отделами
current_dept = extract_department_from_dn(user_dn)
target_dept = _active_dept_name(emp)
if target_dept and current_dept != target_dept:
if do_write:
_move_to_department(conn, user_dn, target_dept)
else:
target_ou = _target_department_ou_dn(target_dept)
if not (
conn.search(
target_ou,
"(objectClass=organizationalUnit)",
BASE,
attributes=["ou"],
)
and conn.entries
):
logger.warning(
"[WARN] Целевой OU не найден: %s", target_ou
)
continue
moved += 1
# avatar -> thumbnailPhoto
avatar_bytes: bytes | None = None
avatar_field = getattr(emp, "avatar", None)
if avatar_field and hasattr(avatar_field, "read"):
avatar_bytes = avatar_field.read()
if avatar_bytes:
try:
avatar_bytes = normalize_avatar_to_jpeg(avatar_bytes)
except Exception as exc:
logger.warning(
"[WARN] avatar: не удалось нормализовать для %s: %s",
user_dn,
exc,
)
else:
modify_user_attrs(
conn,
user_dn,
{"thumbnailPhoto": avatar_bytes},
do_write=do_write,
)
avatars_set += 1
return moved, avatars_set
def export_users_sync_groups(*, cfg: SyncConfig) -> Tuple[int, int]:
"""Приводит членства пользователей в LDAP-группах к целевому набору."""
do_write = not cfg.dry_run
groups_added = groups_removed = 0
with _ldap() as conn:
for emp in _employees_with_dn_qs():
try:
user_dn = _ensure_and_persist_user_dn(
conn, emp, do_write=do_write
)
except RuntimeError as e:
logger.warning("[WARN] %s", e)
continue
current_dept = extract_department_from_dn(user_dn)
target_dept = _active_dept_name(emp)
desired_cns = _desired_group_cns_for_employee(emp)
extra_bases: list[str] = []
dept_for_roles = target_dept or current_dept
if dept_for_roles:
dept_base = getattr(settings, "LDAP_DEPARTMENTS_BASE", "")
extra_bases.append(f"OU=Roles,OU={dept_for_roles},{dept_base}")
added, removed = sync_user_groups_by_cns(
conn,
user_dn,
desired_cns,
extra_bases=extra_bases,
do_write=do_write,
)
groups_added += added
groups_removed += removed
return groups_added, groups_removed
def _export_users_legacy(*, cfg: SyncConfig) -> Tuple[int, int, int, int, int]:
"""Legacy реализация export_users через старые функции."""
from django.utils import timezone
logins_set = export_users_create_attrs(cfg=cfg)
moved, avatars_set = export_users_update_profile(cfg=cfg)
groups_added, groups_removed = export_users_sync_groups(cfg=cfg)
do_write = not cfg.dry_run
with _ldap() as conn:
from django.db import transaction
with transaction.atomic():
now = timezone.now()
for emp in _employees_with_dn_qs().only("pk"):
try:
user_dn = _ensure_and_persist_user_dn(
conn, emp, do_write=do_write
)
except RuntimeError as e:
logger.warning("[WARN] state.touch пропущен: %s", e)
continue
state, _ = LdapSyncState.objects.get_or_create(
model="employee", object_pk=str(emp.pk)
)
state.touch(
ldap_dn=user_dn,
last_django_modify_ts=now,
sync_dir="django",
)
return logins_set, moved, avatars_set, groups_added, groups_removed