From df93afbcb114a3f475b4eb6d50afb5f39e56da30 Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 18 Mar 2026 19:14:34 +0300 Subject: [PATCH] =?UTF-8?q?fix(ldap):=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20primary=5Fkey=20=D0=B2=20ORM=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ПРОБЛЕМА: - 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 объектов --- backend/employees/ldap/orm_models.py | 12 ++++---- .../employees/ldap/services/group_service.py | 28 ++++++++++++------- .../employees/ldap/utils/group_utils_orm.py | 14 ++++++---- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/backend/employees/ldap/orm_models.py b/backend/employees/ldap/orm_models.py index 7f0ef88c..60967b25 100644 --- a/backend/employees/ldap/orm_models.py +++ b/backend/employees/ldap/orm_models.py @@ -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') diff --git a/backend/employees/ldap/services/group_service.py b/backend/employees/ldap/services/group_service.py index 374ca704..a92b178e 100644 --- a/backend/employees/ldap/services/group_service.py +++ b/backend/employees/ldap/services/group_service.py @@ -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, diff --git a/backend/employees/ldap/utils/group_utils_orm.py b/backend/employees/ldap/utils/group_utils_orm.py index 8594cc72..9c501af8 100644 --- a/backend/employees/ldap/utils/group_utils_orm.py +++ b/backend/employees/ldap/utils/group_utils_orm.py @@ -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