админка+
This commit is contained in:
parent
2688df46ef
commit
4c5ea98743
@ -577,46 +577,110 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
changes = {}
|
||||
|
||||
ldap_first = get_ldap_str(ldap_user.given_name)
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение given_name: LDAP='{ldap_first}' vs Django='{employee.first_name}'")
|
||||
if ldap_first != employee.first_name:
|
||||
changes['first_name'] = employee.first_name
|
||||
|
||||
ldap_last = get_ldap_str(ldap_user.sn)
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение sn: LDAP='{ldap_last}' vs Django='{employee.last_name}'")
|
||||
if ldap_last != employee.last_name:
|
||||
changes['last_name'] = employee.last_name
|
||||
|
||||
ldap_email = get_ldap_str(ldap_user.mail)
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение mail: LDAP='{ldap_email}' vs Django='{employee.email}'")
|
||||
if ldap_email != employee.email:
|
||||
changes['email'] = employee.email
|
||||
|
||||
ldap_phone = get_ldap_str(ldap_user.telephone_number or ldap_user.mobile)
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение phone: LDAP='{ldap_phone}' vs Django='{employee.phone_number}'")
|
||||
if employee.phone_number and ldap_phone != employee.phone_number:
|
||||
changes['phone_number'] = employee.phone_number
|
||||
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: обнаружено изменений: {len(changes)} -> {list(changes.keys())}")
|
||||
|
||||
if changes:
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: вызов service.update_user с изменениями: {changes}")
|
||||
service.update_user(employee, changes)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: успешно обновлен в LDAP")
|
||||
updated_count += 1
|
||||
|
||||
# Обновляем sync state
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: обновление LdapSyncState")
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='employee',
|
||||
object_pk=str(employee.pk),
|
||||
defaults={
|
||||
'ldap_dn': ldap_user.dn,
|
||||
'last_sync_dir': 'django',
|
||||
}
|
||||
|
||||
# Проверяем, в правильной ли OU находится пользователь
|
||||
from django.conf import settings as _settings
|
||||
|
||||
expected_base = None
|
||||
if not employee.is_actually_active:
|
||||
expected_base = getattr(
|
||||
_settings, 'LDAP_DISMISSED_BASE', None
|
||||
)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: LdapSyncState обновлен")
|
||||
else:
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: изменений не обнаружено, уже синхронизирован")
|
||||
active_dept = employee.departments.first()
|
||||
if active_dept:
|
||||
dept_sync = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(active_dept.pk),
|
||||
).first()
|
||||
if dept_sync and dept_sync.ldap_dn:
|
||||
expected_base = dept_sync.ldap_dn
|
||||
else:
|
||||
depts_base = getattr(
|
||||
_settings, 'LDAP_DEPARTMENTS_BASE', ''
|
||||
)
|
||||
if depts_base:
|
||||
expected_base = f"OU={active_dept.name},{depts_base}"
|
||||
|
||||
if expected_base:
|
||||
# Текущий base_dn = DN без первой компоненты (CN=...)
|
||||
current_parts = ldap_user.dn.split(',', 1)
|
||||
current_base = current_parts[1] if len(current_parts) > 1 else ''
|
||||
|
||||
if current_base.lower() != expected_base.lower():
|
||||
try:
|
||||
ldap_user.move_to(expected_base)
|
||||
logger.info(
|
||||
f"[sync_from_django_to_ldap] Employee {employee.pk}: "
|
||||
f"перемещён из {current_base} в {expected_base}"
|
||||
)
|
||||
self.message_user(
|
||||
request,
|
||||
f'📦 {employee.email}: перемещён в {expected_base}',
|
||||
level=messages.INFO
|
||||
)
|
||||
|
||||
# Добавляем в группу DEP_* нового отдела
|
||||
try:
|
||||
from employees.ldap.orm_models import LdapOrganizationalUnit
|
||||
ou = LdapOrganizationalUnit.objects.get(
|
||||
dn=expected_base
|
||||
)
|
||||
ou.ensure_department_group().add_member(
|
||||
ldap_user.dn
|
||||
)
|
||||
except Exception:
|
||||
pass # best effort
|
||||
|
||||
except Exception as move_err:
|
||||
logger.warning(
|
||||
f"[sync_from_django_to_ldap] Employee {employee.pk}: "
|
||||
f"ошибка перемещения: {move_err}"
|
||||
)
|
||||
self.message_user(
|
||||
request,
|
||||
f'⚠️ {employee.email}: не удалось переместить в {expected_base}: {move_err}',
|
||||
level=messages.WARNING
|
||||
)
|
||||
|
||||
# Обновляем sync state (DN мог измениться после перемещения)
|
||||
# Перечитываем пользователя чтобы получить актуальный DN
|
||||
try:
|
||||
ldap_user_fresh = LdapUser.objects.get(
|
||||
employee_number=str(employee.pk)
|
||||
)
|
||||
fresh_dn = ldap_user_fresh.dn
|
||||
except LdapUser.DoesNotExist:
|
||||
fresh_dn = ldap_user.dn
|
||||
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='employee',
|
||||
object_pk=str(employee.pk),
|
||||
defaults={
|
||||
'ldap_dn': fresh_dn,
|
||||
'last_sync_dir': 'django',
|
||||
}
|
||||
)
|
||||
|
||||
if not changes:
|
||||
success_count += 1 # Уже синхронизирован
|
||||
except Exception as e:
|
||||
logger.exception(f"[sync_from_django_to_ldap] Employee {employee.pk}: ошибка обновления в LDAP")
|
||||
@ -634,26 +698,57 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
import secrets
|
||||
from employees.ldap.domain.dtos import DirectoryUserDTO
|
||||
from employees.ldap.utils.phone_utils import normalize_phone
|
||||
from django.conf import settings as _settings
|
||||
|
||||
temp_password = secrets.token_urlsafe(16)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: временный пароль сгенерирован")
|
||||
|
||||
phone_e164 = None
|
||||
if employee.phone_number:
|
||||
phone_e164 = normalize_phone(employee.phone_number)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: телефон нормализован: {phone_e164}")
|
||||
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: создание DirectoryUserDTO")
|
||||
# Определяем department_dn на основе отдела и статуса
|
||||
department_dn = None
|
||||
|
||||
if not employee.is_actually_active:
|
||||
# Уволенный → OU=Dismissed
|
||||
department_dn = getattr(
|
||||
_settings, 'LDAP_DISMISSED_BASE', None
|
||||
)
|
||||
logger.info(
|
||||
f"[sync_from_django_to_ldap] Employee {employee.pk}: "
|
||||
f"уволен, department_dn={department_dn}"
|
||||
)
|
||||
else:
|
||||
# Активный → OU отдела из EmployeeDepartment
|
||||
active_dept = employee.departments.first()
|
||||
if active_dept:
|
||||
dept_sync = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(active_dept.pk),
|
||||
).first()
|
||||
if dept_sync and dept_sync.ldap_dn:
|
||||
department_dn = dept_sync.ldap_dn
|
||||
else:
|
||||
# Формируем DN отдела из имени
|
||||
depts_base = getattr(
|
||||
_settings, 'LDAP_DEPARTMENTS_BASE', ''
|
||||
)
|
||||
if depts_base:
|
||||
department_dn = f"OU={active_dept.name},{depts_base}"
|
||||
logger.info(
|
||||
f"[sync_from_django_to_ldap] Employee {employee.pk}: "
|
||||
f"отдел={active_dept.name}, department_dn={department_dn}"
|
||||
)
|
||||
|
||||
dto = DirectoryUserDTO(
|
||||
first_name=employee.first_name,
|
||||
last_name=employee.last_name,
|
||||
email=employee.email,
|
||||
phone_e164=phone_e164,
|
||||
department_dn=None,
|
||||
department_dn=department_dn,
|
||||
group_cns=[],
|
||||
initial_password=temp_password,
|
||||
)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: DTO создан: {dto}")
|
||||
|
||||
# Создаем через сервис
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: вызов service.create_user(dto)")
|
||||
@ -922,11 +1017,11 @@ class EmployeeActionAdmin(SimpleHistoryAdmin):
|
||||
|
||||
@admin.register(Department)
|
||||
class DepartmentAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "head", "head_appointed_at", "created_at")
|
||||
list_display = ("name", "head", "ldap_sync_status", "head_appointed_at", "created_at")
|
||||
list_filter = ("head",)
|
||||
search_fields = ("name", "description", "ldap_group_dn")
|
||||
autocomplete_fields = ("head",)
|
||||
readonly_fields = ("created_at", "head_appointed_at")
|
||||
readonly_fields = ("created_at", "head_appointed_at", "ldap_sync_info")
|
||||
inlines = [DepartmentMembershipInline]
|
||||
|
||||
fieldsets = (
|
||||
@ -935,11 +1030,487 @@ class DepartmentAdmin(admin.ModelAdmin):
|
||||
"fields": ("head", "head_appointed_at"),
|
||||
}),
|
||||
("LDAP", {
|
||||
"fields": ("ldap_group_dn",),
|
||||
"fields": ("ldap_group_dn", "ldap_sync_info"),
|
||||
"classes": ("collapse",),
|
||||
}),
|
||||
("Служебное", {"fields": ("created_at",)}),
|
||||
)
|
||||
|
||||
actions = [
|
||||
'sync_from_ldap_to_django',
|
||||
'sync_from_django_to_ldap',
|
||||
'show_sync_diff',
|
||||
]
|
||||
|
||||
def ldap_sync_status(self, obj):
|
||||
"""Краткий статус LDAP синхронизации для списка."""
|
||||
if not obj.pk:
|
||||
return "-"
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
|
||||
try:
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(obj.pk)
|
||||
).first()
|
||||
|
||||
if not sync_state:
|
||||
# Нет записи синхронизации
|
||||
create_url = reverse('admin:employees_ldapsyncstate_add')
|
||||
return format_html(
|
||||
'<span style="color: red;" title="Нет записи синхронизации">'
|
||||
'<a href="{}?model=department&object_pk={}" style="color: red;">❌ Нет записи</a>'
|
||||
'</span>',
|
||||
create_url,
|
||||
obj.pk
|
||||
)
|
||||
|
||||
# Проверяем полноту данных
|
||||
has_dn = bool(sync_state.ldap_dn)
|
||||
has_updated = bool(sync_state.updated_at)
|
||||
|
||||
# Полные данные
|
||||
if has_dn and has_updated:
|
||||
# Определяем направление последней синхронизации
|
||||
if sync_state.last_sync_dir == 'ldap':
|
||||
icon = '⬇️'
|
||||
title_dir = 'LDAP → Django'
|
||||
elif sync_state.last_sync_dir == 'django':
|
||||
icon = '⬆️'
|
||||
title_dir = 'Django → LDAP'
|
||||
else:
|
||||
icon = '✅'
|
||||
title_dir = 'Синхронизирован'
|
||||
|
||||
dn_short = sync_state.ldap_dn[:60] + '...' if len(sync_state.ldap_dn) > 60 else sync_state.ldap_dn
|
||||
|
||||
return format_html(
|
||||
'{} <span title="DN: {}\nНаправление: {}">{}</span>',
|
||||
icon,
|
||||
dn_short,
|
||||
title_dir,
|
||||
sync_state.updated_at.strftime("%d.%m.%Y %H:%M")
|
||||
)
|
||||
# Неполные данные
|
||||
elif has_updated:
|
||||
return format_html(
|
||||
'<span style="color: orange;" title="Отсутствует DN">⚠️ {}</span>',
|
||||
sync_state.updated_at.strftime("%d.%m.%Y %H:%M")
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<span style="color: orange;" title="Нет даты обновления">⚠️ Неполные данные</span>'
|
||||
)
|
||||
except Exception as e:
|
||||
return format_html(
|
||||
'<span style="color: red;" title="Ошибка: {}">❌ Ошибка</span>',
|
||||
str(e)
|
||||
)
|
||||
|
||||
ldap_sync_status.short_description = "LDAP статус"
|
||||
ldap_sync_status.admin_order_field = "id"
|
||||
|
||||
def ldap_sync_info(self, obj):
|
||||
"""Подробная информация о синхронизации для формы."""
|
||||
if not obj.pk:
|
||||
return "-"
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
|
||||
try:
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(obj.pk)
|
||||
).first()
|
||||
|
||||
if not sync_state:
|
||||
create_url = reverse('admin:employees_ldapsyncstate_add')
|
||||
return format_html(
|
||||
'<div style="padding:10px;background:#f8d7da;border-left:4px solid #dc3545;">'
|
||||
'❌ <strong>Нет записи синхронизации</strong><br>'
|
||||
'<small>Создайте запись синхронизации или выполните синхронизацию.</small><br>'
|
||||
'<a href="{}?model=department&object_pk={}" target="_blank">➕ Создать запись</a>'
|
||||
'</div>',
|
||||
create_url,
|
||||
obj.pk
|
||||
)
|
||||
|
||||
# Формируем подробную информацию
|
||||
sync_url = reverse('admin:employees_ldapsyncstate_change', args=[sync_state.pk])
|
||||
|
||||
if sync_state.last_sync_dir == 'ldap':
|
||||
direction = '⬇️ LDAP → Django'
|
||||
color = '#17a2b8'
|
||||
elif sync_state.last_sync_dir == 'django':
|
||||
direction = '⬆️ Django → LDAP'
|
||||
color = '#0066cc'
|
||||
else:
|
||||
direction = '↔️ Неизвестно'
|
||||
color = '#6c757d'
|
||||
|
||||
return format_html(
|
||||
'<div style="padding:10px;background:#d1ecf1;border-left:4px solid {};">'
|
||||
'<strong>Информация о синхронизации:</strong><br>'
|
||||
'<table style="margin-top:5px;">'
|
||||
'<tr><td><strong>LDAP DN:</strong></td><td><code style="font-size:0.85em;">{}</code></td></tr>'
|
||||
'<tr><td><strong>Направление:</strong></td><td>{}</td></tr>'
|
||||
'<tr><td><strong>Обновлено:</strong></td><td>{}</td></tr>'
|
||||
'</table>'
|
||||
'<a href="{}" target="_blank" style="margin-top:10px;display:inline-block;">📋 Открыть запись</a>'
|
||||
'</div>',
|
||||
color,
|
||||
sync_state.ldap_dn or '—',
|
||||
direction,
|
||||
sync_state.updated_at.strftime('%Y-%m-%d %H:%M:%S') if sync_state.updated_at else '—',
|
||||
sync_url
|
||||
)
|
||||
except Exception as e:
|
||||
return format_html(
|
||||
'<div style="padding:10px;background:#f8d7da;border-left:4px solid #dc3545;">'
|
||||
'❌ <strong>Ошибка:</strong> {}'
|
||||
'</div>',
|
||||
str(e)
|
||||
)
|
||||
|
||||
ldap_sync_info.short_description = "Статус синхронизации"
|
||||
|
||||
@admin.action(description='🔄 Синхронизировать LDAP → Django (LDAP как источник истины)')
|
||||
def sync_from_ldap_to_django(self, request, queryset):
|
||||
"""Синхронизирует выбранные отделы из LDAP OU в Django Department."""
|
||||
from employees.ldap.orm_models import LdapOrganizationalUnit
|
||||
from employees.ldap.utils.ldap_utils import get_ldap_str
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
warnings = []
|
||||
|
||||
for dept in queryset:
|
||||
# Ищем соответствующую OU в LDAP
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(dept.pk),
|
||||
).first()
|
||||
|
||||
ldap_ou = None
|
||||
if sync_state and sync_state.ldap_dn:
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(dn=sync_state.ldap_dn)
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
warnings.append(f'⚠️ {dept.name}: OU с DN {sync_state.ldap_dn} не найдена')
|
||||
|
||||
if not ldap_ou:
|
||||
# Пытаемся найти по имени
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(ou=dept.name)
|
||||
except (LdapOrganizationalUnit.DoesNotExist, LdapOrganizationalUnit.MultipleObjectsReturned):
|
||||
error_count += 1
|
||||
warnings.append(f'⚠️ {dept.name}: не найдена соответствующая OU в LDAP')
|
||||
continue
|
||||
|
||||
try:
|
||||
# Обновляем Department из LDAP OU
|
||||
dept.description = get_ldap_str(ldap_ou.description) or dept.description
|
||||
|
||||
# Обновляем руководителя из managedBy
|
||||
mb = get_ldap_str(ldap_ou.managed_by)
|
||||
if mb:
|
||||
try:
|
||||
from employees.ldap.orm_models import LdapUser
|
||||
mgr = LdapUser.objects.get(dn=mb)
|
||||
if mgr.employee_number:
|
||||
try:
|
||||
dept.head = Employee.objects.get(pk=int(mgr.employee_number))
|
||||
except (Employee.DoesNotExist, ValueError):
|
||||
warnings.append(
|
||||
f'⚠️ {dept.name}: руководитель Employee #{mgr.employee_number} не найден'
|
||||
)
|
||||
except LdapUser.DoesNotExist:
|
||||
warnings.append(f'⚠️ {dept.name}: managedBy DN не найден в LDAP')
|
||||
|
||||
dept._skip_ldap_sync = True
|
||||
dept.save()
|
||||
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='department',
|
||||
object_pk=str(dept.pk),
|
||||
defaults={
|
||||
'ldap_dn': ldap_ou.dn,
|
||||
'last_sync_dir': 'ldap',
|
||||
},
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
self.message_user(
|
||||
request, f'Ошибка синхронизации {dept.name}: {e}',
|
||||
level=messages.ERROR,
|
||||
)
|
||||
|
||||
for w in warnings[:10]:
|
||||
self.message_user(request, w, level=messages.WARNING)
|
||||
if len(warnings) > 10:
|
||||
self.message_user(
|
||||
request, f'...и ещё {len(warnings) - 10} предупреждений',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
if success_count:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Успешно синхронизировано: {success_count} отделов (LDAP → Django)',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
if error_count:
|
||||
self.message_user(
|
||||
request, f'Ошибок: {error_count}', level=messages.WARNING,
|
||||
)
|
||||
|
||||
@admin.action(description='🔄 Синхронизировать Django → LDAP (Django как источник истины)')
|
||||
def sync_from_django_to_ldap(self, request, queryset):
|
||||
"""Синхронизирует выбранные отделы из Django Department в LDAP OU."""
|
||||
from employees.ldap.orm_models import LdapOrganizationalUnit
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
from django.conf import settings
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
created_count = 0
|
||||
warnings = []
|
||||
|
||||
for dept in queryset:
|
||||
if not dept.name or not dept.name.strip():
|
||||
warnings.append(f'⚠️ Department #{dept.pk}: пустое название')
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# Ищем соответствующую OU
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(dept.pk),
|
||||
).first()
|
||||
|
||||
ldap_ou = None
|
||||
created_new = False
|
||||
|
||||
if sync_state and sync_state.ldap_dn:
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(dn=sync_state.ldap_dn)
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not ldap_ou:
|
||||
# Пытаемся найти по имени
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(ou=dept.name)
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
# OU не найдена - создаём через низкоуровневый LDAP
|
||||
try:
|
||||
with _ldap() as conn:
|
||||
base = getattr(settings, 'LDAP_DEPARTMENTS_BASE', '')
|
||||
if not base:
|
||||
raise RuntimeError('LDAP_DEPARTMENTS_BASE not configured')
|
||||
|
||||
ou_dn = f"OU={dept.name},{base}"
|
||||
|
||||
# Проверяем существование
|
||||
from ldap3 import BASE
|
||||
ok = conn.search(
|
||||
ou_dn, '(objectClass=organizationalUnit)', search_scope=BASE
|
||||
)
|
||||
|
||||
if not ok or not conn.entries:
|
||||
# Создаём OU
|
||||
ok = conn.add(ou_dn, ['top', 'organizationalUnit'])
|
||||
if not ok:
|
||||
raise RuntimeError(f'LDAP add OU failed: {conn.result}')
|
||||
created_new = True
|
||||
created_count += 1
|
||||
|
||||
# Получаем созданную OU через ORM
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(dn=ou_dn)
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
raise RuntimeError(f'OU был создан, но не найден по DN: {ou_dn}')
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
self.message_user(
|
||||
request, f'Ош ибка создания OU для {dept.name}: {e}',
|
||||
level=messages.ERROR,
|
||||
)
|
||||
continue
|
||||
except LdapOrganizationalUnit.MultipleObjectsReturned:
|
||||
error_count += 1
|
||||
warnings.append(
|
||||
f'⚠️ {dept.name}: найдено несколько OU с таким именем'
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
# Обновляем LDAP OU из Django Department
|
||||
ldap_ou.description = dept.description or ''
|
||||
|
||||
# Обновляем managedBy
|
||||
if dept.head:
|
||||
head_sync = LdapSyncState.objects.filter(
|
||||
model='employee',
|
||||
object_pk=str(dept.head.pk),
|
||||
).first()
|
||||
if head_sync and head_sync.ldap_dn:
|
||||
ldap_ou.managed_by = head_sync.ldap_dn
|
||||
else:
|
||||
warnings.append(
|
||||
f'⚠️ {dept.name}: руководитель {dept.head} не имеет LDAP DN'
|
||||
)
|
||||
else:
|
||||
ldap_ou.managed_by = ''
|
||||
|
||||
ldap_ou.save()
|
||||
|
||||
# Создаём/обновляем группу отдела DEP_*
|
||||
# и синхронизируем участников
|
||||
try:
|
||||
dep_group = ldap_ou.ensure_department_group()
|
||||
|
||||
# Собираем DN активных сотрудников отдела
|
||||
from employees.models import EmployeeDepartment
|
||||
active_links = EmployeeDepartment.objects.filter(
|
||||
department=dept, is_active=True,
|
||||
).select_related('employee')
|
||||
|
||||
member_dns = []
|
||||
for link in active_links:
|
||||
emp_sync = LdapSyncState.objects.filter(
|
||||
model='employee',
|
||||
object_pk=str(link.employee_id),
|
||||
).first()
|
||||
if emp_sync and emp_sync.ldap_dn:
|
||||
member_dns.append(emp_sync.ldap_dn)
|
||||
|
||||
result = dep_group.sync_members(member_dns)
|
||||
if result['added'] or result['removed']:
|
||||
self.message_user(
|
||||
request,
|
||||
f'👥 {dept.name}: группа DEP_* — '
|
||||
f'+{result["added"]}/−{result["removed"]}',
|
||||
level=messages.INFO,
|
||||
)
|
||||
except Exception as grp_err:
|
||||
warnings.append(
|
||||
f'⚠️ {dept.name}: группа DEP_*: {grp_err}'
|
||||
)
|
||||
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='department',
|
||||
object_pk=str(dept.pk),
|
||||
defaults={
|
||||
'ldap_dn': ldap_ou.dn,
|
||||
'last_sync_dir': 'django',
|
||||
},
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
self.message_user(
|
||||
request, f'Ошибка синхронизации {dept.name}: {e}',
|
||||
level=messages.ERROR,
|
||||
)
|
||||
|
||||
for w in warnings[:10]:
|
||||
self.message_user(request, w, level=messages.WARNING)
|
||||
if len(warnings) > 10:
|
||||
self.message_user(
|
||||
request, f'...и ещё {len(warnings) - 10} предупреждений',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
if created_count:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Создано новых OU в LDAP: {created_count}',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
if success_count:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Успешно синхронизировано: {success_count} отделов (Django → LDAP)',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
if error_count:
|
||||
self.message_user(
|
||||
request, f'Ошибок: {error_count}', level=messages.WARNING,
|
||||
)
|
||||
|
||||
@admin.action(description='🔍 Показать различия Django ↔ LDAP')
|
||||
def show_sync_diff(self, request, queryset):
|
||||
"""Показывает различия между Django Department и LDAP OU."""
|
||||
from employees.ldap.orm_models import LdapOrganizationalUnit
|
||||
from employees.ldap.utils.ldap_utils import get_ldap_str
|
||||
|
||||
diffs = []
|
||||
|
||||
for dept in queryset:
|
||||
# Ищем OU
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
model='department',
|
||||
object_pk=str(dept.pk),
|
||||
).first()
|
||||
|
||||
ldap_ou = None
|
||||
if sync_state and sync_state.ldap_dn:
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(dn=sync_state.ldap_dn)
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not ldap_ou:
|
||||
try:
|
||||
ldap_ou = LdapOrganizationalUnit.objects.get(ou=dept.name)
|
||||
except (LdapOrganizationalUnit.DoesNotExist, LdapOrganizationalUnit.MultipleObjectsReturned):
|
||||
diffs.append(f'{dept.name}: не найдена соответствующая OU в LDAP')
|
||||
continue
|
||||
|
||||
dept_diffs = []
|
||||
|
||||
# Сравниваем описание
|
||||
ldap_desc = get_ldap_str(ldap_ou.description) or ''
|
||||
django_desc = dept.description or ''
|
||||
if ldap_desc != django_desc:
|
||||
dept_diffs.append(
|
||||
f"Описание: LDAP='{ldap_desc[:50]}' vs Django='{django_desc[:50]}'"
|
||||
)
|
||||
|
||||
# Сравниваем руководителя
|
||||
mb = get_ldap_str(ldap_ou.managed_by)
|
||||
if dept.head:
|
||||
head_sync = LdapSyncState.objects.filter(
|
||||
model='employee',
|
||||
object_pk=str(dept.head.pk),
|
||||
).first()
|
||||
head_dn = head_sync.ldap_dn if head_sync else None
|
||||
if mb != head_dn:
|
||||
dept_diffs.append(
|
||||
f"Руководитель: LDAP='{mb or '—'}' vs Django='{dept.head}'"
|
||||
)
|
||||
elif mb:
|
||||
dept_diffs.append(f"Руководитель: LDAP='{mb}' vs Django='не назначен'")
|
||||
|
||||
if dept_diffs:
|
||||
diffs.append(f'{dept.name}: ' + ', '.join(dept_diffs))
|
||||
|
||||
if diffs:
|
||||
self.message_user(
|
||||
request,
|
||||
'Найдены различия:\n' + '\n'.join(diffs),
|
||||
level=messages.WARNING,
|
||||
)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
'Различий не найдено. Все выбранные отделы синхронизированы.',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(DepartmentRole)
|
||||
|
||||
@ -31,12 +31,17 @@ class EmployeesConfig(AppConfig):
|
||||
def safe_results_iter(self, results=None, tuple_expected=False,
|
||||
chunked_fetch=False, chunk_size=None):
|
||||
"""Обёртка над оригинальным results_iter с безопасным attname."""
|
||||
import ldap as ldap_lib
|
||||
try:
|
||||
yield from original_results_iter(
|
||||
self, results=results, tuple_expected=tuple_expected,
|
||||
chunked_fetch=chunked_fetch,
|
||||
**({"chunk_size": chunk_size} if chunk_size is not None else {}),
|
||||
)
|
||||
except ldap_lib.NO_SUCH_OBJECT:
|
||||
# DN не существует — возвращаем пустой результат
|
||||
# (django-ldapdb бросает ldap.NO_SUCH_OBJECT вместо DoesNotExist)
|
||||
return
|
||||
except AttributeError as exc:
|
||||
if "attname" not in str(exc):
|
||||
raise
|
||||
|
||||
@ -72,7 +72,12 @@ from .domain.dtos import (
|
||||
from .utils.group_utils_orm import sync_user_groups_by_cns_orm
|
||||
|
||||
# ORM модели
|
||||
from .orm_models import LdapUser, LdapGroup, LdapOrganizationalUnit
|
||||
from .orm_models import (
|
||||
LdapUser,
|
||||
LdapGroup,
|
||||
LdapOrganizationalUnitGroup,
|
||||
LdapOrganizationalUnit,
|
||||
)
|
||||
|
||||
# Подключения
|
||||
from .infrastructure.connections import LdapConfig, _ldap
|
||||
@ -119,6 +124,7 @@ __all__ = [
|
||||
# ORM
|
||||
"LdapUser",
|
||||
"LdapGroup",
|
||||
"LdapOrganizationalUnitGroup",
|
||||
"LdapOrganizationalUnit",
|
||||
|
||||
# Подключения
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -107,25 +107,7 @@ class ModifyDnMixin:
|
||||
)
|
||||
|
||||
if needs_move:
|
||||
old_dn = self._saved_dn
|
||||
new_rdn = self.build_rdn()
|
||||
new_superior = current_base_dn
|
||||
new_dn = f"{new_rdn},{new_superior}"
|
||||
|
||||
with _ldap() as conn:
|
||||
success = conn.modify_dn(
|
||||
old_dn, new_rdn,
|
||||
new_superior=new_superior,
|
||||
)
|
||||
if not success:
|
||||
raise RuntimeError(
|
||||
f"Failed to move LDAP object: {conn.result}"
|
||||
)
|
||||
logger.info(f"ModifyDnMixin: Moved {old_dn} → {new_dn}")
|
||||
|
||||
self._saved_dn = new_dn
|
||||
self.dn = new_dn
|
||||
self._original_base_dn = current_base_dn
|
||||
self.move_to(current_base_dn)
|
||||
|
||||
# --- 2) RDN-атрибут (cn/ou) изменился ---
|
||||
new_rdn_value = self._get_rdn_value()
|
||||
@ -177,6 +159,44 @@ class ModifyDnMixin:
|
||||
|
||||
return result
|
||||
|
||||
def move_to(self, new_base_dn):
|
||||
"""Переместить объект в другую OU без обновления атрибутов.
|
||||
|
||||
Выполняет LDAP modify_dn с newsuperior. НЕ вызывает super().save(),
|
||||
поэтому не пытается записать системные атрибуты (whenChanged и др.),
|
||||
которые AD обновляет автоматически при перемещении.
|
||||
|
||||
Args:
|
||||
new_base_dn: Новый base DN (целевая OU).
|
||||
Пример: "OU=Dismissed,OU=company,DC=robotail,DC=local"
|
||||
|
||||
Raises:
|
||||
RuntimeError: Если modify_dn не удался.
|
||||
ValueError: Если объект ещё не сохранён в LDAP.
|
||||
"""
|
||||
from .infrastructure.connections import _ldap
|
||||
|
||||
if not hasattr(self, '_saved_dn') or not self._saved_dn:
|
||||
raise ValueError("Cannot move an object that hasn't been saved to LDAP yet")
|
||||
|
||||
old_dn = self._saved_dn
|
||||
new_rdn = self.build_rdn()
|
||||
new_dn = f"{new_rdn},{new_base_dn}"
|
||||
|
||||
with _ldap() as conn:
|
||||
success = conn.modify_dn(
|
||||
old_dn, new_rdn,
|
||||
new_superior=new_base_dn,
|
||||
)
|
||||
if not success:
|
||||
raise RuntimeError(conn.result)
|
||||
logger.info(f"ModifyDnMixin.move_to: {old_dn} → {new_dn}")
|
||||
|
||||
self._saved_dn = new_dn
|
||||
self.dn = new_dn
|
||||
self.base_dn = new_base_dn
|
||||
self._original_base_dn = new_base_dn
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db, field_names, values):
|
||||
"""Переопределяем from_db для сохранения оригинальных значений при загрузке."""
|
||||
|
||||
@ -80,8 +80,10 @@ DELETE пользователя:
|
||||
Группы (зависит от типа):
|
||||
- Глобальные группы:
|
||||
CN=Developers,OU=Groups,OU=company,DC=robotail,DC=local
|
||||
- Группы OU (внутри OU):
|
||||
CN=DEP_IT,OU=IT,OU=Departments,OU=company,DC=robotail,DC=local
|
||||
- Роли отделов:
|
||||
CN=ROLE_Manager,OU=Roles,OU=IT,OU=Departments,OU=company,DC=robotail,DC=local
|
||||
CN=ROLE_Manager,OU=IT,OU=Departments,OU=company,DC=robotail,DC=local
|
||||
|
||||
Организационные единицы:
|
||||
- Отделы:
|
||||
@ -98,7 +100,7 @@ base_dn используется ТОЛЬКО для SUBTREE поиска
|
||||
|
||||
- LdapUser.base_dn = "OU=company,DC=..." — покрывает все отделы и Dismissed
|
||||
- LdapGroup.base_dn = "DC=..." — покрывает Groups и роли отделов
|
||||
- LdapOrganizationalUnit.base_dn = "DC=..." — покрывает все OU
|
||||
- LdapOrganizationalUnit.base_dn = "OU=Departments,..." — покрывает OU отделов
|
||||
"""
|
||||
|
||||
import datetime as _dt
|
||||
@ -173,9 +175,9 @@ def get_users_base():
|
||||
def get_base_dn():
|
||||
"""Получает корневой base DN из settings.
|
||||
|
||||
Используется для групп и OU. Должен покрывать:
|
||||
Используется для групп. Должен покрывать:
|
||||
- OU=Groups (глобальные группы)
|
||||
- OU=Departments (отделы и их роли)
|
||||
- OU=Departments (роли отделов)
|
||||
- Любые другие контейнеры
|
||||
|
||||
Приоритет:
|
||||
@ -190,6 +192,22 @@ def get_base_dn():
|
||||
)
|
||||
|
||||
|
||||
def get_departments_base():
|
||||
"""Получает base DN для отделов (OU) из settings.
|
||||
|
||||
Отделы располагаются в фиксированном контейнере:
|
||||
OU=<DeptName>,OU=Departments,OU=company,DC=robotail,DC=local
|
||||
|
||||
Приоритет:
|
||||
1. LDAP_DEPARTMENTS_BASE (если задан)
|
||||
2. Fallback: OU=Departments,DC=robotail,DC=local
|
||||
"""
|
||||
return getattr(
|
||||
settings, 'LDAP_DEPARTMENTS_BASE',
|
||||
'OU=Departments,DC=robotail,DC=local',
|
||||
)
|
||||
|
||||
|
||||
class LdapUser(LdapSyncStateMixin, ModifyDnMixin, LdapModel):
|
||||
"""LDAP модель для пользователя Active Directory.
|
||||
|
||||
@ -291,7 +309,8 @@ class LdapGroup(ModifyDnMixin, LdapModel):
|
||||
- base_dn используется только для SUBTREE поиска через .objects.filter()
|
||||
- Группы могут находиться в разных OU:
|
||||
• CN=Group,OU=Groups,DC=... (глобальные группы)
|
||||
• CN=ROLE_*,OU=Roles,OU=<Dept>,OU=Departments,... (роли отделов)
|
||||
• CN=DEP_*,OU=<Dept>,OU=Departments,... (группы отделов)
|
||||
• CN=ROLE_*,OU=<Dept>,OU=Departments,... (роли отделов)
|
||||
- При изменении base_dn ModifyDnMixin автоматически выполнит modify_dn!
|
||||
|
||||
Операции через django-ldapdb:
|
||||
@ -301,8 +320,10 @@ class LdapGroup(ModifyDnMixin, LdapModel):
|
||||
- RENAME: переименование через GroupService.rename() (low-level ldap3)
|
||||
- DELETE: удаление через .delete() (ORM)
|
||||
|
||||
При создании ролей отделов DN формируется как:
|
||||
CN=ROLE_{name},OU=Roles,OU={dept},OU=Departments,...
|
||||
Типы групп в LDAP:
|
||||
- Глобальные группы: CN=<Name>,OU=Groups,...
|
||||
- Группы отделов: CN=DEP_<DeptName>,OU=<DeptName>,OU=Departments,...
|
||||
- Роли отделов: CN=ROLE_{name},OU={dept},OU=Departments,...
|
||||
"""
|
||||
|
||||
# Базовая конфигурация
|
||||
@ -342,6 +363,159 @@ class LdapGroup(ModifyDnMixin, LdapModel):
|
||||
return f"<LdapGroup: {self.cn}>"
|
||||
|
||||
|
||||
class LdapOrganizationalUnitGroup(ModifyDnMixin, LdapModel):
|
||||
"""LDAP модель группы организационной единицы (DEP_*).
|
||||
|
||||
Каждая OU в LDAP имеет агрегаторную группу,
|
||||
содержащую всех активных сотрудников этой OU:
|
||||
CN=DEP_IT,OU=IT,OU=Departments,OU=company,DC=robotail,DC=local
|
||||
|
||||
Миксины:
|
||||
- ModifyDnMixin: перемещение при переименовании OU
|
||||
|
||||
ВАЖНО О base_dn:
|
||||
- base_dn = LDAP_DEPARTMENTS_BASE — SUBTREE поиск найдёт
|
||||
все DEP_* группы внутри всех OU
|
||||
- objectClass фильтрация: ищет только group (не OU)
|
||||
|
||||
Операции:
|
||||
- READ: .objects.filter(cn__startswith='DEP_')
|
||||
- UPDATE: изменение description/member через .save()
|
||||
- MOVE: group.base_dn = new_ou_dn; group.save()
|
||||
- DELETE: .delete()
|
||||
- CREATE: ensure() на LdapOrganizationalUnit (low-level ldap3)
|
||||
|
||||
Управление членством:
|
||||
- add_member(user_dn) → добавить пользователя
|
||||
- remove_member(user_dn) → удалить пользователя
|
||||
- sync_members(user_dns) → привести к точному списку
|
||||
- list_members() → текущий список DN участников
|
||||
"""
|
||||
|
||||
# Базовая конфигурация: ищем группы внутри OU отделов
|
||||
base_dn = get_departments_base()
|
||||
object_classes = ['top', 'group']
|
||||
|
||||
rdn_attributes = ['cn']
|
||||
|
||||
# Основные атрибуты
|
||||
cn = CharField(db_column='cn')
|
||||
sam_account_name = CharField(db_column='sAMAccountName')
|
||||
description = CharField(db_column='description', blank=True)
|
||||
|
||||
# Члены группы
|
||||
member = ListField(db_column='member', blank=True)
|
||||
member_of = ListField(db_column='memberOf', blank=True)
|
||||
|
||||
# Временные метки
|
||||
when_created = DateTimeField(db_column='whenCreated')
|
||||
when_changed = DateTimeField(db_column='whenChanged')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return f"OUGroup: {self.cn}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LdapOrganizationalUnitGroup: {self.cn}>"
|
||||
|
||||
# ==================== Membership Methods ==================== #
|
||||
|
||||
def list_members(self) -> list[str]:
|
||||
"""Возвращает список DN участников группы."""
|
||||
return list(self.member or [])
|
||||
|
||||
def add_member(self, member_dn: str) -> None:
|
||||
"""Добавляет участника в группу отдела.
|
||||
|
||||
Args:
|
||||
member_dn: DN пользователя.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Если операция не удалась.
|
||||
"""
|
||||
current = self.member or []
|
||||
if member_dn in current:
|
||||
return
|
||||
|
||||
from .infrastructure.connections import _ldap
|
||||
from ldap3 import MODIFY_ADD
|
||||
|
||||
with _ldap() as conn:
|
||||
ok = conn.modify(
|
||||
self.dn,
|
||||
{"member": [(MODIFY_ADD, [member_dn])]}
|
||||
)
|
||||
if not ok:
|
||||
result_str = str(conn.result)
|
||||
if "attributeOrValueExists" not in result_str:
|
||||
raise RuntimeError(
|
||||
f"add_member failed: {conn.result}"
|
||||
)
|
||||
|
||||
def remove_member(self, member_dn: str) -> None:
|
||||
"""Удаляет участника из группы отдела.
|
||||
|
||||
Args:
|
||||
member_dn: DN пользователя.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Если операция не удалась.
|
||||
"""
|
||||
from .infrastructure.connections import _ldap
|
||||
from ldap3 import MODIFY_DELETE
|
||||
|
||||
with _ldap() as conn:
|
||||
ok = conn.modify(
|
||||
self.dn,
|
||||
{"member": [(MODIFY_DELETE, [member_dn])]}
|
||||
)
|
||||
if not ok:
|
||||
result_str = str(conn.result)
|
||||
if "noSuchAttribute" not in result_str:
|
||||
raise RuntimeError(
|
||||
f"remove_member failed: {conn.result}"
|
||||
)
|
||||
|
||||
def sync_members(self, desired_dns: list[str]) -> dict:
|
||||
"""Синхронизирует состав группы к точному списку.
|
||||
|
||||
Добавляет недостающих, удаляет лишних.
|
||||
|
||||
Args:
|
||||
desired_dns: Целевой список DN участников.
|
||||
|
||||
Returns:
|
||||
dict: {'added': int, 'removed': int}
|
||||
"""
|
||||
current = set(self.member or [])
|
||||
desired = set(desired_dns)
|
||||
|
||||
to_add = desired - current
|
||||
to_remove = current - desired
|
||||
|
||||
from .infrastructure.connections import _ldap
|
||||
from ldap3 import MODIFY_ADD, MODIFY_DELETE
|
||||
|
||||
with _ldap() as conn:
|
||||
if to_remove:
|
||||
conn.modify(
|
||||
self.dn,
|
||||
{"member": [(MODIFY_DELETE, list(to_remove))]}
|
||||
)
|
||||
if to_add:
|
||||
conn.modify(
|
||||
self.dn,
|
||||
{"member": [(MODIFY_ADD, list(to_add))]}
|
||||
)
|
||||
|
||||
return {
|
||||
'added': len(to_add),
|
||||
'removed': len(to_remove),
|
||||
}
|
||||
|
||||
|
||||
class LdapOrganizationalUnit(ModifyDnMixin, LdapModel):
|
||||
"""LDAP модель для Organizational Unit (отдел).
|
||||
|
||||
@ -363,12 +537,21 @@ class LdapOrganizationalUnit(ModifyDnMixin, LdapModel):
|
||||
- MOVE: ou.base_dn = new_dn; ou.save() (автоматический modify_dn!)
|
||||
- DELETE: удаление через ldap3.Connection.delete()
|
||||
|
||||
При создании отдела DN формируется как:
|
||||
OU={dept_name},{LDAP_DEPARTMENTS_BASE}
|
||||
Структура отдела в LDAP:
|
||||
OU=<DeptName>,OU=Departments,... ← LdapOrganizationalUnit
|
||||
├─ CN=DEP_<DeptName> ← LdapOrganizationalUnitGroup (все члены)
|
||||
├─ CN=ROLE_Manager ← LdapGroup (роль)
|
||||
└─ CN=User1, CN=User2, ... ← LdapUser (пользователи)
|
||||
|
||||
Методы работы с группой OU (DEP_*):
|
||||
- get_department_group_dn() → DN группы OU
|
||||
- get_department_group() → LdapOrganizationalUnitGroup или None
|
||||
- ensure_department_group() →
|
||||
создаёт/возвращает LdapOrganizationalUnitGroup
|
||||
"""
|
||||
|
||||
# Базовая конфигурация
|
||||
base_dn = get_base_dn()
|
||||
base_dn = get_departments_base()
|
||||
object_classes = ['top', 'organizationalUnit']
|
||||
|
||||
# RDN атрибут для построения Distinguished Name
|
||||
@ -401,9 +584,78 @@ class LdapOrganizationalUnit(ModifyDnMixin, LdapModel):
|
||||
def __repr__(self):
|
||||
return f"<LdapOrganizationalUnit: {self.ou}>"
|
||||
|
||||
# ==================== Department Group Methods ==================== #
|
||||
|
||||
def get_department_group_dn(self) -> str:
|
||||
"""Возвращает ожидаемый DN группы отдела DEP_*.
|
||||
|
||||
Группа отдела находится внутри OU отдела:
|
||||
CN=DEP_<DeptName>,OU=<DeptName>,OU=Departments,...
|
||||
|
||||
Returns:
|
||||
str: DN группы отдела.
|
||||
"""
|
||||
return f"CN=DEP_{self.ou},{self.dn}"
|
||||
|
||||
def get_department_group(self):
|
||||
"""Возвращает группу OU (DEP_*), если существует.
|
||||
|
||||
Returns:
|
||||
LdapOrganizationalUnitGroup | None
|
||||
"""
|
||||
try:
|
||||
return LdapOrganizationalUnitGroup.objects.get(
|
||||
dn=self.get_department_group_dn()
|
||||
)
|
||||
except LdapOrganizationalUnitGroup.DoesNotExist:
|
||||
return None
|
||||
|
||||
def ensure_department_group(self):
|
||||
"""Создаёт или возвращает группу OU (DEP_*).
|
||||
|
||||
Если группа существует — возвращает её.
|
||||
Если нет — создаёт через low-level ldap3.
|
||||
|
||||
Returns:
|
||||
LdapOrganizationalUnitGroup: Группа OU.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Если создание группы не удалось.
|
||||
"""
|
||||
group = self.get_department_group()
|
||||
if group:
|
||||
return group
|
||||
|
||||
from .infrastructure.connections import _ldap
|
||||
from .utils.ldap_utils import group_type
|
||||
from .utils.text_utils import esc_rdn
|
||||
|
||||
cn = f"DEP_{self.ou}"
|
||||
group_dn = f"CN={esc_rdn(cn)},{self.dn}"
|
||||
|
||||
with _ldap() as conn:
|
||||
attrs = {
|
||||
"sAMAccountName": cn[:20],
|
||||
"description": f"{self.ou} department members",
|
||||
"groupType": group_type(
|
||||
"global", security_enabled=True
|
||||
),
|
||||
}
|
||||
ok = conn.add(group_dn, ["top", "group"], attrs)
|
||||
if not ok:
|
||||
res = str(conn.result)
|
||||
if "entryAlreadyExists" not in res:
|
||||
raise RuntimeError(
|
||||
f"Failed to create dept group: "
|
||||
f"{conn.result}"
|
||||
)
|
||||
|
||||
return LdapOrganizationalUnitGroup.objects.get(dn=group_dn)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'LdapUser',
|
||||
'LdapGroup',
|
||||
'LdapOrganizationalUnitGroup',
|
||||
'LdapOrganizationalUnit',
|
||||
]
|
||||
|
||||
@ -20,6 +20,7 @@ class LdapRouter:
|
||||
'ldapuser',
|
||||
'ldapgroup',
|
||||
'ldaporganizationalunit',
|
||||
'ldaporganizationalunitgroup',
|
||||
}
|
||||
|
||||
def db_for_read(self, model, **hints):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user