469 lines
17 KiB
Python
469 lines
17 KiB
Python
"""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__,
|
||
},
|
||
)
|