469 lines
17 KiB
Python
Raw Permalink 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.

"""Django signals для автоматической синхронизации Group с LDAP.
Файл: employees/signals/ldap/group.py
"""
import logging
from django.conf import settings
from django.contrib.auth.models import Group
from django.db.models.signals import m2m_changed, post_delete, post_save
from django.dispatch import receiver
from employees.ldap import GroupService
from employees.ldap.errors import (
DirectoryDbError,
DirectoryLdapError,
DirectoryServiceError,
)
from employees.models import LdapSyncState
from employees.signals.ldap._queue import _enqueue
logger = logging.getLogger(__name__)
def _is_ldap_enabled():
"""Проверяет, включена ли интеграция с LDAP."""
return getattr(settings, "LDAP_ENABLED", False)
def _resolve_group_dn(group: Group) -> str | None:
"""Возвращает DN группы из LdapSyncState."""
sync_state = LdapSyncState.objects.filter(
model="group", object_pk=str(group.pk)
).first()
if not sync_state or not sync_state.ldap_dn:
return None
return sync_state.ldap_dn
@receiver(post_save, sender=Group)
def sync_group_to_ldap_on_save(sender, instance, created, **kwargs):
"""Синхронизирует изменения Group с LDAP.
При создании: создает LdapGroup
При обновлении: синхронизирует имя и описание
Использует временные атрибуты для передачи LDAP-специфичных данных:
- _ldap_parent_dn: родительский DN для новой группы
- _ldap_description: описание группы
- _ldap_scope: scope ('global', 'universal', 'domainlocal')
- _ldap_security_enabled: security enabled флаг
"""
if not _is_ldap_enabled():
return
if getattr(instance, "_skip_ldap_sync", False):
return
try:
svc = GroupService()
if created:
# Создание новой LDAP группы
parent_dn = getattr(instance, "_ldap_parent_dn", None) or getattr(
settings, "LDAP_GROUPS_BASE", None
)
description = getattr(instance, "_ldap_description", None)
scope = getattr(instance, "_ldap_scope", "global")
security_enabled = getattr(instance, "_ldap_security_enabled", True)
try:
group_dn = svc.create(
cn=instance.name,
parent_dn=parent_dn,
description=description,
scope=scope,
security_enabled=security_enabled,
)
svc._touch_state(
model="group",
object_pk=instance.pk,
ldap_dn=group_dn,
sync_dir="auto",
)
logger.info(
"LDAP group created",
extra={
"event": "ldap.group.created",
"group_id": instance.pk,
"scope": scope,
"security_enabled": security_enabled,
},
)
except Exception as e:
logger.exception(
"LDAP group creation failed",
extra={
"event": "ldap.group.create_failed",
"group_id": instance.pk,
"error_type": type(e).__name__,
},
)
_enqueue(
"group_save",
"group",
instance.pk,
{
"object_pk": str(instance.pk),
"created": True,
"parent_dn": parent_dn,
"description": description,
"scope": scope,
"security_enabled": security_enabled,
},
)
else:
# Обновление существующей группы
dn = _resolve_group_dn(instance)
if not dn:
logger.debug(
"LDAP group sync skipped because DN is missing",
extra={
"event": "ldap.group.sync_skipped",
"group_id": instance.pk,
"reason": "missing_dn",
},
)
return
# Проверяем, было ли переименование
old_name = getattr(instance, "_ldap_old_name", None)
if old_name and old_name != instance.name:
try:
new_dn = svc.rename(dn, instance.name)
# Обновляем DN в LdapSyncState
sync_state = LdapSyncState.objects.filter(
model="group", object_pk=str(instance.pk)
).first()
if sync_state:
sync_state.ldap_dn = new_dn
sync_state.save(update_fields=["ldap_dn"])
dn = new_dn
logger.info(
"LDAP group renamed",
extra={
"event": "ldap.group.renamed",
"group_id": instance.pk,
},
)
except Exception as e:
logger.exception(
"LDAP group rename failed",
extra={
"event": "ldap.group.rename_failed",
"group_id": instance.pk,
"error_type": type(e).__name__,
},
)
_enqueue(
"group_save",
"group",
instance.pk,
{
"object_pk": str(instance.pk),
"created": False,
"old_name": old_name,
"dn": dn,
},
)
# Проверяем, было ли изменение описания
new_description = getattr(instance, "_ldap_description", "__NO_CHANGE__")
if new_description != "__NO_CHANGE__":
try:
svc.set_description(dn, new_description or None)
logger.info(
"LDAP group description updated",
extra={
"event": "ldap.group.description_updated",
"group_id": instance.pk,
},
)
except Exception as e:
logger.exception(
"LDAP group description update failed",
extra={
"event": "ldap.group.description_update_failed",
"group_id": instance.pk,
"error_type": type(e).__name__,
},
)
_enqueue(
"group_save",
"group",
instance.pk,
{
"object_pk": str(instance.pk),
"created": False,
"description": new_description,
"dn": dn,
},
)
except (DirectoryLdapError, DirectoryServiceError, DirectoryDbError) as e:
logger.exception(
"LDAP group sync failed",
extra={
"event": "ldap.group.sync_failed",
"group_id": instance.pk,
"object_created": created,
"error_type": type(e).__name__,
},
)
except Exception as e:
logger.exception(
"Unexpected LDAP group sync failure",
extra={
"event": "ldap.group.sync_unexpected_failure",
"group_id": instance.pk,
"object_created": created,
"error_type": type(e).__name__,
},
)
finally:
# Очищаем временные атрибуты
for attr in [
"_skip_ldap_sync",
"_ldap_parent_dn",
"_ldap_description",
"_ldap_scope",
"_ldap_security_enabled",
"_ldap_old_name",
]:
if hasattr(instance, attr):
delattr(instance, attr)
@receiver(post_delete, sender=Group)
def sync_group_to_ldap_on_delete(sender, instance, **kwargs):
"""Удаляет LDAP группу при удалении Group."""
if not _is_ldap_enabled():
return
try:
dn = _resolve_group_dn(instance)
if not dn:
logger.debug(
"LDAP group deletion skipped because DN is missing",
extra={
"event": "ldap.group.delete_skipped",
"group_id": instance.pk,
"reason": "missing_dn",
},
)
return
svc = GroupService()
svc.delete(dn)
logger.info(
"LDAP group deleted",
extra={"event": "ldap.group.deleted", "group_id": instance.pk},
)
except (DirectoryLdapError, DirectoryServiceError, DirectoryDbError) as e:
logger.exception(
"LDAP group deletion failed",
extra={
"event": "ldap.group.delete_failed",
"group_id": instance.pk,
"error_type": type(e).__name__,
},
)
_enqueue(
"group_delete",
"group",
instance.pk,
{
"dn": dn,
},
)
except Exception as e:
logger.exception(
"Unexpected LDAP group deletion failure",
extra={
"event": "ldap.group.delete_unexpected_failure",
"group_id": instance.pk,
"error_type": type(e).__name__,
},
)
@receiver(m2m_changed, sender=Group.user_set.through)
def sync_group_members_to_ldap(sender, instance, action, pk_set, **kwargs):
"""Синхронизирует изменения участников группы с LDAP.
Реагирует на post_add, post_remove, post_clear
для синхронизации member в LdapGroup.
"""
if not _is_ldap_enabled():
return
if action not in ["post_add", "post_remove", "post_clear"]:
return
if getattr(instance, "_skip_ldap_sync", False):
return
try:
dn = _resolve_group_dn(instance)
if not dn:
logger.debug(
"LDAP group member sync skipped because DN is missing",
extra={
"event": "ldap.group.member_sync_skipped",
"group_id": instance.pk,
"reason": "missing_dn",
},
)
return
svc = GroupService()
if action == "post_add" and pk_set:
# Добавление участников
from employees.models import Employee
users = Employee.objects.filter(pk__in=pk_set, is_ldap_managed=True)
if not users:
return
member_dns = []
for user in users:
sync_state = LdapSyncState.objects.filter(
model="employee", object_pk=str(user.pk)
).first()
if sync_state and sync_state.ldap_dn:
member_dns.append(sync_state.ldap_dn)
if member_dns:
try:
svc.add_members(dn, member_dns)
logger.info(
"Members added to LDAP group",
extra={
"event": "ldap.group.members_added",
"group_id": instance.pk,
"member_count": len(member_dns),
},
)
except Exception as e:
logger.exception(
"Adding members to LDAP group failed",
extra={
"event": "ldap.group.members_add_failed",
"group_id": instance.pk,
"member_count": len(member_dns),
"error_type": type(e).__name__,
},
)
_enqueue(
"group_members",
"group",
instance.pk,
{
"dn": dn,
"action": "add",
"member_dns": member_dns,
},
)
elif action == "post_remove" and pk_set:
# Удаление участников
from employees.models import Employee
users = Employee.objects.filter(pk__in=pk_set, is_ldap_managed=True)
if not users:
return
member_dns = []
for user in users:
sync_state = LdapSyncState.objects.filter(
model="employee", object_pk=str(user.pk)
).first()
if sync_state and sync_state.ldap_dn:
member_dns.append(sync_state.ldap_dn)
if member_dns:
try:
svc.remove_members(dn, member_dns)
logger.info(
"Members removed from LDAP group",
extra={
"event": "ldap.group.members_removed",
"group_id": instance.pk,
"member_count": len(member_dns),
},
)
except Exception as e:
logger.exception(
"Removing members from LDAP group failed",
extra={
"event": "ldap.group.members_remove_failed",
"group_id": instance.pk,
"member_count": len(member_dns),
"error_type": type(e).__name__,
},
)
_enqueue(
"group_members",
"group",
instance.pk,
{
"dn": dn,
"action": "remove",
"member_dns": member_dns,
},
)
elif action == "post_clear":
# Очистка всех участников
try:
svc.replace_members(dn, [])
logger.info(
"LDAP group members cleared",
extra={
"event": "ldap.group.members_cleared",
"group_id": instance.pk,
},
)
except Exception as e:
logger.exception(
"Clearing LDAP group members failed",
extra={
"event": "ldap.group.members_clear_failed",
"group_id": instance.pk,
"error_type": type(e).__name__,
},
)
_enqueue(
"group_members",
"group",
instance.pk,
{
"dn": dn,
"action": "clear",
"member_dns": [],
},
)
except (DirectoryLdapError, DirectoryServiceError, DirectoryDbError) as e:
logger.exception(
"LDAP group member sync failed",
extra={
"event": "ldap.group.member_sync_failed",
"group_id": instance.pk,
"action": action,
"error_type": type(e).__name__,
},
)
except Exception as e:
logger.exception(
"Unexpected LDAP group member sync failure",
extra={
"event": "ldap.group.member_sync_unexpected_failure",
"group_id": instance.pk,
"action": action,
"error_type": type(e).__name__,
},
)