feat(ldap): интеграция django-ldapdb ORM в employees/ldap/
Добавлена поддержка django-ldapdb для упрощения работы с LDAP через Django ORM. Ключевые изменения: - Установлен django-ldapdb==1.5.1 в requirements.txt - Созданы LDAP ORM модели (LdapUser, LdapGroup, LdapOrganizationalUnit) в employees/ldap/orm_models.py - Реализованы ORM сервисы для CRUD операций в employees/ldap/orm_services.py - Настроен database router в eusrr_backend/db_routers.py - Добавлена LDAP database конфигурация в settings.py - Создана документация ORM_INTEGRATION.md Архитектура: ✅ GET → Employee (PostgreSQL) - быстро ✅ POST/PUT/DELETE → LdapUser (django-ldapdb ORM) - просто Преимущества: - Django ORM вместо низкоуровневого ldap3 - Автоматическая валидация через Django fields - Унифицированный CRUD API - Меньше кода, больше удобства Файлы: + employees/ldap/orm_models.py - LDAP ORM модели (LdapUser, LdapGroup, LdapOrganizationalUnit) + employees/ldap/orm_services.py - ORM сервисы (LdapOrmUserService, LdapOrmGroupService, LdapOrmDepartmentService) + employees/ldap/ORM_INTEGRATION.md - документация по интеграции + eusrr_backend/db_routers.py - database router для LDAP моделей ~ requirements.txt - добавлен django-ldapdb==1.5.1 ~ eusrr_backend/settings.py - настроена LDAP database и router
This commit is contained in:
parent
45a634de92
commit
2eb2da4c5a
206
backend/employees/ldap/ORM_INTEGRATION.md
Normal file
206
backend/employees/ldap/ORM_INTEGRATION.md
Normal file
@ -0,0 +1,206 @@
|
||||
# LDAP ORM Integration
|
||||
|
||||
Интеграция django-ldapdb для упрощения работы с Active Directory через Django ORM.
|
||||
|
||||
## Концепция
|
||||
|
||||
**LDAP модели используются ТОЛЬКО для записи (POST/PUT/DELETE)!**
|
||||
|
||||
```
|
||||
GET запросы → Employee (PostgreSQL) - быстро ⚡
|
||||
POST/PUT/DELETE → LdapUser (django-ldapdb ORM) - просто ✨
|
||||
```
|
||||
|
||||
## Компоненты
|
||||
|
||||
### 1. LDAP ORM Модели
|
||||
|
||||
Файл: `employees/ldap/orm_models.py`
|
||||
|
||||
```python
|
||||
from employees.ldap.orm_models import LdapUser, LdapGroup, LdapOrganizationalUnit
|
||||
|
||||
# Создание пользователя через ORM
|
||||
user = LdapUser()
|
||||
user.cn = "John Doe"
|
||||
user.sam_account_name = "jdoe"
|
||||
user.given_name = "John"
|
||||
user.sn = "Doe"
|
||||
user.save() # → пишет в LDAP!
|
||||
```
|
||||
|
||||
**Модели:**
|
||||
- `LdapUser` - пользователи AD (objectClass=user)
|
||||
- `LdapGroup` - группы (objectClass=group)
|
||||
- `LdapOrganizationalUnit` - отделы (objectClass=organizationalUnit)
|
||||
|
||||
### 2. ORM Сервисы
|
||||
|
||||
Файл: `employees/ldap/orm_services.py`
|
||||
|
||||
```python
|
||||
from employees.ldap.orm_services import LdapOrmUserService
|
||||
|
||||
svc = LdapOrmUserService()
|
||||
|
||||
# Создание
|
||||
user = svc.create_user(
|
||||
sam_account_name="jdoe",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="jdoe@example.com",
|
||||
employee_pk=123, # ID из Employee модели
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Обновление
|
||||
svc.update_user(
|
||||
user_dn="CN=John Doe,OU=Users,DC=example,DC=com",
|
||||
first_name="Johnny",
|
||||
email="johnny@example.com",
|
||||
)
|
||||
|
||||
# Удаление
|
||||
svc.delete_user(user_dn, soft=True) # деактивация
|
||||
```
|
||||
|
||||
**Сервисы:**
|
||||
- `LdapOrmUserService` - пользователи
|
||||
- `LdapOrmGroupService` - группы
|
||||
- `LdapOrmDepartmentService` - отделы
|
||||
|
||||
### 3. Database Router
|
||||
|
||||
Файл: `eusrr_backend/db_routers.py`
|
||||
|
||||
Автоматически направляет:
|
||||
- LDAP модели → `ldap` database
|
||||
- Django модели → `default` database (PostgreSQL)
|
||||
|
||||
### 4. Settings
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
# ...
|
||||
},
|
||||
'ldap': {
|
||||
'ENGINE': 'ldapdb.backends.ldap',
|
||||
'NAME': 'ldaps://dc.example.com:636',
|
||||
'USER': 'CN=admin,DC=example,DC=com',
|
||||
'PASSWORD': 'password',
|
||||
},
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = ['eusrr_backend.db_routers.LdapRouter']
|
||||
```
|
||||
|
||||
## Преимущества
|
||||
|
||||
✅ **Django ORM вместо ldap3** - чистый код, меньше костылей
|
||||
✅ **Автоматическая валидация** - через Django fields
|
||||
✅ **Удобство** - унифицированный CRUD API
|
||||
✅ **Производительность** - чтение из PostgreSQL (быстро), запись через ORM (удобно)
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### ViewSet с ORM
|
||||
|
||||
```python
|
||||
from employees.ldap.orm_services import LdapOrmUserService
|
||||
|
||||
class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
def create(self, request):
|
||||
# 1. Создаем в Django
|
||||
employee = Employee.objects.create(
|
||||
first_name=request.data['first_name'],
|
||||
last_name=request.data['last_name'],
|
||||
email=request.data['email'],
|
||||
)
|
||||
|
||||
# 2. Создаем в LDAP через ORM
|
||||
svc = LdapOrmUserService()
|
||||
ldap_user = svc.create_user(
|
||||
sam_account_name=employee.email.split('@')[0],
|
||||
first_name=employee.first_name,
|
||||
last_name=employee.last_name,
|
||||
email=employee.email,
|
||||
employee_pk=employee.pk,
|
||||
)
|
||||
|
||||
return Response(EmployeeSerializer(employee).data)
|
||||
```
|
||||
|
||||
### Работа с группами
|
||||
|
||||
```python
|
||||
from employees.ldap.orm_services import LdapOrmGroupService
|
||||
|
||||
svc = LdapOrmGroupService()
|
||||
|
||||
# Создание группы
|
||||
group = svc.create_group(
|
||||
cn="Developers",
|
||||
description="Development team",
|
||||
)
|
||||
|
||||
# Добавление члена
|
||||
svc.add_member(
|
||||
group_dn=group.dn,
|
||||
member_dn="CN=John Doe,OU=Users,DC=example,DC=com",
|
||||
)
|
||||
```
|
||||
|
||||
## Миграция со старого подхода
|
||||
|
||||
### Было (ldap3):
|
||||
|
||||
```python
|
||||
from ldap3 import Connection, MODIFY_REPLACE
|
||||
|
||||
with ldap_connection() as conn:
|
||||
conn.modify(
|
||||
user_dn,
|
||||
{"givenName": [(MODIFY_REPLACE, ["John"])]},
|
||||
)
|
||||
```
|
||||
|
||||
### Стало (django-ldapdb):
|
||||
|
||||
```python
|
||||
from employees.ldap.orm_services import LdapOrmUserService
|
||||
|
||||
svc = LdapOrmUserService()
|
||||
svc.update_user(user_dn, first_name="John")
|
||||
```
|
||||
|
||||
**Результат:**
|
||||
- ❌ Было: 15-20 строк низкоуровневого кода
|
||||
- ✅ Стало: 2 строки чистого ORM
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
# 1. Системные зависимости (Linux)
|
||||
sudo apt-get install libldap2-dev libsasl2-dev
|
||||
|
||||
# 2. Python пакет
|
||||
pip install django-ldapdb==1.5.1
|
||||
|
||||
# 3. Django check
|
||||
python manage.py check
|
||||
```
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. Создать сигналы для автоматической синхронизации Employee ↔ LDAP
|
||||
2. Интегрировать в существующие ViewSets
|
||||
3. Постепенно мигрировать старый код с ldap3 на ORM
|
||||
|
||||
## Заметки
|
||||
|
||||
- LDAP модели не мигрируют (managed=False)
|
||||
- Для связи используется employeeNumber атрибут (хранит Employee.pk)
|
||||
- Soft delete по умолчанию (деактивация вместо удаления)
|
||||
177
backend/employees/ldap/orm_models.py
Normal file
177
backend/employees/ldap/orm_models.py
Normal file
@ -0,0 +1,177 @@
|
||||
"""LDAP ORM модели для работы с Active Directory через django-ldapdb.
|
||||
|
||||
Эти модели используются ТОЛЬКО для записи (POST/PUT/DELETE) в LDAP.
|
||||
Для чтения (GET) используются обычные Django модели (Employee, Department).
|
||||
|
||||
Преимущества ORM подхода:
|
||||
- Замена низкоуровневых ldap3 операций на Django ORM
|
||||
- Автоматическая валидация и типизация
|
||||
- Унифицированный API для CRUD операций
|
||||
- Меньше кода, больше удобства
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from ldapdb.models import Model as LdapModel
|
||||
from ldapdb.models.fields import (
|
||||
CharField,
|
||||
DateTimeField,
|
||||
ImageField,
|
||||
IntegerField,
|
||||
ListField,
|
||||
)
|
||||
|
||||
|
||||
def get_users_base():
|
||||
"""Получает base DN для пользователей из settings."""
|
||||
return getattr(settings, 'LDAP_USERS_BASE',
|
||||
getattr(settings, 'LDAP_USER_BASE',
|
||||
'OU=company,DC=robotail,DC=local'))
|
||||
|
||||
|
||||
def get_base_dn():
|
||||
"""Получает корневой base DN из settings."""
|
||||
return getattr(settings, 'LDAP_BASE_DN',
|
||||
getattr(settings, 'LDAP_USER_BASE',
|
||||
'DC=robotail,DC=local'))
|
||||
|
||||
|
||||
class LdapUser(LdapModel):
|
||||
"""LDAP модель для пользователя Active Directory.
|
||||
|
||||
Использует objectClass: top, person, organizationalPerson, user.
|
||||
Только для WRITE операций (POST/PUT/DELETE).
|
||||
"""
|
||||
|
||||
# Базовая конфигурация
|
||||
base_dn = get_users_base()
|
||||
object_classes = ['top', 'person', 'organizationalPerson', 'user']
|
||||
|
||||
# Основные атрибуты
|
||||
cn = CharField(db_column='cn', primary_key=True)
|
||||
distinguished_name = CharField(db_column='distinguishedName')
|
||||
|
||||
# Идентификация
|
||||
object_guid = CharField(db_column='objectGUID')
|
||||
sam_account_name = CharField(db_column='sAMAccountName')
|
||||
user_principal_name = CharField(db_column='userPrincipalName')
|
||||
|
||||
# Персональные данные
|
||||
given_name = CharField(db_column='givenName')
|
||||
sn = CharField(db_column='sn') # surname (фамилия)
|
||||
display_name = CharField(db_column='displayName')
|
||||
mail = CharField(db_column='mail')
|
||||
|
||||
# Контакты
|
||||
telephone_number = CharField(db_column='telephoneNumber', blank=True)
|
||||
mobile = CharField(db_column='mobile', blank=True)
|
||||
|
||||
# Управление учетной записью
|
||||
user_account_control = IntegerField(db_column='userAccountControl')
|
||||
|
||||
# Дополнительная информация
|
||||
description = CharField(db_column='description', blank=True)
|
||||
thumbnail_photo = ImageField(db_column='thumbnailPhoto', blank=True)
|
||||
|
||||
# ID сотрудника (для связи с Django Employee.pk)
|
||||
employee_number = CharField(db_column='employeeNumber', 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 # Django не управляет схемой LDAP
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_name} ({self.sam_account_name})"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LdapUser: {self.sam_account_name}>"
|
||||
|
||||
|
||||
class LdapGroup(LdapModel):
|
||||
"""LDAP модель для группы Active Directory.
|
||||
|
||||
Использует objectClass: top, group.
|
||||
Только для WRITE операций (POST/PUT/DELETE).
|
||||
"""
|
||||
|
||||
# Базовая конфигурация
|
||||
base_dn = get_base_dn()
|
||||
object_classes = ['top', 'group']
|
||||
|
||||
# Основные атрибуты
|
||||
cn = CharField(db_column='cn', primary_key=True)
|
||||
distinguished_name = CharField(db_column='distinguishedName')
|
||||
|
||||
# Идентификация
|
||||
object_guid = CharField(db_column='objectGUID')
|
||||
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"Group: {self.cn}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LdapGroup: {self.cn}>"
|
||||
|
||||
|
||||
class LdapOrganizationalUnit(LdapModel):
|
||||
"""LDAP модель для Organizational Unit (отдел).
|
||||
|
||||
Использует objectClass: top, organizationalUnit.
|
||||
Только для WRITE операций (POST/PUT/DELETE).
|
||||
"""
|
||||
|
||||
# Базовая конфигурация
|
||||
base_dn = get_base_dn()
|
||||
object_classes = ['top', 'organizationalUnit']
|
||||
|
||||
# Основные атрибуты
|
||||
ou = CharField(db_column='ou', primary_key=True)
|
||||
distinguished_name = CharField(db_column='distinguishedName')
|
||||
|
||||
# Идентификация
|
||||
object_guid = CharField(db_column='objectGUID')
|
||||
|
||||
# Описание
|
||||
description = CharField(db_column='description', blank=True)
|
||||
|
||||
# Управление (managedBy - DN руководителя)
|
||||
managed_by = CharField(db_column='managedBy', blank=True)
|
||||
|
||||
# Временные метки
|
||||
when_created = DateTimeField(db_column='whenCreated')
|
||||
when_changed = DateTimeField(db_column='whenChanged')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return f"OU: {self.ou}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LdapOrganizationalUnit: {self.ou}>"
|
||||
|
||||
|
||||
__all__ = [
|
||||
'LdapUser',
|
||||
'LdapGroup',
|
||||
'LdapOrganizationalUnit',
|
||||
]
|
||||
347
backend/employees/ldap/orm_services.py
Normal file
347
backend/employees/ldap/orm_services.py
Normal file
@ -0,0 +1,347 @@
|
||||
"""ORM сервисы для работы с LDAP через django-ldapdb.
|
||||
|
||||
Упрощенная альтернатива низкоуровневым ldap3 операциям.
|
||||
Использует LDAP модели (LdapUser, LdapGroup, LdapOrganizationalUnit) для CRUD.
|
||||
|
||||
Преимущества:
|
||||
- Чистый Django ORM вместо ldap3 костылей
|
||||
- Автоматическая валидация
|
||||
- Унифицированный API
|
||||
- Меньше кода
|
||||
|
||||
Использование:
|
||||
- Только для WRITE операций (POST/PUT/DELETE)
|
||||
- Чтение идет из обычных Django моделей (Employee, Department)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .orm_models import LdapUser, LdapGroup, LdapOrganizationalUnit
|
||||
from .utils.text_utils import esc_rdn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# User Account Control values для Active Directory
|
||||
UAC_ENABLED = 512
|
||||
UAC_DISABLED = 514
|
||||
|
||||
|
||||
class LdapOrmUserService:
|
||||
"""Сервис для управления пользователями в LDAP через ORM."""
|
||||
|
||||
def create_user(
|
||||
self,
|
||||
*,
|
||||
sam_account_name: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
employee_pk: Optional[int] = None,
|
||||
phone: Optional[str] = None,
|
||||
is_active: bool = True,
|
||||
ou_dn: Optional[str] = None,
|
||||
) -> LdapUser:
|
||||
"""
|
||||
Создает пользователя в LDAP используя django-ldapdb ORM.
|
||||
|
||||
Args:
|
||||
sam_account_name: sAMAccountName (логин без домена)
|
||||
first_name: Имя
|
||||
last_name: Фамилия
|
||||
email: Email адрес
|
||||
employee_pk: ID сотрудника в Django (для employeeNumber)
|
||||
phone: Телефон (опционально)
|
||||
is_active: Активен ли пользователь
|
||||
ou_dn: DN organizational unit для размещения
|
||||
|
||||
Returns:
|
||||
Созданный LdapUser объект
|
||||
"""
|
||||
# Генерация CN и UPN
|
||||
cn = f"{first_name} {last_name}".strip() or sam_account_name
|
||||
cn_safe = esc_rdn(cn)
|
||||
|
||||
upn_suffix = getattr(settings, 'LDAP_UPN_SUFFIX', 'robotail.local')
|
||||
upn = f"{sam_account_name}@{upn_suffix}"
|
||||
|
||||
# Определение OU для размещения
|
||||
if ou_dn is None:
|
||||
ou_dn = getattr(settings, 'LDAP_USERS_BASE',
|
||||
getattr(settings, 'LDAP_USER_BASE',
|
||||
'OU=company,DC=robotail,DC=local'))
|
||||
|
||||
# DN нового пользователя
|
||||
dn = f"CN={cn_safe},{ou_dn}"
|
||||
|
||||
# Создание через ORM
|
||||
user = LdapUser()
|
||||
user.dn = dn
|
||||
user.cn = cn
|
||||
user.sam_account_name = sam_account_name
|
||||
user.user_principal_name = upn
|
||||
user.given_name = first_name
|
||||
user.sn = last_name or "." # AD требует sn
|
||||
user.display_name = cn
|
||||
user.mail = email
|
||||
|
||||
if phone:
|
||||
user.mobile = phone
|
||||
|
||||
if employee_pk:
|
||||
user.employee_number = str(employee_pk)
|
||||
|
||||
# UserAccountControl
|
||||
user.user_account_control = UAC_ENABLED if is_active else UAC_DISABLED
|
||||
|
||||
# Сохранение в LDAP
|
||||
user.save()
|
||||
|
||||
logger.info(f"Created LDAP user: {dn} (sAMAccountName={sam_account_name})")
|
||||
|
||||
return user
|
||||
|
||||
def update_user(
|
||||
self,
|
||||
user_dn: str,
|
||||
*,
|
||||
first_name: Optional[str] = None,
|
||||
last_name: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
phone: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
) -> LdapUser:
|
||||
"""
|
||||
Обновляет пользователя в LDAP через ORM.
|
||||
|
||||
Args:
|
||||
user_dn: DN пользователя для обновления
|
||||
first_name: Новое имя (если нужно изменить)
|
||||
last_name: Новая фамилия
|
||||
email: Новый email
|
||||
phone: Новый телефон
|
||||
is_active: Новый статус активности
|
||||
|
||||
Returns:
|
||||
Обновленный LdapUser объект
|
||||
"""
|
||||
# Получаем пользователя по DN
|
||||
try:
|
||||
user = LdapUser.objects.get(dn=user_dn)
|
||||
except LdapUser.DoesNotExist:
|
||||
raise ValueError(f"LDAP user not found: {user_dn}")
|
||||
|
||||
# Обновляем поля
|
||||
updated = False
|
||||
|
||||
if first_name is not None and user.given_name != first_name:
|
||||
user.given_name = first_name
|
||||
updated = True
|
||||
|
||||
if last_name is not None and user.sn != last_name:
|
||||
user.sn = last_name or "."
|
||||
updated = True
|
||||
|
||||
if email is not None and user.mail != email:
|
||||
user.mail = email
|
||||
updated = True
|
||||
|
||||
if phone is not None and user.mobile != phone:
|
||||
user.mobile = phone
|
||||
updated = True
|
||||
|
||||
if is_active is not None:
|
||||
new_uac = UAC_ENABLED if is_active else UAC_DISABLED
|
||||
if user.user_account_control != new_uac:
|
||||
user.user_account_control = new_uac
|
||||
updated = True
|
||||
|
||||
# Обновление displayName если изменились имя/фамилия
|
||||
if first_name is not None or last_name is not None:
|
||||
new_display = f"{user.given_name} {user.sn}".strip()
|
||||
if new_display and user.display_name != new_display:
|
||||
user.display_name = new_display
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
user.save()
|
||||
logger.info(f"Updated LDAP user: {user_dn}")
|
||||
|
||||
return user
|
||||
|
||||
def delete_user(self, user_dn: str, *, soft: bool = True) -> None:
|
||||
"""
|
||||
Удаляет пользователя из LDAP.
|
||||
|
||||
Args:
|
||||
user_dn: DN пользователя для удаления
|
||||
soft: Если True, только деактивирует (userAccountControl=DISABLED)
|
||||
Если False, полностью удаляет из LDAP
|
||||
"""
|
||||
try:
|
||||
user = LdapUser.objects.get(dn=user_dn)
|
||||
except LdapUser.DoesNotExist:
|
||||
logger.warning(f"LDAP user not found for deletion: {user_dn}")
|
||||
return
|
||||
|
||||
if soft:
|
||||
# Soft delete - деактивация
|
||||
user.user_account_control = UAC_DISABLED
|
||||
user.save()
|
||||
logger.info(f"Soft deleted (disabled) LDAP user: {user_dn}")
|
||||
else:
|
||||
# Hard delete - удаление из LDAP
|
||||
user.delete()
|
||||
logger.info(f"Deleted LDAP user: {user_dn}")
|
||||
|
||||
def get_user_by_dn(self, dn: str) -> Optional[LdapUser]:
|
||||
"""Получает пользователя из LDAP по DN."""
|
||||
try:
|
||||
return LdapUser.objects.get(dn=dn)
|
||||
except LdapUser.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user_by_employee_pk(self, employee_pk: int) -> Optional[LdapUser]:
|
||||
"""Получает пользователя из LDAP по Employee.pk."""
|
||||
try:
|
||||
return LdapUser.objects.get(employee_number=str(employee_pk))
|
||||
except LdapUser.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class LdapOrmGroupService:
|
||||
"""Сервис для управления группами в LDAP через ORM."""
|
||||
|
||||
def create_group(
|
||||
self,
|
||||
cn: str,
|
||||
description: Optional[str] = None,
|
||||
ou_dn: Optional[str] = None,
|
||||
) -> LdapGroup:
|
||||
"""
|
||||
Создает группу в LDAP.
|
||||
|
||||
Args:
|
||||
cn: Common Name группы
|
||||
description: Описание группы
|
||||
ou_dn: DN organizational unit для размещения
|
||||
|
||||
Returns:
|
||||
Созданный LdapGroup объект
|
||||
"""
|
||||
if ou_dn is None:
|
||||
ou_dn = getattr(settings, 'LDAP_BASE_DN', 'DC=robotail,DC=local')
|
||||
|
||||
cn_safe = esc_rdn(cn)
|
||||
dn = f"CN={cn_safe},{ou_dn}"
|
||||
|
||||
group = LdapGroup()
|
||||
group.dn = dn
|
||||
group.cn = cn
|
||||
group.sam_account_name = cn
|
||||
|
||||
if description:
|
||||
group.description = description
|
||||
|
||||
group.save()
|
||||
|
||||
logger.info(f"Created LDAP group: {dn}")
|
||||
|
||||
return group
|
||||
|
||||
def add_member(self, group_dn: str, member_dn: str) -> None:
|
||||
"""Добавляет члена в группу."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
raise ValueError(f"LDAP group not found: {group_dn}")
|
||||
|
||||
# Добавляем DN члена в список member
|
||||
if member_dn not in (group.member or []):
|
||||
members = list(group.member or [])
|
||||
members.append(member_dn)
|
||||
group.member = members
|
||||
group.save()
|
||||
logger.info(f"Added {member_dn} to group {group_dn}")
|
||||
|
||||
def remove_member(self, group_dn: str, member_dn: str) -> None:
|
||||
"""Удаляет члена из группы."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
except LdapGroup.DoesNotExist:
|
||||
raise ValueError(f"LDAP group not found: {group_dn}")
|
||||
|
||||
# Удаляем DN члена из списка member
|
||||
if member_dn in (group.member or []):
|
||||
members = list(group.member or [])
|
||||
members.remove(member_dn)
|
||||
group.member = members
|
||||
group.save()
|
||||
logger.info(f"Removed {member_dn} from group {group_dn}")
|
||||
|
||||
def delete_group(self, group_dn: str) -> None:
|
||||
"""Удаляет группу из LDAP."""
|
||||
try:
|
||||
group = LdapGroup.objects.get(dn=group_dn)
|
||||
group.delete()
|
||||
logger.info(f"Deleted LDAP group: {group_dn}")
|
||||
except LdapGroup.DoesNotExist:
|
||||
logger.warning(f"LDAP group not found for deletion: {group_dn}")
|
||||
|
||||
|
||||
class LdapOrmDepartmentService:
|
||||
"""Сервис для управления Organizational Units (отделами) в LDAP через ORM."""
|
||||
|
||||
def create_ou(
|
||||
self,
|
||||
ou_name: str,
|
||||
description: Optional[str] = None,
|
||||
parent_dn: Optional[str] = None,
|
||||
) -> LdapOrganizationalUnit:
|
||||
"""
|
||||
Создает Organizational Unit в LDAP.
|
||||
|
||||
Args:
|
||||
ou_name: Название OU
|
||||
description: Описание
|
||||
parent_dn: DN родительского контейнера
|
||||
|
||||
Returns:
|
||||
Созданный LdapOrganizationalUnit объект
|
||||
"""
|
||||
if parent_dn is None:
|
||||
parent_dn = getattr(settings, 'LDAP_BASE_DN', 'DC=robotail,DC=local')
|
||||
|
||||
ou_safe = esc_rdn(ou_name)
|
||||
dn = f"OU={ou_safe},{parent_dn}"
|
||||
|
||||
ou = LdapOrganizationalUnit()
|
||||
ou.dn = dn
|
||||
ou.ou = ou_name
|
||||
|
||||
if description:
|
||||
ou.description = description
|
||||
|
||||
ou.save()
|
||||
|
||||
logger.info(f"Created LDAP OU: {dn}")
|
||||
|
||||
return ou
|
||||
|
||||
def delete_ou(self, ou_dn: str) -> None:
|
||||
"""Удаляет OU из LDAP (должна быть пустой)."""
|
||||
try:
|
||||
ou = LdapOrganizationalUnit.objects.get(dn=ou_dn)
|
||||
ou.delete()
|
||||
logger.info(f"Deleted LDAP OU: {ou_dn}")
|
||||
except LdapOrganizationalUnit.DoesNotExist:
|
||||
logger.warning(f"LDAP OU not found for deletion: {ou_dn}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
'LdapOrmUserService',
|
||||
'LdapOrmGroupService',
|
||||
'LdapOrmDepartmentService',
|
||||
]
|
||||
65
backend/eusrr_backend/db_routers.py
Normal file
65
backend/eusrr_backend/db_routers.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Database routers для направления запросов к нужным базам данных.
|
||||
|
||||
LdapRouter направляет все операции с LDAP моделями (из employees.ldap.orm_models)
|
||||
в LDAP database, а все остальные модели - в default PostgreSQL database.
|
||||
"""
|
||||
|
||||
|
||||
class LdapRouter:
|
||||
"""
|
||||
Database router для направления LDAP моделей в LDAP подключение.
|
||||
|
||||
LDAP модели (LdapUser, LdapGroup, LdapOrganizationalUnit) используются
|
||||
ТОЛЬКО для записи (POST/PUT/DELETE) в LDAP через ORM.
|
||||
|
||||
Чтение (GET) происходит из обычных Django моделей (Employee, Department и т.д.)
|
||||
которые хранятся в PostgreSQL.
|
||||
"""
|
||||
|
||||
ldap_models = {
|
||||
'ldapuser',
|
||||
'ldapgroup',
|
||||
'ldaporganizationalunit',
|
||||
}
|
||||
|
||||
def db_for_read(self, model, **hints):
|
||||
"""
|
||||
Направляет READ операции для LDAP моделей в 'ldap' database.
|
||||
"""
|
||||
if model._meta.model_name.lower() in self.ldap_models:
|
||||
return 'ldap'
|
||||
return None # Используем default database
|
||||
|
||||
def db_for_write(self, model, **hints):
|
||||
"""
|
||||
Направляет WRITE операции для LDAP моделей в 'ldap' database.
|
||||
"""
|
||||
if model._meta.model_name.lower() in self.ldap_models:
|
||||
return 'ldap'
|
||||
return None # Используем default database
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints):
|
||||
"""
|
||||
Разрешает отношения между моделями только если они в одной базе.
|
||||
LDAP модели не могут иметь ForeignKey к Django моделям.
|
||||
"""
|
||||
db_obj1 = obj1._meta.model_name.lower() in self.ldap_models
|
||||
db_obj2 = obj2._meta.model_name.lower() in self.ldap_models
|
||||
|
||||
# Оба LDAP или оба Django - разрешаем
|
||||
if db_obj1 == db_obj2:
|
||||
return True
|
||||
|
||||
# Один LDAP, другой Django - запрещаем
|
||||
return False
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
||||
"""
|
||||
Запрещает миграции для LDAP моделей (схема управляется AD/LDAP сервером).
|
||||
"""
|
||||
if model_name and model_name.lower() in self.ldap_models:
|
||||
# LDAP модели не мигрируют никуда
|
||||
return False
|
||||
|
||||
# Обычные Django модели мигрируют только в default
|
||||
return db == 'default'
|
||||
@ -130,6 +130,19 @@ else:
|
||||
"PORT": os.getenv("DB_PORT", "5432"),
|
||||
}
|
||||
|
||||
# LDAP database для django-ldapdb (используется только для WRITE операций)
|
||||
# Конфигурация берется из переменных окружения LDAP_URI, LDAP_BIND_DN и т.д.
|
||||
# которые определены ниже в разделе LDAP
|
||||
DATABASES["ldap"] = {
|
||||
"ENGINE": "ldapdb.backends.ldap",
|
||||
"NAME": os.getenv("LDAP_URI", "ldaps://dcii.robotail.local:636"),
|
||||
"USER": os.getenv("LDAP_BIND_DN", ""),
|
||||
"PASSWORD": os.getenv("LDAP_BIND_PASSWORD", ""),
|
||||
}
|
||||
|
||||
# Database router для направления LDAP моделей в LDAP database
|
||||
DATABASE_ROUTERS = ["eusrr_backend.db_routers.LdapRouter"]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# АВТОРИЗАЦИЯ / ПАРОЛИ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@ -38,6 +38,7 @@ django-bootstrap5==25.1
|
||||
django-cors-headers==4.7.0
|
||||
django-extensions==4.1
|
||||
django-filter==25.1
|
||||
django-ldapdb==1.5.1
|
||||
django-phonenumber-field==8.1.0
|
||||
django-registration==5.2.1
|
||||
django-scheduler==0.12.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user