fix(ldap): исправлен primary_key в ORM моделях

ПРОБЛЕМА:
- cn/ou были установлены как primary_key в LdapUser/LdapGroup/LdapOrganizationalUnit
- CN/OU не уникальны глобально (могут быть в разных OU)
- .objects.get(cn=...) возвращал случайную группу при дубликатах
- Нарушался дизайн django-ldapdb, где dn - базовый primary_key

РЕШЕНИЕ:
- Убран primary_key=True из cn/ou полей
- Используется dn (Distinguished Name) как primary_key из базового класса
- DN - уникальный идентификатор объекта в LDAP, требуется для всех операций
- Удалено избыточное поле distinguished_name

ИЗМЕНЕНИЯ:
- orm_models.py: cn/ou теперь обычные CharField, dn наследуется из ldapdb.models.Model
- group_utils_orm.py: .get(cn=...) → .filter(cn=...).first() с предупреждением
- group_service.py: find_dn() и find_dn_wrapped() используют .filter().first()

ОБОСНОВАНИЕ ОТКАЗА ОТ objectGUID:
- objectGUID неизменяем и уникален, НО:
- LDAP протокол требует DN для всех операций (add/modify/delete/rename)
- django-ldapdb жёстко завязан на self.dn для save()/delete()
- GUID можно использовать только в search filter, результат всё равно возвращает DN
- DN - естественный primary key для LDAP объектов
This commit is contained in:
lizerk 2026-03-18 19:14:34 +03:00
parent 1b03ed8a7b
commit df93afbcb1
3 changed files with 33 additions and 21 deletions

View File

@ -175,8 +175,8 @@ class LdapUser(LdapModel):
object_classes = ['top', 'person', 'organizationalPerson', 'user']
# Основные атрибуты
cn = CharField(db_column='cn', primary_key=True)
distinguished_name = CharField(db_column='distinguishedName')
# dn наследуется от ldapdb.models.Model как primary_key (Distinguished Name)
cn = CharField(db_column='cn')
# Идентификация
object_guid = CharField(db_column='objectGUID')
@ -247,8 +247,8 @@ class LdapGroup(LdapModel):
object_classes = ['top', 'group']
# Основные атрибуты
cn = CharField(db_column='cn', primary_key=True)
distinguished_name = CharField(db_column='distinguishedName')
# dn наследуется от ldapdb.models.Model как primary_key (Distinguished Name)
cn = CharField(db_column='cn')
# Идентификация
object_guid = CharField(db_column='objectGUID')
@ -301,8 +301,8 @@ class LdapOrganizationalUnit(LdapModel):
object_classes = ['top', 'organizationalUnit']
# Основные атрибуты
ou = CharField(db_column='ou', primary_key=True)
distinguished_name = CharField(db_column='distinguishedName')
# dn наследуется от ldapdb.models.Model как primary_key (Distinguished Name)
ou = CharField(db_column='ou')
# Идентификация
object_guid = CharField(db_column='objectGUID')

View File

@ -218,12 +218,16 @@ class GroupService:
def find_dn(
self, conn: Connection, cn: str, bases: Optional[List[str]] = None
) -> Optional[str]:
"""Ищет DN группы по CN (ORM)."""
try:
group = LdapGroup.objects.get(cn=cn)
"""Ищет DN группы по CN (ORM).
Warning: Если есть несколько групп с одинаковым CN,
вернёт первую найденную.
"""
# .filter().first() вместо .get() т.к. cn больше не primary_key
group = LdapGroup.objects.filter(cn=cn).first()
if group:
return group.dn
except LdapGroup.DoesNotExist:
return None
return None
def groups_with_member(self, conn: Connection, member_dn: str) -> Set[str]:
"""Находит все группы, в которых состоит DN (ORM через memberOf)."""
@ -329,12 +333,16 @@ class GroupService:
def find_dn_wrapped(
self, cn: str, bases: Optional[list[str]] = None
) -> Optional[str]:
"""Ищет DN группы по CN (ORM)."""
try:
group = LdapGroup.objects.get(cn=cn)
"""Ищет DN группы по CN (ORM).
Warning: Если есть несколько групп с одинаковым CN,
вернёт первую найденную.
"""
# .filter().first() вместо .get() т.к. cn больше не primary_key
group = LdapGroup.objects.filter(cn=cn).first()
if group:
return group.dn
except LdapGroup.DoesNotExist:
return None
return None
def create_wrapped(
self,

View File

@ -55,16 +55,20 @@ def group_remove_member_orm(group_dn: str, member_dn: str) -> None:
def resolve_group_dns_by_cn_orm(cns: set[str]) -> dict[str, str]:
"""Ищет DN групп по их CN через ORM."""
"""Ищет DN групп по их CN через ORM.
Warning: Если есть несколько групп с одинаковым CN в разных OU,
вернёт первую найденную (может быть неправильной).
Рекомендуется использовать DN напрямую вместо CN.
"""
if not cns:
return {}
found: dict[str, str] = {}
for cn in sorted(cns):
try:
group = LdapGroup.objects.get(cn=cn)
# .filter().first() вместо .get() т.к. cn больше не primary_key
group = LdapGroup.objects.filter(cn=cn).first()
if group:
found[cn] = group.dn
except LdapGroup.DoesNotExist:
continue
return found