fix(ldap): handle dismissal of employees without departments
Проблема: При увольнении сотрудника, не состоящего ни в одном отделе, он оставался в текущей OU (например, OU=Users) вместо перемещения в OU=Dismissed. Причина: Цикл по active_departments был пустым, remove_member() не вызывался, перемещение в OU=Dismissed не происходило. Решение: - Добавлена проверка departments_processed - Если сотрудник не состоял ни в одном отделе, выполняется явное перемещение - Используется get_base_dn_for_employee() для определения целевой OU - Прямой вызов _move_user_to_base() для перемещения в OU=Dismissed Тест: test_dismissal_without_department_moves_to_dismissed - Проверяет увольнение сотрудника без отделов - Убеждается, что LDAP соединение создано для перемещения - Подтверждает деактивацию в LDAP (userAccountControl=514) Edge case покрыт: Теперь все уволенные попадают в OU=Dismissed независимо от наличия отделов.
This commit is contained in:
parent
c7f43d0c5b
commit
e973cc6075
@ -2765,9 +2765,11 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
|
||||
employee=emp
|
||||
).select_related('department')
|
||||
|
||||
departments_processed = False
|
||||
for emp_dept in active_departments:
|
||||
try:
|
||||
svc.remove_member(emp_dept.department, emp)
|
||||
departments_processed = True
|
||||
except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e:
|
||||
# Логируем ошибку удаления из конкретного отдела
|
||||
import logging
|
||||
@ -2776,6 +2778,45 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
|
||||
f"Failed to remove dismissed employee from department: "
|
||||
f"employee_id={emp.id}, department_id={emp_dept.department.id}, error={e}"
|
||||
)
|
||||
|
||||
# Если сотрудник не состоял ни в одном отделе, вручную перемещаем в OU=Dismissed
|
||||
if not departments_processed:
|
||||
try:
|
||||
from employees.ldap.utils.ldap_utils import get_base_dn_for_employee
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
from employees.ldap.repositories.ldap_repository import ensure_container_exists
|
||||
from employees.models import LdapSyncState
|
||||
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
model='employee',
|
||||
object_pk=str(emp.pk)
|
||||
).first()
|
||||
|
||||
if sync_state and sync_state.ldap_dn:
|
||||
target_base = get_base_dn_for_employee(emp)
|
||||
current_dn = sync_state.ldap_dn
|
||||
|
||||
# Проверяем, нужно ли перемещение
|
||||
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")
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
f"Dismissed employee without department moved to OU=Dismissed: "
|
||||
f"employee_id={emp.id}, new_dn={new_dn}"
|
||||
)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(
|
||||
f"Failed to move dismissed employee without department to OU=Dismissed: "
|
||||
f"employee_id={emp.id}, error={e}"
|
||||
)
|
||||
except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e:
|
||||
# Логируем ошибку, но не прерываем процесс
|
||||
# БД-изменения уже применены
|
||||
|
||||
@ -548,3 +548,61 @@ def test_restoration_moves_from_dismissed_to_users(
|
||||
# Проверяем, что был вызван _move_user_to_base (через mock LDAP)
|
||||
# Это косвенная проверка - мы мокнули _ldap(), так что код выполнился
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch('api.v1.employees.views._is_ldap_enabled')
|
||||
@patch('employees.ldap.infrastructure.connections._ldap')
|
||||
@patch('employees.ldap.directory_service.DirectoryService.update_user')
|
||||
def test_dismissal_without_department_moves_to_dismissed(
|
||||
mock_update, mock_ldap_ctx, mock_is_enabled, api_client, settings
|
||||
):
|
||||
"""Проверяет перемещение в OU=Dismissed при увольнении сотрудника без отдела."""
|
||||
from employees.models import LdapSyncState
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
mock_is_enabled.return_value = True
|
||||
settings.LDAP_DISMISSED_BASE = "OU=Dismissed,DC=example,DC=com"
|
||||
settings.LDAP_USERS_BASE = "OU=Users,DC=example,DC=com"
|
||||
|
||||
# Мокаем LDAP соединение
|
||||
mock_conn = MagicMock()
|
||||
mock_ldap_ctx.return_value.__enter__.return_value = mock_conn
|
||||
mock_ldap_ctx.return_value.__exit__.return_value = False
|
||||
|
||||
staff = _user(staff=True)
|
||||
api_client.force_authenticate(user=staff)
|
||||
emp = _user()
|
||||
|
||||
# Сотрудник НЕ состоит ни в одном отделе (нет EmployeeDepartment записей)
|
||||
# Создаём запись LdapSyncState - сотрудник в OU=Users
|
||||
sync_state = LdapSyncState.objects.create(
|
||||
model='employee',
|
||||
object_pk=str(emp.pk),
|
||||
ldap_dn=f'CN={emp.email},OU=Users,DC=example,DC=com',
|
||||
last_sync_dir='ldap'
|
||||
)
|
||||
|
||||
url = reverse("api:v1:employee-actions-list")
|
||||
resp = api_client.post(
|
||||
url,
|
||||
{
|
||||
"employee": emp.id,
|
||||
"action": ACTION_DISMISSED,
|
||||
"date": timezone.now().isoformat(),
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert resp.status_code == 201
|
||||
emp.refresh_from_db()
|
||||
assert emp.is_active is False
|
||||
|
||||
# Проверяем вызов update_user (деактивация в LDAP)
|
||||
mock_update.assert_called_once()
|
||||
call_args = mock_update.call_args
|
||||
assert call_args[0][0].id == emp.id
|
||||
assert call_args[1]['changes'] == {'is_active': False}
|
||||
|
||||
# Проверяем, что LDAP соединение было создано (для перемещения в OU=Dismissed)
|
||||
assert mock_ldap_ctx.called, "LDAP connection should be established for moving to OU=Dismissed"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user