refactor(ldap): fix architectural issues in views

DirectoryService:
- Added public methods: assign_role, revoke_role, create_role, update_role, delete_role
- Added public method: move_user_to_base (replaces private _user_service._move_user_to_base)
- Fixed method signatures to match DepartmentService API

roles.py:
- Replaced direct DepartmentService usage with DirectoryService facade (5 methods)
- Removed manual GroupService/UserService instantiation
- All CRUD operations now go through facade layer

actions.py:
- Replaced private API svc._user_service._move_user_to_base() with public svc.move_user_to_base()
- Simplified LDAP connection handling (2 occurrences)
- Removed manual _ldap() and ensure_container_exists calls

manage.py check: System check identified no issues
This commit is contained in:
igor_nadein 2026-02-16 16:38:01 +03:00
parent 19464a38cf
commit 1f0858b085
3 changed files with 156 additions and 90 deletions

View File

@ -128,10 +128,6 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
if not departments_processed:
try:
from employees.ldap.infrastructure.connections import \
_ldap
from employees.ldap.repositories.ldap_repository import \
ensure_container_exists
from employees.ldap.utils.ldap_utils import \
get_base_dn_for_employee
@ -146,21 +142,18 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
if not current_dn.lower().endswith(
target_base.lower()
):
with _ldap() as conn:
ensure_container_exists(
conn, target_base)
new_dn = (
svc._user_service._move_user_to_base(
conn, current_dn, target_base
)
)
sync_state.touch(
ldap_dn=new_dn, sync_dir="ldap"
try:
new_dn = svc.move_user_to_base(
emp, target_base
)
logger.info(
f"Dismissed employee without department moved to OU=Dismissed: "
f"employee_id={emp.id}, new_dn={new_dn}"
)
except Exception as move_err:
logger.error(
f"Failed to move dismissed employee: {move_err}"
)
except Exception as e:
logger.error(
f"Failed to move dismissed employee without department to OU=Dismissed: "
@ -209,26 +202,18 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
settings, "LDAP_USERS_BASE", None
) or getattr(settings, "LDAP_USER_BASE", None)
if users_base:
from employees.ldap.infrastructure.connections import \
_ldap
from employees.ldap.repositories.ldap_repository import \
ensure_container_exists
with _ldap() as conn:
ensure_container_exists(
conn, users_base)
new_dn = (
svc._user_service._move_user_to_base(
conn, current_dn, users_base
)
)
sync_state.touch(
ldap_dn=new_dn, sync_dir="ldap"
try:
new_dn = svc.move_user_to_base(
emp, users_base
)
logger.info(
f"Restored employee moved from Dismissed to Users: "
f"employee_id={emp.id}, new_dn={new_dn}"
)
except Exception as move_err:
logger.error(
f"Failed to move restored employee: {move_err}"
)
except Exception as e:
logger.error(
f"Failed to move restored employee from Dismissed to Users: "

View File

@ -89,28 +89,19 @@ class DepartmentRoleViewSet(viewsets.ModelViewSet):
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
svc = DirectoryService()
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(group_service, user_service)
scoped_permissions = None
permission_codes = None
if codes is not None:
scoped_permissions = list(
DepartmentPermission.objects.filter(code__in=codes)
)
permission_codes = codes
elif perms is not None:
scoped_permissions = list(perms)
permission_codes = [p.code for p in perms]
try:
role = dept_service.create_role(
department=dept,
role = svc.create_role(
dept=dept,
name=name,
scoped_permissions=scoped_permissions,
permission_codes=permission_codes,
)
return Response(
self.get_serializer(
@ -149,27 +140,12 @@ class DepartmentRoleViewSet(viewsets.ModelViewSet):
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
svc = DirectoryService()
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(group_service, user_service)
changes = {}
if "name" in ser.validated_data:
changes["name"] = ser.validated_data["name"]
if "scoped_permissions" in ser.validated_data:
changes["scoped_permissions"] = ser.validated_data["scoped_permissions"]
if "scoped_permission_codes" in ser.validated_data:
changes["scoped_permission_codes"] = ser.validated_data[
"scoped_permission_codes"
]
new_name = ser.validated_data.get("name")
try:
role = dept_service.update_role(instance, changes)
role = svc.update_role(instance, name=new_name)
return Response(self.get_serializer(role).data)
except DirectoryLdapError as e:
return Response(
@ -195,17 +171,10 @@ class DepartmentRoleViewSet(viewsets.ModelViewSet):
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(group_service, user_service)
svc = DirectoryService()
try:
dept_service.delete_role(instance)
svc.delete_role(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
except DirectoryLdapError as e:
return Response(
@ -312,16 +281,9 @@ class DepartmentRoleViewSet(viewsets.ModelViewSet):
if ldap_enabled:
try:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
svc = DirectoryService()
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(group_service, user_service)
assignment = dept_service.assign_role(
assignment = svc.assign_role(
employee, role, assigned_by)
return Response(
@ -384,16 +346,9 @@ class DepartmentRoleViewSet(viewsets.ModelViewSet):
if ldap_enabled:
try:
from employees.ldap.services.department_service import \
DepartmentService
from employees.ldap.services.group_service import GroupService
from employees.ldap.services.user_service import UserService
svc = DirectoryService()
group_service = GroupService()
user_service = UserService(group_service)
dept_service = DepartmentService(group_service, user_service)
dept_service.revoke_role(employee, role)
svc.revoke_role(employee, role)
return Response(status=204)
except DirectoryLdapError as e:

View File

@ -169,6 +169,132 @@ class DirectoryService:
"""Меняет роль участника с синхронизацией LDAP-групп Roles."""
return self._department_service.set_member_role(dept, employee, role)
def assign_role(
self,
employee: Employee,
role: DepartmentRole,
assigned_by: Optional[Employee] = None,
):
"""Назначает роль сотруднику (не требует членства в отделе).
NOTE: Метод делегирует к DepartmentService.
Args:
employee: Сотрудник.
role: Роль для назначения.
assigned_by: Кто назначил (опционально).
Returns:
RoleAssignment: Созданное/обновлённое назначение.
"""
return self._department_service.assign_role(employee, role, assigned_by)
def revoke_role(self, employee: Employee, role: DepartmentRole) -> None:
"""Отзывает роль у сотрудника.
NOTE: Метод делегирует к DepartmentService.
Args:
employee: Сотрудник.
role: Роль для отзыва.
"""
return self._department_service.revoke_role(employee, role)
def create_role(
self,
dept: Department,
name: str,
permission_codes: Optional[List[str]] = None,
) -> DepartmentRole:
"""Создаёт роль отдела с LDAP-группой.
NOTE: Метод делегирует к DepartmentService.
Args:
dept: Отдел.
name: Название роли.
permission_codes: Коды прав (опционально).
Returns:
DepartmentRole: Созданная роль.
"""
from employees.models import DepartmentPermission
scoped_permissions = None
if permission_codes:
scoped_permissions = list(
DepartmentPermission.objects.filter(code__in=permission_codes)
)
return self._department_service.create_role(
department=dept,
name=name,
scoped_permissions=scoped_permissions,
)
def update_role(
self, role: DepartmentRole, name: Optional[str] = None
) -> DepartmentRole:
"""Обновляет роль (переименование LDAP-группы + БД).
NOTE: Метод делегирует к DepartmentService.
Args:
role: Роль.
name: Новое название (опционально).
Returns:
DepartmentRole: Обновлённая роль.
"""
changes = {}
if name:
changes["name"] = name
return self._department_service.update_role(role, changes)
def delete_role(self, role: DepartmentRole) -> None:
"""Удаляет роль: LDAP-группу + БД.
NOTE: Метод делегирует к DepartmentService.
Args:
role: Роль для удаления.
"""
return self._department_service.delete_role(role)
def move_user_to_base(self, employee: Employee, base_dn: str) -> str:
"""Перемещает пользователя в указанный базовый OU.
NOTE: Метод делегирует к UserService.
Args:
employee: Сотрудник для перемещения.
base_dn: DN базового OU.
Returns:
str: Новый DN пользователя.
Raises:
DirectoryLdapError: Ошибка LDAP.
DirectoryServiceError: Пользователь не имеет DN.
"""
from .infrastructure.connections import _ldap
sync_state = LdapSyncState.objects.filter(
model="employee", object_pk=str(employee.pk)
).first()
if not sync_state or not sync_state.ldap_dn:
from .errors import DirectoryServiceError
raise DirectoryServiceError(f"Employee {employee.pk} has no LDAP DN")
with _ldap() as conn:
new_dn = self._user_service._move_user_to_base(
conn, sync_state.ldap_dn, base_dn
)
sync_state.touch(ldap_dn=new_dn, sync_dir="ldap")
return new_dn
# =========================== POSITIONS =========================== #
def reconcile_position(self, pos: Position) -> str: