refactor(ldap): углубить замену ldap3 → ORM
- user_service: _enable_user через LdapUser ORM (убран ldap_modify_or_ignore) - user_service: _unique_logins через LdapUser.objects.filter (убран is_taken) - user_service: удалены импорты MODIFY_REPLACE, is_taken, ldap_modify_or_ignore - group_service: set_description, list_members, add/remove/replace_members, find_dn, groups_with_member — все через LdapGroup ORM - group_service: удалены ldap_modify_or_ignore, MODIFY_*, BASE, esc_filter - department_service: _set_ou_managed_by через LdapOrganizationalUnit ORM (заменены ~60 строк ADD/REPLACE/DELETE логики на 4 строки ORM) - department_service: удалены MODIFY_ADD/DELETE/REPLACE - position_service: delete_position_group и _reconcile_position_nesting через groups_with_member (ORM) вместо conn.search Итого: -94 строки, убраны 3 импорта ldap_repository из сервисов.
This commit is contained in:
parent
95ff0dc0af
commit
ea0a7e1f29
@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from ldap3 import BASE, MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, Connection
|
||||
from ldap3 import BASE, Connection
|
||||
|
||||
from ...models import Department, DepartmentRole, Employee, EmployeeDepartment, LdapSyncState, RoleAssignment
|
||||
from ..domain.dtos import DirectoryDepartmentDTO
|
||||
@ -1066,70 +1066,29 @@ class DepartmentService:
|
||||
def _set_ou_managed_by(
|
||||
self, conn: Connection, dept_dn: str, head_dn: Optional[str]
|
||||
) -> None:
|
||||
"""Устанавливает/очищает managedBy у OU в AD (идемпотентно).
|
||||
|
||||
TODO(ldap-orm): Можно заменить на ORM (ou.managed_by = head_dn; save()),
|
||||
но нужна проверка существования head_dn и обработка ADD/REPLACE/DELETE.
|
||||
"""Устанавливает/очищает managedBy у OU в AD через ORM.
|
||||
|
||||
Args:
|
||||
conn: LDAP соединение.
|
||||
conn: LDAP соединение (не используется, ORM).
|
||||
dept_dn: DN OU отдела.
|
||||
head_dn: DN руководителя или None.
|
||||
|
||||
Raises:
|
||||
DirectoryServiceError: Если OU или head DN не найдены.
|
||||
RuntimeError: Если операция modify не удалась.
|
||||
"""
|
||||
if not dept_dn:
|
||||
raise DirectoryServiceError("dept_dn пуст")
|
||||
|
||||
if not conn.search(
|
||||
search_base=dept_dn,
|
||||
search_filter="(objectClass=organizationalUnit)",
|
||||
search_scope=BASE,
|
||||
attributes=["managedBy", "allowedAttributes"],
|
||||
):
|
||||
try:
|
||||
ou = LdapOrganizationalUnit.objects.get(dn=dept_dn)
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
raise DirectoryServiceError(f"OU not found: {dept_dn}")
|
||||
|
||||
ou_entry = conn.entries[0]
|
||||
current_vals = list(
|
||||
map(str, getattr(ou_entry, "managedBy", []) or [])
|
||||
)
|
||||
current = (ou.managed_by or "").strip().lower()
|
||||
target = (head_dn or "").strip().lower()
|
||||
|
||||
if head_dn:
|
||||
if not conn.search(
|
||||
search_base=head_dn,
|
||||
search_filter="(objectClass=*)",
|
||||
search_scope=BASE,
|
||||
attributes=["distinguishedName"],
|
||||
):
|
||||
raise DirectoryServiceError(f"Head DN not found: {head_dn}")
|
||||
|
||||
def _norm(x: Optional[str]) -> str:
|
||||
return (x or "").strip().lower()
|
||||
|
||||
target = _norm(head_dn)
|
||||
current = _norm(current_vals[0] if current_vals else None)
|
||||
if target == current:
|
||||
if current == target:
|
||||
return
|
||||
|
||||
if head_dn is None:
|
||||
if not current_vals:
|
||||
return
|
||||
op, values = MODIFY_DELETE, []
|
||||
else:
|
||||
op, values = (
|
||||
(MODIFY_ADD, [head_dn])
|
||||
if not current_vals
|
||||
else (MODIFY_REPLACE, [head_dn])
|
||||
)
|
||||
|
||||
ok = conn.modify(dept_dn, {"managedBy": [(op, values)]})
|
||||
if not ok:
|
||||
res = dict(conn.result)
|
||||
if res.get("result") == 16 and op == MODIFY_DELETE:
|
||||
return
|
||||
raise RuntimeError(f"LDAP modify managedBy failed: {res}")
|
||||
ou.managed_by = head_dn or ""
|
||||
ou.save()
|
||||
|
||||
def _set_ou_description(
|
||||
self, conn: Connection, dept_dn: str, description: Optional[str]
|
||||
|
||||
@ -14,12 +14,11 @@ from django.contrib.auth.models import Group
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from employees.models import LdapSyncState
|
||||
from ldap3 import BASE, MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, SUBTREE, Connection
|
||||
from ldap3 import SUBTREE, Connection
|
||||
|
||||
from ..infrastructure.connections import _ldap
|
||||
from ..orm_models import LdapGroup
|
||||
from ..repositories.ldap_repository import ldap_modify_or_ignore
|
||||
from ..utils.text_utils import esc_filter, esc_rdn
|
||||
from ..utils.text_utils import esc_rdn
|
||||
from ..utils.ldap_utils import get_guid_str, group_type
|
||||
|
||||
|
||||
@ -145,119 +144,102 @@ class GroupService:
|
||||
def set_description(
|
||||
self, conn: Connection, group_dn: str, description: Optional[str]
|
||||
) -> None:
|
||||
"""Ставит/очищает description."""
|
||||
changes = (
|
||||
{"description": [(MODIFY_REPLACE, [description])]}
|
||||
if description
|
||||
else {"description": [(MODIFY_DELETE, [])]}
|
||||
)
|
||||
ldap_modify_or_ignore(conn, group_dn, changes, {"noSuchAttribute"})
|
||||
"""Ставит/очищает description (ORM)."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
group.description = description or ""
|
||||
group.save()
|
||||
except LdapGroup.DoesNotExist:
|
||||
pass
|
||||
|
||||
# ======================== Member Management ======================== #
|
||||
|
||||
def _modify_members(
|
||||
self, conn: Connection, group_dn: str, op: int, members: List[str]
|
||||
) -> None:
|
||||
"""Применяет операцию к членству группы (DRY-хелпер).
|
||||
|
||||
Args:
|
||||
conn: Подключение LDAP.
|
||||
group_dn: DN группы.
|
||||
op: MODIFY_ADD | MODIFY_DELETE | MODIFY_REPLACE.
|
||||
members: Список DN участников.
|
||||
|
||||
Raises:
|
||||
RuntimeError: При ошибке modify вне допустимых кейсов.
|
||||
"""
|
||||
if not members and op in (MODIFY_ADD, MODIFY_DELETE):
|
||||
return
|
||||
ignore = (
|
||||
{"typeOrValueExists", "entryAlreadyExists"}
|
||||
if op == MODIFY_ADD
|
||||
else (
|
||||
{"noSuchAttribute", "noSuchObject"}
|
||||
if op == MODIFY_DELETE
|
||||
else {"noSuchAttribute"}
|
||||
)
|
||||
)
|
||||
ldap_modify_or_ignore(conn, group_dn, {"member": [(op, members or [])]}, ignore)
|
||||
|
||||
def add_members(
|
||||
self, conn: Connection, group_dn: str, member_dns: List[str]
|
||||
) -> None:
|
||||
"""Добавляет участников (member ADD)."""
|
||||
self._modify_members(conn, group_dn, MODIFY_ADD, member_dns)
|
||||
"""Добавляет участников (ORM)."""
|
||||
if not member_dns:
|
||||
return
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
return
|
||||
members = list(group.member or [])
|
||||
changed = False
|
||||
for dn in member_dns:
|
||||
if dn not in members:
|
||||
members.append(dn)
|
||||
changed = True
|
||||
if changed:
|
||||
group.member = members
|
||||
group.save()
|
||||
|
||||
def remove_members(
|
||||
self, conn: Connection, group_dn: str, member_dns: List[str]
|
||||
) -> None:
|
||||
"""Удаляет участников (member DELETE)."""
|
||||
self._modify_members(conn, group_dn, MODIFY_DELETE, member_dns)
|
||||
"""Удаляет участников (ORM)."""
|
||||
if not member_dns:
|
||||
return
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
return
|
||||
members = list(group.member or [])
|
||||
changed = False
|
||||
for dn in member_dns:
|
||||
if dn in members:
|
||||
members.remove(dn)
|
||||
changed = True
|
||||
if changed:
|
||||
group.member = members
|
||||
group.save()
|
||||
|
||||
def replace_members(
|
||||
self, conn: Connection, group_dn: str, exact_member_dns: List[str]
|
||||
) -> None:
|
||||
"""Полная замена состава группы."""
|
||||
self._modify_members(conn, group_dn, MODIFY_REPLACE, exact_member_dns)
|
||||
"""Полная замена состава группы (ORM)."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
return
|
||||
group.member = exact_member_dns
|
||||
group.save()
|
||||
|
||||
def list_members(self, conn: Connection, group_dn: str) -> List[str]:
|
||||
"""Возвращает DN всех участников группы."""
|
||||
ok = conn.search(
|
||||
search_base=group_dn,
|
||||
search_filter="(objectClass=group)",
|
||||
search_scope=BASE,
|
||||
attributes=["member"],
|
||||
)
|
||||
if not ok or not conn.entries:
|
||||
"""Возвращает DN всех участников группы (ORM)."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
return list(group.member or [])
|
||||
except LdapGroup.DoesNotExist:
|
||||
return []
|
||||
entry = conn.entries[0]
|
||||
vals = getattr(getattr(entry, "member", None), "values", None)
|
||||
return list(vals) if vals else []
|
||||
|
||||
# ======================== Finding and Search ======================== #
|
||||
|
||||
def find_dn(
|
||||
self, conn: Connection, cn: str, bases: Optional[List[str]] = None
|
||||
) -> Optional[str]:
|
||||
"""Ищет DN группы по CN в заданных базах."""
|
||||
bases = list(bases or [])
|
||||
if not bases:
|
||||
gb = getattr(settings, "LDAP_GROUPS_BASE", None)
|
||||
if gb:
|
||||
bases.append(gb)
|
||||
for base in bases:
|
||||
ok = conn.search(
|
||||
search_base=base,
|
||||
search_filter=f"(cn={esc_filter(cn)})",
|
||||
search_scope=SUBTREE,
|
||||
attributes=["distinguishedName"],
|
||||
)
|
||||
if ok and conn.entries:
|
||||
return str(conn.entries[0].entry_dn)
|
||||
return None
|
||||
"""Ищет DN группы по CN (ORM)."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(cn=cn)
|
||||
return group.dn
|
||||
except LdapGroup.DoesNotExist:
|
||||
return None
|
||||
|
||||
def groups_with_member(self, conn: Connection, member_dn: str) -> Set[str]:
|
||||
"""Находит все группы, в которых состоит указанный DN.
|
||||
|
||||
Args:
|
||||
conn: Подключение LDAP.
|
||||
member_dn: DN участника.
|
||||
|
||||
Returns:
|
||||
Множество DN групп.
|
||||
"""
|
||||
base = getattr(settings, "LDAP_GROUPS_BASE", None) or getattr(
|
||||
settings, "LDAP_BASE_DN", None
|
||||
)
|
||||
if not base:
|
||||
"""Находит все группы, в которых состоит DN (ORM через memberOf)."""
|
||||
from ..orm_models import LdapUser
|
||||
# Пробуем получить memberOf пользователя (быстрее чем поиск по всем группам)
|
||||
try:
|
||||
user = LdapUser.objects.get(dn=member_dn)
|
||||
return set(user.member_of or [])
|
||||
except LdapUser.DoesNotExist:
|
||||
pass
|
||||
# Fallback: может быть группа вложена в группу
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=member_dn)
|
||||
return set(group.member_of or [])
|
||||
except LdapGroup.DoesNotExist:
|
||||
return set()
|
||||
|
||||
ok = conn.search(
|
||||
search_base=base,
|
||||
search_filter=f"(&(objectClass=group)(member={esc_filter(member_dn)}))",
|
||||
search_scope=SUBTREE,
|
||||
attributes=["distinguishedName"],
|
||||
)
|
||||
if not ok:
|
||||
return set()
|
||||
return {str(e.entry_dn) for e in conn.entries}
|
||||
|
||||
@ -123,34 +123,21 @@ class PositionService:
|
||||
position: Должность с POS-группой для удаления
|
||||
"""
|
||||
from ..infrastructure.connections import _ldap
|
||||
|
||||
from ..orm_models import LdapGroup
|
||||
|
||||
with _ldap() as conn:
|
||||
dn = (position.ldap_group_dn or "").strip()
|
||||
if not dn:
|
||||
return
|
||||
# снять вложения
|
||||
bases = [
|
||||
b
|
||||
for b in [
|
||||
getattr(settings, "LDAP_GROUPS_BASE", None),
|
||||
getattr(settings, "LDAP_BASE_DN", None),
|
||||
]
|
||||
if b
|
||||
]
|
||||
for base in bases:
|
||||
ok = conn.search(
|
||||
search_base=base,
|
||||
search_filter=f"(&(objectClass=group)(member={esc_filter(dn)}))",
|
||||
search_scope=SUBTREE,
|
||||
attributes=["distinguishedName"],
|
||||
)
|
||||
if ok and conn.entries:
|
||||
for e in conn.entries:
|
||||
self._group_service.remove_members(conn, str(e.entry_dn), [dn])
|
||||
# удалить саму POS-группу
|
||||
# снять вложения через ORM (groups_with_member)
|
||||
parent_groups = self._group_service.groups_with_member(conn, dn)
|
||||
for parent_dn in parent_groups:
|
||||
self._group_service.remove_members(conn, parent_dn, [dn])
|
||||
# удалить саму POS-группу (ORM)
|
||||
try:
|
||||
self._group_service.delete(conn, dn)
|
||||
except Exception:
|
||||
group = LdapGroup.objects.get(dn=dn)
|
||||
group.delete()
|
||||
except LdapGroup.DoesNotExist:
|
||||
pass
|
||||
|
||||
# ======================== PRIVATE HELPERS ======================== #
|
||||
@ -307,25 +294,8 @@ class PositionService:
|
||||
if dn:
|
||||
desired_dns.add(dn)
|
||||
|
||||
# текущие группы, где POS уже состоит
|
||||
current_dns: Set[str] = set()
|
||||
search_bases = [
|
||||
b
|
||||
for b in [
|
||||
getattr(settings, "LDAP_GROUPS_BASE", None),
|
||||
getattr(settings, "LDAP_BASE_DN", None),
|
||||
]
|
||||
if b
|
||||
]
|
||||
for base in search_bases:
|
||||
ok = conn.search(
|
||||
search_base=base,
|
||||
search_filter=f"(&(objectClass=group)(member={esc_filter(pos_dn)}))",
|
||||
search_scope=SUBTREE,
|
||||
attributes=["distinguishedName"],
|
||||
)
|
||||
if ok and conn.entries:
|
||||
current_dns.update(str(e.entry_dn) for e in conn.entries)
|
||||
# текущие группы, где POS уже состоит (ORM)
|
||||
current_dns = self._group_service.groups_with_member(conn, pos_dn)
|
||||
|
||||
# добавить недостающее
|
||||
for add_dn in desired_dns - current_dns:
|
||||
|
||||
@ -11,7 +11,7 @@ from typing import Any, Dict, Iterable, List, Optional
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from ldap3 import MODIFY_REPLACE, Connection
|
||||
from ldap3 import Connection
|
||||
|
||||
from ...models import Employee, LdapSyncState, Position
|
||||
from ..domain.dtos import DirectoryUserDTO
|
||||
@ -25,8 +25,6 @@ from ..utils.group_utils_orm import sync_user_groups_by_cns_orm
|
||||
from ..infrastructure.connections import _ldap
|
||||
from ..repositories.ldap_repository import (
|
||||
ensure_container_exists,
|
||||
is_taken,
|
||||
ldap_modify_or_ignore,
|
||||
)
|
||||
from ..utils.image_utils import normalize_avatar_to_jpeg
|
||||
from ..utils.ldap_utils import (
|
||||
@ -477,35 +475,32 @@ class UserService:
|
||||
raise RuntimeError(f"LDAP set password failed: {msg}")
|
||||
|
||||
def _enable_user(self, conn: Connection, dn: str) -> None:
|
||||
"""Включает учётку (UAC=512).
|
||||
|
||||
TODO(ldap-orm): Можно заменить на ORM (ldap_user.user_account_control = 512; save()).
|
||||
Но используется в связке с _create_user_in_ldap → conn уже в контексте.
|
||||
"""
|
||||
ldap_modify_or_ignore(
|
||||
conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [UAC_ENABLED])]}, set()
|
||||
)
|
||||
"""Включает учётку (UAC=512) через ORM."""
|
||||
ldap_user = LdapUser.objects.get(dn=dn)
|
||||
ldap_user.user_account_control = UAC_ENABLED
|
||||
ldap_user.save()
|
||||
|
||||
def _unique_logins(
|
||||
self, conn: Connection, dto: DirectoryUserDTO, upn_suffix: str
|
||||
) -> tuple[str, str]:
|
||||
"""Генерирует уникальные sAMAccountName и UPN.
|
||||
|
||||
TODO(ldap-orm): is_taken() использует ldap3 raw search с OR фильтром.
|
||||
Можно заменить на LdapUser.objects.filter(sam_account_name=s).exists(),
|
||||
но нужен фильтр по нескольким атрибутам одновременно.
|
||||
"""
|
||||
"""Генерирует уникальные sAMAccountName и UPN через ORM."""
|
||||
sam, upn = build_logins_for_user(
|
||||
first_name=dto.first_name,
|
||||
last_name=dto.last_name,
|
||||
email=dto.email,
|
||||
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}),
|
||||
is_taken_sam=lambda s: LdapUser.objects.filter(
|
||||
sam_account_name=s
|
||||
).exists(),
|
||||
is_taken_upn=lambda u: LdapUser.objects.filter(
|
||||
user_principal_name=u
|
||||
).exists(),
|
||||
guid=getattr(dto, "ldap_guid", None),
|
||||
)
|
||||
if not sam or not upn:
|
||||
raise ValueError("Не удалось сгенерировать уникальные sAMAccountName/UPN")
|
||||
raise ValueError(
|
||||
"Не удалось сгенерировать уникальные sAMAccountName/UPN"
|
||||
)
|
||||
return sam, upn
|
||||
|
||||
def _phone_write_attr(self) -> str:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user