админка +
This commit is contained in:
parent
4c5ea98743
commit
1a3d533c1d
@ -2,7 +2,7 @@
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as DefaultGroupAdmin
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField, AdminPasswordChangeForm
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||
@ -1595,3 +1595,97 @@ class LdapSyncStateAdmin(admin.ModelAdmin):
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Разрешаем удаление для очистки."""
|
||||
return True
|
||||
|
||||
|
||||
# =========================
|
||||
# Кастомный GroupAdmin с LDAP синхронизацией
|
||||
# =========================
|
||||
|
||||
admin.site.unregister(Group)
|
||||
|
||||
|
||||
@admin.register(Group)
|
||||
class GroupAdmin(DefaultGroupAdmin):
|
||||
"""Расширенный GroupAdmin с кнопками синхронизации LDAP."""
|
||||
|
||||
list_display = ('name', 'ldap_dn_display')
|
||||
actions = ['sync_groups_from_ldap', 'sync_groups_to_ldap']
|
||||
|
||||
def ldap_dn_display(self, obj):
|
||||
sync = LdapSyncState.objects.filter(
|
||||
model='group', object_pk=str(obj.pk),
|
||||
).first()
|
||||
return sync.ldap_dn if sync else '—'
|
||||
ldap_dn_display.short_description = 'LDAP DN'
|
||||
|
||||
# ---------- LDAP → Django ----------
|
||||
def sync_groups_from_ldap(self, request, queryset):
|
||||
"""Синхронизирует ВСЕ группы из OU=Groups в Django Group."""
|
||||
from employees.ldap.services.group_service import GroupService
|
||||
|
||||
svc = GroupService()
|
||||
try:
|
||||
created = svc.sync_catalog(throttle_seconds=0)
|
||||
self.message_user(
|
||||
request,
|
||||
f'LDAP → Django: создано {created} новых Django Group',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
except Exception as e:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Ошибка синхронизации: {e}',
|
||||
level=messages.ERROR,
|
||||
)
|
||||
sync_groups_from_ldap.short_description = '⬇️ LDAP → Django (создать Django Group)'
|
||||
|
||||
# ---------- Django → LDAP ----------
|
||||
def sync_groups_to_ldap(self, request, queryset):
|
||||
"""Создаёт в LDAP группы, которых там ещё нет (с реальной проверкой LDAP)."""
|
||||
from employees.ldap.orm_models import LdapGroup
|
||||
from employees.ldap.services.group_service import GroupService
|
||||
|
||||
svc = GroupService()
|
||||
created = 0
|
||||
already = 0
|
||||
for dg in queryset:
|
||||
# Реальная проверка наличия группы в LDAP
|
||||
exists_in_ldap = LdapGroup.objects.filter(cn=dg.name).exists()
|
||||
if exists_in_ldap:
|
||||
ldap_grp = LdapGroup.objects.filter(cn=dg.name).first()
|
||||
# Обновляем/создаём LdapSyncState на случай рассинхрона
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='group',
|
||||
object_pk=str(dg.pk),
|
||||
defaults={
|
||||
'ldap_dn': ldap_grp.dn,
|
||||
'last_sync_dir': 'ldap',
|
||||
},
|
||||
)
|
||||
already += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
dn = svc.create(cn=dg.name)
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='group',
|
||||
object_pk=str(dg.pk),
|
||||
defaults={
|
||||
'ldap_dn': dn,
|
||||
'last_sync_dir': 'django',
|
||||
},
|
||||
)
|
||||
created += 1
|
||||
except Exception as e:
|
||||
self.message_user(
|
||||
request,
|
||||
f'⚠️ {dg.name}: {e}',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'Django → LDAP: создано {created}, уже существовало {already}',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
sync_groups_to_ldap.short_description = '⬆️ Django → LDAP (создать LDAP Group)'
|
||||
|
||||
@ -1280,6 +1280,11 @@ class LdapOrganizationalUnitGroupAdmin(admin.ModelAdmin):
|
||||
|
||||
list_per_page = 100
|
||||
|
||||
actions = [
|
||||
'show_members_info',
|
||||
'sync_members_from_django',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Группы создаются автоматически через OU."""
|
||||
return False
|
||||
@ -1288,6 +1293,81 @@ class LdapOrganizationalUnitGroupAdmin(admin.ModelAdmin):
|
||||
"""Удаление через LDAP OU admin."""
|
||||
return False
|
||||
|
||||
def _resolve_ou_groups(self, request):
|
||||
"""Получает выбранные OU группы по DN из POST данных."""
|
||||
selected_dns = request.POST.getlist('_selected_action')
|
||||
groups = []
|
||||
for dn in selected_dns:
|
||||
try:
|
||||
group = LdapOrganizationalUnitGroup.objects.get(dn=dn)
|
||||
groups.append(group)
|
||||
except LdapOrganizationalUnitGroup.DoesNotExist:
|
||||
pass
|
||||
return groups
|
||||
|
||||
# --- Actions ---
|
||||
|
||||
def show_members_info(self, request, queryset):
|
||||
"""Показывает информацию об участниках."""
|
||||
groups = self._resolve_ou_groups(request)
|
||||
for group in groups:
|
||||
members = group.member or []
|
||||
self.message_user(
|
||||
request,
|
||||
f'👥 {group.cn}: {len(members)} участников',
|
||||
level=messages.INFO,
|
||||
)
|
||||
show_members_info.short_description = '📋 Показать состав'
|
||||
|
||||
def sync_members_from_django(self, request, queryset):
|
||||
"""Синхронизирует участников из Django EmployeeDepartment."""
|
||||
groups = self._resolve_ou_groups(request)
|
||||
if not groups:
|
||||
self.message_user(
|
||||
request, 'Не удалось найти выбранные группы.',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
return
|
||||
|
||||
from employees.models import EmployeeDepartment, Department
|
||||
|
||||
for group in groups:
|
||||
cn = get_ldap_str(group.cn) or ''
|
||||
if not cn.startswith('DEP_'):
|
||||
continue
|
||||
dept_name = cn[4:]
|
||||
try:
|
||||
dept = Department.objects.get(name=dept_name)
|
||||
except Department.DoesNotExist:
|
||||
self.message_user(
|
||||
request,
|
||||
f'⚠️ {cn}: Django Department "{dept_name}" не найден',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
continue
|
||||
|
||||
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 = group.sync_members(member_dns)
|
||||
self.message_user(
|
||||
request,
|
||||
f'👥 {cn}: +{result["added"]}/−{result["removed"]} '
|
||||
f'(всего: {len(member_dns)})',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
sync_members_from_django.short_description = '🔄 Синхронизировать участников из Django'
|
||||
|
||||
def _find_django_department(self, ou_group):
|
||||
"""Находит Department по имени группы DEP_*."""
|
||||
from employees.models import Department
|
||||
@ -1586,6 +1666,13 @@ class LdapGroupAdmin(admin.ModelAdmin):
|
||||
|
||||
list_per_page = 100
|
||||
|
||||
actions = [
|
||||
'show_group_info',
|
||||
'sync_groups_from_ldap',
|
||||
'sync_groups_to_ldap',
|
||||
'delete_selected_ldap',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Группы создаются через GroupService или автоматически."""
|
||||
return False
|
||||
@ -1594,6 +1681,161 @@ class LdapGroupAdmin(admin.ModelAdmin):
|
||||
"""Удаление через LDAP services."""
|
||||
return False
|
||||
|
||||
def _resolve_ldap_groups(self, request):
|
||||
"""Получает выбранные LDAP группы по DN из POST данных."""
|
||||
selected_dns = request.POST.getlist('_selected_action')
|
||||
groups = []
|
||||
for dn in selected_dns:
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=dn)
|
||||
groups.append(group)
|
||||
except LdapGroup.DoesNotExist:
|
||||
pass
|
||||
return groups
|
||||
|
||||
# --- Actions ---
|
||||
|
||||
def show_group_info(self, request, queryset):
|
||||
"""Показывает информацию о выбранных группах."""
|
||||
groups = self._resolve_ldap_groups(request)
|
||||
if not groups:
|
||||
self.message_user(
|
||||
request, 'Не удалось найти выбранные группы.',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
return
|
||||
for group in groups:
|
||||
members = group.member or []
|
||||
member_of = group.member_of or []
|
||||
self.message_user(
|
||||
request,
|
||||
f'👥 {group.cn}: {len(members)} участников, '
|
||||
f'входит в {len(member_of)} групп | DN: {group.dn}',
|
||||
level=messages.INFO,
|
||||
)
|
||||
show_group_info.short_description = '📋 Показать информацию'
|
||||
|
||||
def delete_selected_ldap(self, request, queryset):
|
||||
"""Удаляет выбранные LDAP группы."""
|
||||
groups = self._resolve_ldap_groups(request)
|
||||
if not groups:
|
||||
self.message_user(
|
||||
request, 'Не удалось найти выбранные группы.',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
return
|
||||
|
||||
deleted = 0
|
||||
for group in groups:
|
||||
try:
|
||||
group.delete()
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Ошибка удаления {group.cn}: {e}',
|
||||
level=messages.ERROR,
|
||||
)
|
||||
if deleted:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Удалено: {deleted} групп из LDAP.',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
delete_selected_ldap.short_description = '🗑️ Удалить из LDAP'
|
||||
|
||||
def sync_groups_from_ldap(self, request, queryset):
|
||||
"""Синхронизирует ВСЕ группы из OU=Groups в Django Group."""
|
||||
from employees.ldap.services.group_service import GroupService
|
||||
|
||||
svc = GroupService()
|
||||
try:
|
||||
created = svc.sync_catalog(throttle_seconds=0)
|
||||
self.message_user(
|
||||
request,
|
||||
f'🔄 LDAP → Django: создано {created} новых Django Group',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
except Exception as e:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Ошибка синхронизации: {e}',
|
||||
level=messages.ERROR,
|
||||
)
|
||||
sync_groups_from_ldap.short_description = '⬇️ LDAP → Django (создать Django Group)'
|
||||
|
||||
def sync_groups_to_ldap(self, request, queryset):
|
||||
"""Синхронизирует Django Groups → LDAP (создаёт отсутствующие, проверяя LDAP)."""
|
||||
from django.contrib.auth.models import Group as DjangoGroup
|
||||
from employees.ldap.services.group_service import GroupService
|
||||
|
||||
svc = GroupService()
|
||||
groups = self._resolve_ldap_groups(request)
|
||||
|
||||
if not groups:
|
||||
# Если ничего не выбрано — синхронизируем все Django Groups
|
||||
django_groups = DjangoGroup.objects.all()
|
||||
created = 0
|
||||
already = 0
|
||||
for dg in django_groups:
|
||||
# Реальная проверка наличия в LDAP
|
||||
exists_in_ldap = LdapGroup.objects.filter(cn=dg.name).exists()
|
||||
if exists_in_ldap:
|
||||
ldap_grp = LdapGroup.objects.filter(cn=dg.name).first()
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='group',
|
||||
object_pk=str(dg.pk),
|
||||
defaults={
|
||||
'ldap_dn': ldap_grp.dn,
|
||||
'last_sync_dir': 'ldap',
|
||||
},
|
||||
)
|
||||
already += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
dn = svc.create(cn=dg.name)
|
||||
LdapSyncState.objects.update_or_create(
|
||||
model='group',
|
||||
object_pk=str(dg.pk),
|
||||
defaults={
|
||||
'ldap_dn': dn,
|
||||
'last_sync_dir': 'django',
|
||||
},
|
||||
)
|
||||
created += 1
|
||||
except Exception as e:
|
||||
self.message_user(
|
||||
request,
|
||||
f'⚠️ {dg.name}: {e}',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'🔄 Django → LDAP: создано {created}, уже существовало {already}',
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
else:
|
||||
# Показываем info о выбранных LDAP-группах
|
||||
for group in groups:
|
||||
sync = LdapSyncState.objects.filter(
|
||||
model='group', ldap_dn=group.dn,
|
||||
).first()
|
||||
if sync:
|
||||
self.message_user(
|
||||
request,
|
||||
f'✅ {group.cn}: связана с Django Group pk={sync.object_pk}',
|
||||
level=messages.INFO,
|
||||
)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
f'⚠️ {group.cn}: нет связи с Django Group',
|
||||
level=messages.WARNING,
|
||||
)
|
||||
sync_groups_to_ldap.short_description = '⬆️ Django → LDAP (создать LDAP Group)'
|
||||
|
||||
# --- Кастомные поля для отображения ---
|
||||
|
||||
def group_display(self, obj):
|
||||
|
||||
@ -99,7 +99,7 @@ base_dn используется ТОЛЬКО для SUBTREE поиска
|
||||
и НЕ определяет местоположение создаваемых/перемещаемых объектов.
|
||||
|
||||
- LdapUser.base_dn = "OU=company,DC=..." — покрывает все отделы и Dismissed
|
||||
- LdapGroup.base_dn = "DC=..." — покрывает Groups и роли отделов
|
||||
- LdapGroup.base_dn = "OU=Groups,..." — покрывает только глобальные группы
|
||||
- LdapOrganizationalUnit.base_dn = "OU=Departments,..." — покрывает OU отделов
|
||||
"""
|
||||
|
||||
@ -175,15 +175,11 @@ def get_users_base():
|
||||
def get_base_dn():
|
||||
"""Получает корневой base DN из settings.
|
||||
|
||||
Используется для групп. Должен покрывать:
|
||||
- OU=Groups (глобальные группы)
|
||||
- OU=Departments (роли отделов)
|
||||
- Любые другие контейнеры
|
||||
Используется для операций, которым нужен охват всего домена.
|
||||
|
||||
Приоритет:
|
||||
1. LDAP_BASE_DN (если задан)
|
||||
2. Fallback: корневой DN домена (DC=robotail,DC=local)
|
||||
или LDAP_USERS_BASE если нет LDAP_BASE_DN
|
||||
2. Fallback: LDAP_USERS_BASE или DC=robotail,DC=local
|
||||
"""
|
||||
return getattr(
|
||||
settings, 'LDAP_BASE_DN',
|
||||
@ -192,6 +188,22 @@ def get_base_dn():
|
||||
)
|
||||
|
||||
|
||||
def get_groups_base():
|
||||
"""Получает base DN для глобальных групп из settings.
|
||||
|
||||
Глобальные группы располагаются в:
|
||||
CN=<GroupName>,OU=Groups,OU=company,DC=robotail,DC=local
|
||||
|
||||
Приоритет:
|
||||
1. LDAP_GROUPS_BASE (если задан)
|
||||
2. Fallback: OU=Groups,DC=robotail,DC=local
|
||||
"""
|
||||
return getattr(
|
||||
settings, 'LDAP_GROUPS_BASE',
|
||||
'OU=Groups,DC=robotail,DC=local',
|
||||
)
|
||||
|
||||
|
||||
def get_departments_base():
|
||||
"""Получает base DN для отделов (OU) из settings.
|
||||
|
||||
@ -297,7 +309,7 @@ class LdapUser(LdapSyncStateMixin, ModifyDnMixin, LdapModel):
|
||||
|
||||
|
||||
class LdapGroup(ModifyDnMixin, LdapModel):
|
||||
"""LDAP модель для группы Active Directory.
|
||||
"""LDAP модель для глобальных групп Active Directory.
|
||||
|
||||
Использует objectClass: top, group.
|
||||
Только для WRITE операций (POST/PUT/DELETE).
|
||||
@ -306,11 +318,9 @@ class LdapGroup(ModifyDnMixin, LdapModel):
|
||||
- ModifyDnMixin: поддержка перемещения между OU через base_dn изменение
|
||||
|
||||
ВАЖНО О base_dn:
|
||||
- base_dn используется только для SUBTREE поиска через .objects.filter()
|
||||
- Группы могут находиться в разных OU:
|
||||
• CN=Group,OU=Groups,DC=... (глобальные группы)
|
||||
• CN=DEP_*,OU=<Dept>,OU=Departments,... (группы отделов)
|
||||
• CN=ROLE_*,OU=<Dept>,OU=Departments,... (роли отделов)
|
||||
- base_dn = LDAP_GROUPS_BASE — сканирует ТОЛЬКО OU=Groups
|
||||
- Для групп отделов (DEP_*): используйте LdapOrganizationalUnitGroup
|
||||
- Для должностей (POS_*): используйте PositionService
|
||||
- При изменении base_dn ModifyDnMixin автоматически выполнит modify_dn!
|
||||
|
||||
Операции через django-ldapdb:
|
||||
@ -320,14 +330,15 @@ class LdapGroup(ModifyDnMixin, LdapModel):
|
||||
- RENAME: переименование через GroupService.rename() (low-level ldap3)
|
||||
- DELETE: удаление через .delete() (ORM)
|
||||
|
||||
Типы групп в LDAP:
|
||||
- Глобальные группы: CN=<Name>,OU=Groups,...
|
||||
- Группы отделов: CN=DEP_<DeptName>,OU=<DeptName>,OU=Departments,...
|
||||
- Роли отделов: CN=ROLE_{name},OU={dept},OU=Departments,...
|
||||
Расположение групп по типам:
|
||||
- Глобальные: CN=<Name>,OU=Groups,... ← эта модель
|
||||
- Отделы: CN=DEP_*,OU=<Dept>,OU=Departments ← LdapOrganizationalUnitGroup
|
||||
- Должности: CN=POS_*,OU=Positions,... ← PositionService
|
||||
- Роли: CN=ROLE_*,OU=<Dept>,OU=Departments ← через GroupService
|
||||
"""
|
||||
|
||||
# Базовая конфигурация
|
||||
base_dn = get_base_dn()
|
||||
# Базовая конфигурация: только OU=Groups
|
||||
base_dn = get_groups_base()
|
||||
object_classes = ['top', 'group']
|
||||
|
||||
# RDN атрибут для построения Distinguished Name
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user