refactor(ldap): удалены неиспользуемые файлы и методы из employees/ldap
- Удалён orm_services.py (424 строки) — не использовался нигде - Удалён group_utils.py (142 строки) — заменён group_utils_orm.py - Перенесена _desired_group_cns_for_employee в group_utils_orm.py - DirectoryService: удалены ~200 строк неиспользуемых методов (position_*, department member helpers, conn-based group ops, employee_ids_to_dns, dns_to_employee_ids, etc.) - Очищены __init__.py и utils/__init__.py от устаревших импортов
This commit is contained in:
parent
b5b1b20a13
commit
1ffcee9d8a
@ -65,30 +65,15 @@ from .domain.dtos import (
|
||||
)
|
||||
|
||||
# Утилиты работы с группами
|
||||
from .utils.group_utils import sync_user_groups_by_cns
|
||||
from .utils.group_utils_orm import sync_user_groups_by_cns_orm
|
||||
|
||||
# ORM модели и сервисы
|
||||
# ORM модели
|
||||
from .orm_models import LdapUser, LdapGroup, LdapOrganizationalUnit
|
||||
from .orm_services import LdapOrmUserService, LdapOrmGroupService, LdapOrmDepartmentService
|
||||
|
||||
# Подключения
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
|
||||
# Обратная совместимость: обёртки над SyncService
|
||||
def import_departments(cfg=None):
|
||||
"""Обёртка для SyncService().import_departments(cfg)."""
|
||||
svc = SyncService()
|
||||
return svc.import_departments(cfg or SyncConfig())
|
||||
|
||||
|
||||
def import_users(cfg=None):
|
||||
"""Обёртка для SyncService().import_users(cfg)."""
|
||||
svc = SyncService()
|
||||
return svc.import_users(cfg or SyncConfig())
|
||||
|
||||
|
||||
def export_users(cfg=None):
|
||||
"""Обёртка для SyncService().export_users(cfg)."""
|
||||
svc = SyncService()
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from employees.models import LdapSyncState
|
||||
from ldap3 import Connection
|
||||
|
||||
from ..models import Department, DepartmentRole, Employee, Position
|
||||
from ..models import Department, Employee
|
||||
from .domain.dtos import DirectoryUserDTO, DirectoryDepartmentDTO
|
||||
|
||||
# ------------------------- Сервис LDAP → DB ------------------------- #
|
||||
@ -20,9 +19,6 @@ class DirectoryService:
|
||||
3) Post-actions (best effort)
|
||||
|
||||
В случае падения БД выполняется best-effort компенсация LDAP, где разумно.
|
||||
|
||||
NOTE: Этот класс постепенно рефакторится. Методы работы с пользователями
|
||||
делегируются к UserService (см. services/user_service.py).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@ -30,16 +26,12 @@ class DirectoryService:
|
||||
from .services.department_service import DepartmentService
|
||||
from .services.user_service import UserService
|
||||
from .services.group_service import GroupService
|
||||
from .services.position_service import PositionService
|
||||
|
||||
self._user_service = UserService()
|
||||
self._group_service = GroupService(directory_service=self)
|
||||
self._department_service = DepartmentService(
|
||||
group_service=self._group_service, user_service=self._user_service
|
||||
)
|
||||
self._position_service = PositionService(
|
||||
group_service=self._group_service, user_service=self._user_service
|
||||
)
|
||||
|
||||
# ====================== ВСПОМОГАТЕЛЬНЫЕ ХЕЛПЕРЫ ====================== #
|
||||
|
||||
@ -151,175 +143,29 @@ class DirectoryService:
|
||||
"""Удаляет отдел: исключает сотрудников → удаляет группу → OU → БД."""
|
||||
return self._department_service.delete_department(dept)
|
||||
|
||||
def add_member(self, dept: Department, employee: Employee) -> None:
|
||||
"""Добавляет сотрудника в OU отдела и в одноименную группу отдела."""
|
||||
return self._department_service.add_member(dept, employee)
|
||||
# ======================== GROUPS ======================== #
|
||||
|
||||
def remove_member(self, dept: Department, employee: Employee) -> None:
|
||||
"""Удаляет сотрудника из отдела: из группы → MOVE → удаление линка."""
|
||||
return self._department_service.remove_member(dept, employee)
|
||||
# --- Conn-based методы (используются внутри services) ---
|
||||
|
||||
def set_head(self, dept: Department, head: Optional[Employee]) -> Department:
|
||||
"""Назначает/снимает руководителя отдела: LDAP managedBy → DB.head."""
|
||||
return self._department_service.set_head(dept, head)
|
||||
|
||||
def set_member_role(
|
||||
self, dept: Department, employee: Employee, role: Optional[DepartmentRole]
|
||||
) -> None:
|
||||
"""Меняет роль участника с синхронизацией LDAP-групп Roles."""
|
||||
return self._department_service.set_member_role(dept, employee, role)
|
||||
|
||||
# =========================== POSITIONS =========================== #
|
||||
|
||||
def reconcile_position(self, pos: Position) -> str:
|
||||
"""Публичный метод — открыть соединение и привести POS к консистентности."""
|
||||
return self._position_service.reconcile_position(pos)
|
||||
|
||||
def assign_position(self, employee: Employee, position: Position) -> None:
|
||||
"""
|
||||
Добавляет сотрудника в POS-группу должности и убеждается, что вложения POS в целевые группы актуальны.
|
||||
"""
|
||||
self._position_service.assign_position(employee, position)
|
||||
|
||||
def unassign_position(self, employee: Employee, position: Position) -> None:
|
||||
"""
|
||||
Удаляет сотрудника из POS-группы должности.
|
||||
"""
|
||||
self._position_service.unassign_position(employee, position)
|
||||
|
||||
def delete_position_group(self, position: Position) -> None:
|
||||
"""
|
||||
Best-effort: снять POS-группу из всех правовых групп и удалить её.
|
||||
"""
|
||||
self._position_service.delete_position_group(position)
|
||||
|
||||
# ======================== GROUPS (DRY-версия) ======================== #
|
||||
|
||||
def create_group(
|
||||
self,
|
||||
conn: Connection,
|
||||
cn: str,
|
||||
parent_dn: Optional[str] = None,
|
||||
*,
|
||||
description: Optional[str] = None,
|
||||
scope: str = "global",
|
||||
security_enabled: bool = True,
|
||||
) -> str:
|
||||
"""Создаёт группу в LDAP под указанным контейнером."""
|
||||
return self._group_service.create(
|
||||
conn,
|
||||
cn,
|
||||
parent_dn,
|
||||
description=description,
|
||||
scope=scope,
|
||||
security_enabled=security_enabled,
|
||||
)
|
||||
|
||||
def delete_group(self, conn: Connection, group_dn: str) -> None:
|
||||
"""Удаляет группу по DN (игнорирует noSuchObject)."""
|
||||
self._group_service.delete(conn, group_dn)
|
||||
|
||||
def rename_group(self, conn: Connection, group_dn: str, new_cn: str) -> str:
|
||||
"""Переименовывает группу (modify_dn)."""
|
||||
return self._group_service.rename(conn, group_dn, new_cn)
|
||||
|
||||
def set_group_description(
|
||||
self, conn: Connection, group_dn: str, description: Optional[str]
|
||||
) -> None:
|
||||
"""Ставит/очищает description."""
|
||||
self._group_service.set_description(conn, group_dn, description)
|
||||
|
||||
def sync_groups_catalog(
|
||||
self, *, throttle_seconds: int = 60, delete_absent: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
Тянет группы из AD под LDAP_GROUPS_BASE и гарантирует их наличие в Django Group.
|
||||
Возвращает количество созданных групп. Обновляет LdapSyncState(model='group').
|
||||
Троттлит запросы к AD не чаще, чем раз в throttle_seconds.
|
||||
"""
|
||||
return self._group_service.sync_catalog(
|
||||
throttle_seconds=throttle_seconds, delete_absent=delete_absent
|
||||
)
|
||||
|
||||
def _modify_group_members(
|
||||
self, conn: Connection, group_dn: str, op: int, members: List[str]
|
||||
) -> None:
|
||||
"""Применяет операцию к членству группы (DRY-хелпер)."""
|
||||
self._group_service._modify_members(conn, group_dn, op, members)
|
||||
def find_group_dn(
|
||||
self, conn, cn: str, bases: Optional[List[str]] = None
|
||||
) -> Optional[str]:
|
||||
"""Ищет DN группы по CN в заданных базах."""
|
||||
return self._group_service.find_dn(conn, cn, bases)
|
||||
|
||||
def add_group_members(
|
||||
self, conn: Connection, group_dn: str, member_dns: List[str]
|
||||
self, conn, group_dn: str, member_dns: List[str]
|
||||
) -> None:
|
||||
"""Добавляет участников (member ADD)."""
|
||||
self._group_service.add_members(conn, group_dn, member_dns)
|
||||
|
||||
def remove_group_members(
|
||||
self, conn: Connection, group_dn: str, member_dns: List[str]
|
||||
self, conn, group_dn: str, member_dns: List[str]
|
||||
) -> None:
|
||||
"""Удаляет участников (member DELETE)."""
|
||||
self._group_service.remove_members(conn, group_dn, member_dns)
|
||||
|
||||
def replace_group_members(
|
||||
self, conn: Connection, group_dn: str, exact_member_dns: List[str]
|
||||
) -> None:
|
||||
"""Полная замена состава группы."""
|
||||
self._group_service.replace_members(conn, group_dn, exact_member_dns)
|
||||
|
||||
def list_group_members(self, conn: Connection, group_dn: str) -> List[str]:
|
||||
"""Возвращает DN всех участников группы."""
|
||||
return self._group_service.list_members(conn, group_dn)
|
||||
|
||||
def find_group_dn(
|
||||
self, conn: Connection, cn: str, bases: Optional[List[str]] = None
|
||||
) -> Optional[str]:
|
||||
"""Ищет DN группы по CN в заданных базах."""
|
||||
return self._group_service.find_dn(conn, cn, bases)
|
||||
|
||||
# ============ LDAP HELPER METHODS (DEPARTMENT) ============ #
|
||||
|
||||
def _ensure_department_ou(self, conn: Connection, name: str) -> str:
|
||||
"""Гарантирует наличие OU отдела + OU=Roles."""
|
||||
return self._department_service._ensure_department_ou(conn, name)
|
||||
|
||||
def _rename_department_ou(
|
||||
self, conn: Connection, dept_dn: str, new_name: str
|
||||
) -> str:
|
||||
"""Переименовывает OU отдела и возвращает новый DN."""
|
||||
return self._department_service._rename_department_ou(conn, dept_dn, new_name)
|
||||
|
||||
def _set_ou_managed_by(
|
||||
self, conn: Connection, dept_dn: str, head_dn: Optional[str]
|
||||
) -> None:
|
||||
"""Устанавливает/очищает managedBy у OU в AD (идемпотентно и с no-op при пустом состоянии).
|
||||
|
||||
Args:
|
||||
conn (Connection): LDAP соединение.
|
||||
dept_dn (str): DN OU отдела.
|
||||
head_dn (Optional[str]): DN руководителя или None.
|
||||
"""
|
||||
return self._department_service._set_ou_managed_by(conn, dept_dn, head_dn)
|
||||
|
||||
def _set_ou_description(
|
||||
self, conn: Connection, dept_dn: str, description: Optional[str]
|
||||
) -> None:
|
||||
"""Ставит/очищает description у OU."""
|
||||
return self._department_service._set_ou_description(conn, dept_dn, description)
|
||||
|
||||
def _delete_department_ou(self, conn: Connection, dept_dn: str) -> None:
|
||||
"""Удаляет OU (ignore noSuchObject)."""
|
||||
return self._department_service._delete_department_ou(conn, dept_dn)
|
||||
|
||||
def _evict_all_users_from_department_ou(
|
||||
self, conn: Connection, dept_dn: str
|
||||
) -> None:
|
||||
"""Перемещает всех пользователей из OU отдела в Users base."""
|
||||
return self._department_service._evict_all_users_from_department_ou(
|
||||
conn, dept_dn
|
||||
)
|
||||
|
||||
def _groups_with_member(self, conn: Connection, member_dn: str) -> Set[str]:
|
||||
"""Возвращает множество DN групп, где member=member_dn."""
|
||||
return self._group_service.groups_with_member(conn, member_dn)
|
||||
# --- Высокоуровневые методы (используются в сигналах) ---
|
||||
|
||||
def group_find_dn(
|
||||
self, cn: str, bases: Optional[list[str]] = None
|
||||
@ -341,7 +187,7 @@ class DirectoryService:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
return self.create_group(
|
||||
return self._group_service.create(
|
||||
conn,
|
||||
cn,
|
||||
parent_dn,
|
||||
@ -354,61 +200,40 @@ class DirectoryService:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
self.delete_group(conn, group_dn)
|
||||
self._group_service.delete(conn, group_dn)
|
||||
|
||||
def group_rename(self, group_dn: str, new_cn: str) -> str:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
return self.rename_group(conn, group_dn, new_cn)
|
||||
return self._group_service.rename(conn, group_dn, new_cn)
|
||||
|
||||
def group_set_description(self, group_dn: str, description: Optional[str]) -> None:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
self.set_group_description(conn, group_dn, description)
|
||||
self._group_service.set_description(conn, group_dn, description)
|
||||
|
||||
def group_list_members(self, group_dn: str) -> list[str]:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
return self.list_group_members(conn, group_dn)
|
||||
return self._group_service.list_members(conn, group_dn)
|
||||
|
||||
def group_add_members(self, group_dn: str, member_dns: list[str]) -> None:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
self.add_group_members(conn, group_dn, member_dns)
|
||||
self._group_service.add_members(conn, group_dn, member_dns)
|
||||
|
||||
def group_remove_members(self, group_dn: str, member_dns: list[str]) -> None:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
self.remove_group_members(conn, group_dn, member_dns)
|
||||
self._group_service.remove_members(conn, group_dn, member_dns)
|
||||
|
||||
def group_replace_members(self, group_dn: str, exact_member_dns: list[str]) -> None:
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
with _ldap() as conn:
|
||||
self.replace_group_members(conn, group_dn, exact_member_dns)
|
||||
|
||||
def employee_ids_to_dns(self, ids: list[int]) -> list[str]:
|
||||
"""Конвертирует ID сотрудников в их Distinguished Names.
|
||||
|
||||
NOTE: Метод делегирует к UserService.
|
||||
"""
|
||||
return self._user_service.employee_ids_to_dns(ids)
|
||||
|
||||
def dns_to_employee_ids(self, dns: list[str]) -> list[int]:
|
||||
"""Конвертирует Distinguished Names в ID сотрудников.
|
||||
|
||||
NOTE: Метод делегирует к UserService.
|
||||
"""
|
||||
return self._user_service.dns_to_employee_ids(dns)
|
||||
|
||||
def employees_brief_by_dns(self, dns: list[str]) -> list[dict]:
|
||||
"""Получает краткую информацию о сотрудниках по их DN.
|
||||
|
||||
NOTE: Метод делегирует к UserService.
|
||||
"""
|
||||
return self._user_service.employees_brief_by_dns(dns)
|
||||
self._group_service.replace_members(conn, group_dn, exact_member_dns)
|
||||
|
||||
@ -1,424 +0,0 @@
|
||||
"""ORM сервисы для работы с LDAP через django-ldapdb.
|
||||
|
||||
Упрощенная альтернатива низкоуровневым ldap3 операциям.
|
||||
Использует LDAP модели (LdapUser, LdapGroup, LdapOrganizationalUnit) для CRUD.
|
||||
|
||||
Преимущества:
|
||||
- Чистый Django ORM вместо ldap3 костылей
|
||||
- Автоматическая валидация
|
||||
- Унифицированный API
|
||||
- Меньше кода
|
||||
|
||||
Использование:
|
||||
- Только для WRITE операций (POST/PUT/DELETE)
|
||||
- Чтение идет из обычных Django моделей (Employee, Department)
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# User Account Control values для Active Directory
|
||||
UAC_ENABLED = 512
|
||||
UAC_DISABLED = 514
|
||||
|
||||
|
||||
class LdapOrmUserService:
|
||||
"""Сервис для управления пользователями в LDAP через ORM."""
|
||||
|
||||
def create_user(
|
||||
self,
|
||||
*,
|
||||
sam_account_name: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
employee_pk: Optional[int] = None,
|
||||
phone: Optional[str] = None,
|
||||
is_active: bool = True,
|
||||
ou_dn: Optional[str] = None,
|
||||
) -> LdapUser:
|
||||
"""
|
||||
Создает пользователя в LDAP используя django-ldapdb ORM.
|
||||
|
||||
Args:
|
||||
sam_account_name: sAMAccountName (логин без домена)
|
||||
first_name: Имя
|
||||
last_name: Фамилия
|
||||
email: Email адрес
|
||||
employee_pk: ID сотрудника в Django (для employeeNumber)
|
||||
phone: Телефон (опционально)
|
||||
is_active: Активен ли пользователь
|
||||
ou_dn: DN organizational unit для размещения
|
||||
|
||||
Returns:
|
||||
Созданный LdapUser объект
|
||||
"""
|
||||
# Генерация CN и UPN
|
||||
cn = f"{first_name} {last_name}".strip() or sam_account_name
|
||||
cn_safe = esc_rdn(cn)
|
||||
|
||||
upn_suffix = getattr(settings, 'LDAP_UPN_SUFFIX', 'robotail.local')
|
||||
upn = f"{sam_account_name}@{upn_suffix}"
|
||||
|
||||
# Определение OU для размещения
|
||||
if ou_dn is None:
|
||||
ou_dn = getattr(settings, 'LDAP_USERS_BASE',
|
||||
getattr(settings, 'LDAP_USER_BASE',
|
||||
'OU=company,DC=robotail,DC=local'))
|
||||
|
||||
# DN нового пользователя
|
||||
dn = f"CN={cn_safe},{ou_dn}"
|
||||
|
||||
# Создание через ORM
|
||||
user = LdapUser()
|
||||
user.dn = dn
|
||||
user.cn = cn
|
||||
user.sam_account_name = sam_account_name
|
||||
user.user_principal_name = upn
|
||||
user.given_name = first_name
|
||||
user.sn = last_name or "." # AD требует sn
|
||||
user.display_name = cn
|
||||
user.mail = email
|
||||
|
||||
if phone:
|
||||
user.mobile = phone
|
||||
|
||||
if employee_pk:
|
||||
user.employee_number = str(employee_pk)
|
||||
|
||||
# UserAccountControl
|
||||
user.user_account_control = UAC_ENABLED if is_active else UAC_DISABLED
|
||||
|
||||
# Сохранение в 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
|
||||
|
||||
def update_user(
|
||||
self,
|
||||
user_dn: str,
|
||||
*,
|
||||
first_name: Optional[str] = None,
|
||||
last_name: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
phone: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
) -> LdapUser:
|
||||
"""
|
||||
Обновляет пользователя в LDAP через ORM.
|
||||
|
||||
Args:
|
||||
user_dn: DN пользователя для обновления
|
||||
first_name: Новое имя (если нужно изменить)
|
||||
last_name: Новая фамилия
|
||||
email: Новый email
|
||||
phone: Новый телефон
|
||||
is_active: Новый статус активности
|
||||
|
||||
Returns:
|
||||
Обновленный LdapUser объект
|
||||
"""
|
||||
# Получаем пользователя по DN
|
||||
try:
|
||||
user = LdapUser.objects.get(dn=user_dn)
|
||||
except LdapUser.DoesNotExist:
|
||||
raise ValueError(f"LDAP user not found: {user_dn}")
|
||||
|
||||
# Обновляем поля
|
||||
updated = False
|
||||
|
||||
if first_name is not None and user.given_name != first_name:
|
||||
user.given_name = first_name
|
||||
updated = True
|
||||
|
||||
if last_name is not None and user.sn != last_name:
|
||||
user.sn = last_name or "."
|
||||
updated = True
|
||||
|
||||
if email is not None and user.mail != email:
|
||||
user.mail = email
|
||||
updated = True
|
||||
|
||||
if phone is not None and user.mobile != phone:
|
||||
user.mobile = phone
|
||||
updated = True
|
||||
|
||||
if is_active is not None:
|
||||
new_uac = UAC_ENABLED if is_active else UAC_DISABLED
|
||||
if user.user_account_control != new_uac:
|
||||
user.user_account_control = new_uac
|
||||
updated = True
|
||||
|
||||
# Обновление displayName если изменились имя/фамилия
|
||||
if first_name is not None or last_name is not None:
|
||||
new_display = f"{user.given_name} {user.sn}".strip()
|
||||
if new_display and user.display_name != new_display:
|
||||
user.display_name = new_display
|
||||
updated = True
|
||||
|
||||
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
|
||||
|
||||
def delete_user(self, user_dn: str, *, soft: bool = True) -> None:
|
||||
"""
|
||||
Удаляет пользователя из LDAP.
|
||||
|
||||
Args:
|
||||
user_dn: DN пользователя для удаления
|
||||
soft: Если True, только деактивирует (userAccountControl=DISABLED)
|
||||
Если False, полностью удаляет из LDAP
|
||||
"""
|
||||
try:
|
||||
user = LdapUser.objects.get(dn=user_dn)
|
||||
except LdapUser.DoesNotExist:
|
||||
logger.warning(f"LDAP user not found for deletion: {user_dn}")
|
||||
return
|
||||
|
||||
if soft:
|
||||
# Soft delete - деактивация
|
||||
user.user_account_control = UAC_DISABLED
|
||||
user.save()
|
||||
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]:
|
||||
"""Получает пользователя из LDAP по DN."""
|
||||
try:
|
||||
return LdapUser.objects.get(dn=dn)
|
||||
except LdapUser.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user_by_employee_pk(self, employee_pk: int) -> Optional[LdapUser]:
|
||||
"""
|
||||
Получает пользователя из 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:
|
||||
"""Сервис для управления группами в LDAP через ORM."""
|
||||
|
||||
def create_group(
|
||||
self,
|
||||
cn: str,
|
||||
description: Optional[str] = None,
|
||||
ou_dn: Optional[str] = None,
|
||||
) -> LdapGroup:
|
||||
"""
|
||||
Создает группу в LDAP.
|
||||
|
||||
Args:
|
||||
cn: Common Name группы
|
||||
description: Описание группы
|
||||
ou_dn: DN organizational unit для размещения
|
||||
|
||||
Returns:
|
||||
Созданный LdapGroup объект
|
||||
"""
|
||||
if ou_dn is None:
|
||||
ou_dn = getattr(settings, 'LDAP_BASE_DN', 'DC=robotail,DC=local')
|
||||
|
||||
cn_safe = esc_rdn(cn)
|
||||
dn = f"CN={cn_safe},{ou_dn}"
|
||||
|
||||
group = LdapGroup()
|
||||
group.dn = dn
|
||||
group.cn = cn
|
||||
group.sam_account_name = cn
|
||||
|
||||
if description:
|
||||
group.description = description
|
||||
|
||||
group.save()
|
||||
|
||||
logger.info(f"Created LDAP group: {dn}")
|
||||
|
||||
return group
|
||||
|
||||
def add_member(self, group_dn: str, member_dn: str) -> None:
|
||||
"""Добавляет члена в группу."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
raise ValueError(f"LDAP group not found: {group_dn}")
|
||||
|
||||
# Добавляем DN члена в список member
|
||||
if member_dn not in (group.member or []):
|
||||
members = list(group.member or [])
|
||||
members.append(member_dn)
|
||||
group.member = members
|
||||
group.save()
|
||||
logger.info(f"Added {member_dn} to group {group_dn}")
|
||||
|
||||
def remove_member(self, group_dn: str, member_dn: str) -> None:
|
||||
"""Удаляет члена из группы."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
raise ValueError(f"LDAP group not found: {group_dn}")
|
||||
|
||||
# Удаляем DN члена из списка member
|
||||
if member_dn in (group.member or []):
|
||||
members = list(group.member or [])
|
||||
members.remove(member_dn)
|
||||
group.member = members
|
||||
group.save()
|
||||
logger.info(f"Removed {member_dn} from group {group_dn}")
|
||||
|
||||
def delete_group(self, group_dn: str) -> None:
|
||||
"""Удаляет группу из LDAP."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
group.delete()
|
||||
logger.info(f"Deleted LDAP group: {group_dn}")
|
||||
except LdapGroup.DoesNotExist:
|
||||
logger.warning(f"LDAP group not found for deletion: {group_dn}")
|
||||
|
||||
|
||||
class LdapOrmDepartmentService:
|
||||
"""Сервис для управления Organizational Units (отделами) в LDAP через ORM."""
|
||||
|
||||
def create_ou(
|
||||
self,
|
||||
ou_name: str,
|
||||
description: Optional[str] = None,
|
||||
parent_dn: Optional[str] = None,
|
||||
) -> LdapOrganizationalUnit:
|
||||
"""
|
||||
Создает Organizational Unit в LDAP.
|
||||
|
||||
Args:
|
||||
ou_name: Название OU
|
||||
description: Описание
|
||||
parent_dn: DN родительского контейнера
|
||||
|
||||
Returns:
|
||||
Созданный LdapOrganizationalUnit объект
|
||||
"""
|
||||
if parent_dn is None:
|
||||
parent_dn = getattr(settings, 'LDAP_BASE_DN', 'DC=robotail,DC=local')
|
||||
|
||||
ou_safe = esc_rdn(ou_name)
|
||||
dn = f"OU={ou_safe},{parent_dn}"
|
||||
|
||||
ou = LdapOrganizationalUnit()
|
||||
ou.dn = dn
|
||||
ou.ou = ou_name
|
||||
|
||||
if description:
|
||||
ou.description = description
|
||||
|
||||
ou.save()
|
||||
|
||||
logger.info(f"Created LDAP OU: {dn}")
|
||||
|
||||
return ou
|
||||
|
||||
def delete_ou(self, ou_dn: str) -> None:
|
||||
"""Удаляет OU из LDAP (должна быть пустой)."""
|
||||
try:
|
||||
ou = LdapOrganizationalUnit.objects.get(dn=ou_dn)
|
||||
ou.delete()
|
||||
logger.info(f"Deleted LDAP OU: {ou_dn}")
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
logger.warning(f"LDAP OU not found for deletion: {ou_dn}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
'LdapOrmUserService',
|
||||
'LdapOrmGroupService',
|
||||
'LdapOrmDepartmentService',
|
||||
]
|
||||
@ -34,11 +34,8 @@ from .dn_utils import (
|
||||
# Утилиты изображений
|
||||
from .image_utils import normalize_avatar_to_jpeg
|
||||
|
||||
# Утилиты групп
|
||||
from .group_utils import (
|
||||
_desired_group_cns_for_employee,
|
||||
sync_user_groups_by_cns,
|
||||
)
|
||||
# Утилиты групп (ORM)
|
||||
from .group_utils_orm import sync_user_groups_by_cns_orm
|
||||
|
||||
__all__ = [
|
||||
# Текстовые
|
||||
@ -66,7 +63,6 @@ __all__ = [
|
||||
"_move_to_department",
|
||||
# Изображения
|
||||
"normalize_avatar_to_jpeg",
|
||||
# Группы
|
||||
"_desired_group_cns_for_employee",
|
||||
"sync_user_groups_by_cns",
|
||||
# Группы (ORM)
|
||||
"sync_user_groups_by_cns_orm",
|
||||
]
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from ldap3 import BASE, SUBTREE, Connection
|
||||
|
||||
from employees.models import Employee
|
||||
from .text_utils import esc_filter
|
||||
|
||||
|
||||
def _read_user_memberof_dns(conn: Connection, user_dn: str) -> set[str]:
|
||||
"""Возвращает текущее множество DN групп (memberOf) пользователя."""
|
||||
conn.search(user_dn, "(objectClass=*)", BASE, attributes=["memberOf"])
|
||||
if not conn.entries:
|
||||
return set()
|
||||
raw = getattr(conn.entries[0], "memberOf", [])
|
||||
vals = raw.values if hasattr(raw, "values") else raw
|
||||
return {str(x) for x in (vals or [])}
|
||||
|
||||
|
||||
def _group_add_member(
|
||||
conn: Connection, group_dn: str, user_dn: str, *, do_write: bool
|
||||
) -> None:
|
||||
"""Добавляет пользователя в группу (или пропускает в dry-run)."""
|
||||
from ldap3 import MODIFY_ADD
|
||||
|
||||
if not do_write:
|
||||
return
|
||||
ok = conn.modify(group_dn, {"member": [(MODIFY_ADD, [user_dn])]})
|
||||
if not ok and conn.result.get("description") not in {"typeOrValueExists"}:
|
||||
raise RuntimeError(f"Add member failed: {group_dn}: {conn.result}")
|
||||
|
||||
|
||||
def _group_remove_member(
|
||||
conn: Connection, group_dn: str, user_dn: str, *, do_write: bool
|
||||
) -> None:
|
||||
"""Удаляет пользователя из группы (или пропускает в dry-run)."""
|
||||
from ldap3 import MODIFY_DELETE
|
||||
|
||||
if not do_write:
|
||||
return
|
||||
ok = conn.modify(group_dn, {"member": [(MODIFY_DELETE, [user_dn])]})
|
||||
if not ok and conn.result.get("description") not in {"noSuchAttribute"}:
|
||||
raise RuntimeError(f"Remove member failed: {group_dn}: {conn.result}")
|
||||
|
||||
|
||||
def _resolve_group_dns_by_cn(
|
||||
conn: Connection, cns: set[str], extra_bases: Optional[list[str]] = None
|
||||
) -> dict[str, str]:
|
||||
"""Ищет DN групп по их CN в OU=Groups и дополнительных базах (например, Roles отдела)."""
|
||||
if not cns:
|
||||
return {}
|
||||
bases = [getattr(settings, "LDAP_GROUPS_BASE", "")]
|
||||
if extra_bases:
|
||||
bases.extend(extra_bases)
|
||||
found: dict[str, str] = {}
|
||||
for base in bases:
|
||||
if not base:
|
||||
continue
|
||||
for cn in sorted(cns - set(found.keys())):
|
||||
conn.search(
|
||||
base,
|
||||
f"(&(objectClass=group)(cn={esc_filter(cn)}))",
|
||||
SUBTREE,
|
||||
attributes=["distinguishedName", "cn"],
|
||||
)
|
||||
if conn.entries:
|
||||
found[cn] = str(conn.entries[0].entry_dn)
|
||||
return found
|
||||
|
||||
|
||||
def _desired_group_cns_for_employee(emp: "Employee") -> set[str]:
|
||||
"""Возвращает целевые CN групп для сотрудника из Django (Position/DeptRole/Direct)."""
|
||||
cns: set[str] = set()
|
||||
pos = getattr(emp, "position", None)
|
||||
if pos is not None and hasattr(pos, "groups"):
|
||||
cns |= {g.name for g in pos.groups.all()}
|
||||
user_obj = getattr(emp, "user", None) or emp
|
||||
if hasattr(user_obj, "groups"):
|
||||
cns |= {g.name for g in user_obj.groups.all()}
|
||||
dept_roles = getattr(emp, "department_roles", None)
|
||||
if dept_roles is not None and hasattr(dept_roles, "all"):
|
||||
for dr in dept_roles.all():
|
||||
if hasattr(dr, "groups"):
|
||||
cns |= {g.name for g in dr.groups.all()}
|
||||
return cns
|
||||
|
||||
|
||||
def sync_user_groups_by_cns(
|
||||
conn: Connection,
|
||||
user_dn: str,
|
||||
target_cns: Iterable[str],
|
||||
*,
|
||||
extra_bases: list[str] | None = None,
|
||||
do_write: bool = True,
|
||||
) -> tuple[int, int]:
|
||||
"""Приводит memberOf пользователя к точному набору CN (добавляет/удаляет).
|
||||
|
||||
Args:
|
||||
conn: соединение LDAP.
|
||||
user_dn: DN пользователя.
|
||||
target_cns: итоговый набор CN групп.
|
||||
extra_bases: доп. базы поиска групп (например, OU=Roles,<dept>).
|
||||
do_write: False => только расчёт.
|
||||
|
||||
Returns:
|
||||
(added, removed).
|
||||
|
||||
Raises:
|
||||
RuntimeError: если операции LDAP завершаются ошибкой.
|
||||
"""
|
||||
desired_dns = set(
|
||||
_resolve_group_dns_by_cn(
|
||||
conn, set(target_cns), extra_bases=extra_bases
|
||||
).values()
|
||||
)
|
||||
current_dns = _read_user_memberof_dns(conn, user_dn)
|
||||
|
||||
# ограничим удаление только «наших» веток (Groups, OU отделов с ROLE_*),
|
||||
# как и в батч-логике
|
||||
groups_base = getattr(settings, "LDAP_GROUPS_BASE", "")
|
||||
depts_base = getattr(settings, "LDAP_DEPARTMENTS_BASE", "")
|
||||
to_add = desired_dns - current_dns
|
||||
to_del = {
|
||||
dn
|
||||
for dn in (current_dns - desired_dns)
|
||||
if (groups_base and dn.endswith(groups_base))
|
||||
or (depts_base and dn.endswith(depts_base) and "CN=ROLE_" in dn)
|
||||
}
|
||||
|
||||
added = removed = 0
|
||||
if do_write:
|
||||
for gdn in sorted(to_add):
|
||||
_group_add_member(conn, gdn, user_dn, do_write=True)
|
||||
added += 1
|
||||
for gdn in sorted(to_del):
|
||||
_group_remove_member(conn, gdn, user_dn, do_write=True)
|
||||
removed += 1
|
||||
else:
|
||||
added, removed = len(to_add), len(to_del)
|
||||
return added, removed
|
||||
@ -111,9 +111,21 @@ def sync_user_groups_by_cns_orm(
|
||||
return added, removed
|
||||
|
||||
|
||||
# _desired_group_cns_for_employee — чистая Django логика, нет ldap3
|
||||
# Реэкспортируем из оригинала
|
||||
from .group_utils import _desired_group_cns_for_employee
|
||||
def _desired_group_cns_for_employee(emp: "Employee") -> set[str]:
|
||||
"""Возвращает целевые CN групп для сотрудника из Django (Position/DeptRole/Direct)."""
|
||||
cns: set[str] = set()
|
||||
pos = getattr(emp, "position", None)
|
||||
if pos is not None and hasattr(pos, "groups"):
|
||||
cns |= {g.name for g in pos.groups.all()}
|
||||
user_obj = getattr(emp, "user", None) or emp
|
||||
if hasattr(user_obj, "groups"):
|
||||
cns |= {g.name for g in user_obj.groups.all()}
|
||||
dept_roles = getattr(emp, "department_roles", None)
|
||||
if dept_roles is not None and hasattr(dept_roles, "all"):
|
||||
for dr in dept_roles.all():
|
||||
if hasattr(dr, "groups"):
|
||||
cns |= {g.name for g in dr.groups.all()}
|
||||
return cns
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user