Fix LDAP synchronization issues, improve logging, and resolve binary decoding errors
This commit is contained in:
parent
06931f3706
commit
98e98289bf
@ -538,9 +538,12 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
Django считается источником истины - данные из БД будут
|
||||
записаны в Active Directory (создание или обновление).
|
||||
"""
|
||||
import logging
|
||||
from employees.ldap.services import UserService
|
||||
from employees.ldap.orm_models import LdapUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
success_count = 0
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
@ -549,46 +552,70 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
service = UserService()
|
||||
|
||||
for employee in queryset:
|
||||
logger.info(f"[sync_from_django_to_ldap] Начинаем обработку employee ID={employee.pk}, email={employee.email}")
|
||||
try:
|
||||
# Ищем существующего LDAP пользователя
|
||||
ldap_user = None
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: is_ldap_managed={employee.is_ldap_managed}")
|
||||
|
||||
if employee.is_ldap_managed:
|
||||
# Пробуем найти по employee_number
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: поиск в LDAP по employee_number={employee.pk}")
|
||||
ldap_user = LdapUser.objects.filter(
|
||||
employee_number=str(employee.pk)
|
||||
).first()
|
||||
if ldap_user:
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: найден LDAP user с DN={ldap_user.dn}")
|
||||
else:
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: не найден по employee_number")
|
||||
|
||||
if ldap_user:
|
||||
# ОБНОВЛЕНИЕ существующего LDAP пользователя
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: LDAP пользователь существует, начинаем проверку изменений")
|
||||
try:
|
||||
changes = {}
|
||||
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение given_name: LDAP='{ldap_user.given_name}' vs Django='{employee.first_name}'")
|
||||
if ldap_user.given_name != employee.first_name:
|
||||
changes['first_name'] = employee.first_name
|
||||
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение sn: LDAP='{ldap_user.sn}' vs Django='{employee.last_name}'")
|
||||
if ldap_user.sn != employee.last_name:
|
||||
changes['last_name'] = employee.last_name
|
||||
|
||||
logger.debug(f"[sync_from_django_to_ldap] Employee {employee.pk}: сравнение mail: LDAP='{ldap_user.mail}' vs Django='{employee.email}'")
|
||||
if ldap_user.mail != employee.email:
|
||||
changes['email'] = employee.email
|
||||
|
||||
ldap_phone = 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_to_ldap',
|
||||
'last_sync_dir': 'django',
|
||||
}
|
||||
)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: LdapSyncState обновлен")
|
||||
else:
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: изменений не обнаружено, уже синхронизирован")
|
||||
success_count += 1 # Уже синхронизирован
|
||||
except Exception as e:
|
||||
logger.exception(f"[sync_from_django_to_ldap] Employee {employee.pk}: ошибка обновления в LDAP")
|
||||
error_count += 1
|
||||
self.message_user(
|
||||
request,
|
||||
@ -597,20 +624,42 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
)
|
||||
else:
|
||||
# СОЗДАНИЕ нового LDAP пользователя
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: LDAP пользователь не найден, создаем новый")
|
||||
try:
|
||||
# Генерируем временный пароль
|
||||
import secrets
|
||||
from employees.ldap.domain.dtos import DirectoryUserDTO
|
||||
from employees.ldap.utils.phone_utils import normalize_phone
|
||||
|
||||
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")
|
||||
dto = DirectoryUserDTO(
|
||||
first_name=employee.first_name,
|
||||
last_name=employee.last_name,
|
||||
email=employee.email,
|
||||
phone_e164=phone_e164,
|
||||
department_dn=None,
|
||||
group_cns=[],
|
||||
initial_password=temp_password,
|
||||
)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: DTO создан: {dto}")
|
||||
|
||||
# Создаем через сервис
|
||||
ldap_user = service.create_user(
|
||||
employee=employee,
|
||||
password=temp_password,
|
||||
)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: вызов service.create_user(dto)")
|
||||
service.create_user(dto)
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: LDAP пользователь успешно создан")
|
||||
|
||||
created_count += 1
|
||||
employee.is_ldap_managed = True
|
||||
employee.save(update_fields=['is_ldap_managed'])
|
||||
logger.info(f"[sync_from_django_to_ldap] Employee {employee.pk}: is_ldap_managed установлен в True")
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
@ -619,6 +668,7 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
level=messages.SUCCESS
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"[sync_from_django_to_ldap] Employee {employee.pk}: ошибка создания в LDAP")
|
||||
error_count += 1
|
||||
self.message_user(
|
||||
request,
|
||||
@ -626,6 +676,7 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
level=messages.ERROR
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"[sync_from_django_to_ldap] Employee {employee.pk}: необработанное исключение на верхнем уровне")
|
||||
error_count += 1
|
||||
self.message_user(
|
||||
request,
|
||||
@ -728,7 +779,7 @@ class EmployeeAdmin(DjangoUserAdmin):
|
||||
defaults={
|
||||
'ldap_dn': ldap_user.dn,
|
||||
'ldap_guid': str(ldap_user.object_guid) if hasattr(ldap_user, 'object_guid') else None,
|
||||
'last_sync_dir': 'ldap_to_django',
|
||||
'last_sync_dir': 'ldap',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -15,12 +15,11 @@ class EmployeesConfig(AppConfig):
|
||||
def _patch_ldapdb_compiler(self):
|
||||
"""Патчит django-ldapdb для поддержки Value выражений.
|
||||
|
||||
Проблема: ldapdb компилятор на строке 246 вызывает e[0].field.attname,
|
||||
Проблема: ldapdb компилятор в results_iter вызывает e[0].field.attname,
|
||||
но у полей из Value() выражений (например, от .exists()) нет attname.
|
||||
|
||||
Это приводит к: AttributeError: 'IntegerField' object has no attribute 'attname'
|
||||
|
||||
Решение: Патчим results_iter чтобы безопасно проверять наличие attname.
|
||||
Решение: Патчим results_iter, заменяя прямые обращения к .attname
|
||||
на безопасный getattr(..., 'attname', None).
|
||||
"""
|
||||
try:
|
||||
from ldapdb.backends.ldap.compiler import SQLCompiler
|
||||
@ -29,35 +28,66 @@ class EmployeesConfig(AppConfig):
|
||||
|
||||
original_results_iter = SQLCompiler.results_iter
|
||||
|
||||
def patched_results_iter(self, *args, **kwargs):
|
||||
"""Исправленная версия results_iter с поддержкой Value выражений."""
|
||||
# Вызываем оригинальный генератор, но оборачиваем в try-catch
|
||||
for result in original_results_iter(self, *args, **kwargs):
|
||||
yield result
|
||||
|
||||
# Заменяем весь метод для правильной обработки
|
||||
import ldap
|
||||
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
|
||||
|
||||
def safe_results_iter(self, results=None, do_slice=True):
|
||||
"""Безопасная версия results_iter с проверкой attname."""
|
||||
if results is None:
|
||||
results = self.execute_sql(
|
||||
result_type='multi', chunked_fetch=False,
|
||||
chunk_size=GET_ITERATOR_CHUNK_SIZE,
|
||||
def safe_results_iter(self, results=None, tuple_expected=False,
|
||||
chunked_fetch=False, chunk_size=None):
|
||||
"""Обёртка над оригинальным results_iter с безопасным attname."""
|
||||
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 AttributeError as exc:
|
||||
if "attname" not in str(exc):
|
||||
raise
|
||||
# Fallback: повторяем логику оригинала с безопасным getattr
|
||||
yield from self._safe_results_iter_fallback()
|
||||
|
||||
def _safe_results_iter_fallback(self):
|
||||
"""Fallback-итератор с безопасными проверками attname."""
|
||||
import ldap as ldap_lib
|
||||
|
||||
if results is None:
|
||||
lookup = None
|
||||
try:
|
||||
from ldapdb.backends.ldap.compiler import query_as_ldap
|
||||
lookup = query_as_ldap(self.query, compiler=self, connection=self.connection)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
for dn, attrs in results:
|
||||
if lookup is None:
|
||||
return
|
||||
|
||||
if len(self.query.select):
|
||||
fields = [x.field for x in self.query.select]
|
||||
else:
|
||||
fields = self.query.model._meta.fields
|
||||
|
||||
attrlist = [x.db_column for x in fields if x.db_column]
|
||||
|
||||
try:
|
||||
vals = self.connection.search_s(
|
||||
base=lookup.base,
|
||||
scope=lookup.scope,
|
||||
filterstr=lookup.filterstr,
|
||||
attrlist=attrlist,
|
||||
)
|
||||
except ldap_lib.NO_SUCH_OBJECT:
|
||||
return
|
||||
|
||||
# Слайсинг
|
||||
pos = 0
|
||||
for dn, attrs in vals:
|
||||
if (self.query.low_mark and pos < self.query.low_mark) or \
|
||||
(self.query.high_mark is not None and pos >= self.query.high_mark):
|
||||
pos += 1
|
||||
continue
|
||||
|
||||
row = []
|
||||
self.setup_query()
|
||||
for e in self.select:
|
||||
if isinstance(e[0], aggregates.Count):
|
||||
value = 0
|
||||
input_field = e[0].get_source_expressions()[0].field
|
||||
# Безопасная проверка attname
|
||||
if getattr(input_field, 'attname', None) == 'dn':
|
||||
value = 1
|
||||
elif hasattr(input_field, 'from_ldap'):
|
||||
@ -70,7 +100,6 @@ class EmployeesConfig(AppConfig):
|
||||
value = len(result)
|
||||
row.append(value)
|
||||
else:
|
||||
# Безопасная проверка attname с getattr
|
||||
if getattr(e[0].field, 'attname', None) == 'dn':
|
||||
row.append(dn)
|
||||
elif hasattr(e[0].field, 'from_ldap'):
|
||||
@ -79,10 +108,11 @@ class EmployeesConfig(AppConfig):
|
||||
connection=self.connection))
|
||||
else:
|
||||
row.append(None)
|
||||
|
||||
yield row
|
||||
pos += 1
|
||||
|
||||
SQLCompiler.results_iter = safe_results_iter
|
||||
SQLCompiler._safe_results_iter_fallback = _safe_results_iter_fallback
|
||||
|
||||
except ImportError:
|
||||
# ldapdb не установлен - это нормально для тестов без LDAP
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
- Копирует данные из Active Directory в Django Employee
|
||||
- Используется когда LDAP содержит актуальные данные
|
||||
- Обновляет: first_name, last_name, email, phone_number
|
||||
- Устанавливает `last_sync_dir = 'ldap_to_django'`
|
||||
- Устанавливает `last_sync_dir = 'ldap'`
|
||||
|
||||
#### 🔄 **Django → LDAP** (Django как источник истины)
|
||||
```
|
||||
@ -41,7 +41,7 @@
|
||||
- Копирует данные из Django Employee в Active Directory
|
||||
- Используется когда БД содержит актуальные данные
|
||||
- Вызывает `UserService.update_user()` (с полной бизнес-логикой)
|
||||
- Устанавливает `last_sync_dir = 'django_to_ldap'`
|
||||
- Устанавливает `last_sync_dir = 'django'`
|
||||
|
||||
#### 🔍 **Показать различия**
|
||||
```
|
||||
|
||||
@ -212,11 +212,11 @@ class LdapUserAdmin(admin.ModelAdmin):
|
||||
object_pk=obj.employee_number
|
||||
)
|
||||
|
||||
if sync_state.last_sync_dir == 'django_to_ldap':
|
||||
if sync_state.last_sync_dir == 'django':
|
||||
icon = '⬆️'
|
||||
color = '#0066cc'
|
||||
text = 'Django → LDAP'
|
||||
elif sync_state.last_sync_dir == 'ldap_to_django':
|
||||
elif sync_state.last_sync_dir == 'ldap':
|
||||
icon = '⬇️'
|
||||
color = '#00cc66'
|
||||
text = 'LDAP → Django'
|
||||
@ -306,8 +306,6 @@ class LdapUserAdmin(admin.ModelAdmin):
|
||||
LDAP считается источником истины - данные из Active Directory
|
||||
перезапишут данные в Django Employee.
|
||||
"""
|
||||
from employees.ldap.services import UserService
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
@ -332,7 +330,7 @@ class LdapUserAdmin(admin.ModelAdmin):
|
||||
object_pk=str(emp.pk),
|
||||
defaults={
|
||||
'ldap_dn': ldap_user.dn,
|
||||
'last_sync_dir': 'ldap_to_django',
|
||||
'last_sync_dir': 'ldap',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -516,10 +516,15 @@ class UserService(BaseService):
|
||||
self._password_service.set_password(conn, dn, new_password)
|
||||
|
||||
def _enable_user(self, conn: Connection, dn: str) -> None:
|
||||
"""Включает учётку (UAC=ENABLED)."""
|
||||
ldap_user = LdapUser.objects.get(dn=dn)
|
||||
ldap_user.user_account_control = UserAccountControl.ENABLED
|
||||
ldap_user.save()
|
||||
"""Включает учётку (UAC=ENABLED) через низкоуровневую модификацию."""
|
||||
from ldap3 import MODIFY_REPLACE
|
||||
conn.modify(dn, {
|
||||
'userAccountControl': [(MODIFY_REPLACE, [str(UserAccountControl.ENABLED)])]
|
||||
})
|
||||
if not conn.result['result'] == 0:
|
||||
raise DirectoryLdapError(
|
||||
f"Failed to enable user {dn}: {conn.result['description']}"
|
||||
)
|
||||
|
||||
def _unique_logins(
|
||||
self,
|
||||
|
||||
@ -604,7 +604,7 @@ class LdapSyncState(models.Model):
|
||||
ldap_guid (str | None): objectGUID из Active Directory.
|
||||
last_ldap_modify_ts (datetime | None): Последний modifyTimestamp/whenChanged из LDAP.
|
||||
last_django_modify_ts (datetime | None): Последний updated_at из Django на момент фиксации.
|
||||
last_sync_dir (str): 'ldap'|'django'|'auto' — направление последнего успешного синка.
|
||||
last_sync_dir (str): 'ldap'|'django'|'auto'|'manual' — направление последнего успешного синка.
|
||||
data_hash (str | None): Хэш значимых полей (опционально) для ускоренной детекции изменений.
|
||||
updated_at (datetime): Время обновления записи состояния.
|
||||
|
||||
@ -648,14 +648,14 @@ class LdapSyncState(models.Model):
|
||||
ldap_guid (str | None): Новый GUID.
|
||||
last_ldap_modify_ts: Новый штамп LDAP (UTC).
|
||||
last_django_modify_ts: Новый штамп Django (UTC).
|
||||
sync_dir (str | None): Направление ('ldap'|'django'|'auto').
|
||||
sync_dir (str | None): Направление ('ldap'|'django'|'auto'|'manual').
|
||||
data_hash (str | None): Хэш полезных данных.
|
||||
|
||||
Raises:
|
||||
ValueError: Если sync_dir не из допустимого набора.
|
||||
"""
|
||||
if sync_dir and sync_dir not in ("ldap", "django", "auto"):
|
||||
raise ValueError("sync_dir должен быть 'ldap'|'django'|'auto'")
|
||||
if sync_dir and sync_dir not in ("ldap", "django", "auto", "manual"):
|
||||
raise ValueError("sync_dir должен быть 'ldap'|'django'|'auto'|'manual'")
|
||||
if ldap_dn is not None:
|
||||
self.ldap_dn = ldap_dn
|
||||
if ldap_guid is not None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user