From b63162b1cba8030ed6167b039b0eca15154b190e Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 10 Jan 2026 00:10:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=82=20Django=20Groups/Permissions=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20CustomUser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomUser теперь наследуется от AbstractBaseUser (вместо AbstractUser) - Удалены поля groups и user_permissions из CustomUser - Все authentication backends (TenantUserBackend, PlatformAdminBackend, RoleBasedPermissionBackend) больше НЕ наследуются от ModelBackend - Добавлены методы has_perm() и has_module_perms() в CustomUser для делегирования проверки прав кастомным backends - Полная изоляция: CustomUser использует только систему ролей (UserRole), PlatformAdmin использует только is_superuser - Удалён весь старый код, связанный с Django permissions - Нет обратной совместимости (не требуется) - Чистая архитектура для multi-tenant приложения --- myproject/accounts/backends.py | 7 +- ...2_remove_customuser_first_name_and_more.py | 54 +++++++++++++++ myproject/accounts/models.py | 67 +++++++++++++------ myproject/platform_admin/backends.py | 44 +++++++++++- myproject/user_roles/auth_backend.py | 33 +++++---- 5 files changed, 162 insertions(+), 43 deletions(-) create mode 100644 myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py diff --git a/myproject/accounts/backends.py b/myproject/accounts/backends.py index 7eaaa63..231cb46 100644 --- a/myproject/accounts/backends.py +++ b/myproject/accounts/backends.py @@ -7,15 +7,18 @@ Authentication backend для CustomUser (пользователей тенан ВАЖНО: CustomUser теперь в TENANT_APPS - каждый тенант имеет свою таблицу! Backend работает с таблицей accounts_customuser в текущей tenant schema. + +ВАЖНО: НЕ наследуется от ModelBackend! Полностью независимая реализация. """ -from django.contrib.auth.backends import ModelBackend from django.db import connection -class TenantUserBackend(ModelBackend): +class TenantUserBackend: """ Backend аутентификации для CustomUser (tenant-only). + НЕ наследуется от ModelBackend! Полностью независимая реализация. + Особенности: - Работает ТОЛЬКО на tenant доменах (не на public) - Ищет пользователя в таблице accounts_customuser текущей tenant schema diff --git a/myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py b/myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py new file mode 100644 index 0000000..0a07b89 --- /dev/null +++ b/myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 5.0.10 on 2026-01-09 21:04 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='customuser', + name='first_name', + ), + migrations.RemoveField( + model_name='customuser', + name='groups', + ), + migrations.RemoveField( + model_name='customuser', + name='last_name', + ), + migrations.RemoveField( + model_name='customuser', + name='user_permissions', + ), + migrations.RemoveField( + model_name='customuser', + name='username', + ), + migrations.AlterField( + model_name='customuser', + name='date_joined', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='customuser', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='customuser', + name='is_staff', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='customuser', + name='is_superuser', + field=models.BooleanField(default=False), + ), + ] diff --git a/myproject/accounts/models.py b/myproject/accounts/models.py index adb8453..1a290f9 100644 --- a/myproject/accounts/models.py +++ b/myproject/accounts/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.contrib.auth.models import AbstractUser, BaseUserManager +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager from django.utils import timezone import uuid @@ -9,15 +9,13 @@ class CustomUserManager(BaseUserManager): if not email: raise ValueError('Email обязателен') email = self.normalize_email(email) - # Generate a unique username based on email to satisfy the AbstractUser constraint - username = email # SECURITY FIX: Явно устанавливаем флаги безопасности в False по умолчанию # Обычные пользователи НЕ должны иметь доступ к админке extra_fields.setdefault('is_staff', False) extra_fields.setdefault('is_superuser', False) - user = self.model(email=email, name=name, username=username, **extra_fields) + user = self.model(email=email, name=name, **extra_fields) user.set_password(password) user.save(using=self._db) return user @@ -42,7 +40,7 @@ class CustomUserManager(BaseUserManager): return user -class CustomUser(AbstractUser): +class CustomUser(AbstractBaseUser): """ Пользователь тенанта (магазина). @@ -51,9 +49,16 @@ class CustomUser(AbstractUser): Полная изоляция обеспечивается на уровне PostgreSQL schemas. НЕ является AUTH_USER_MODEL (это PlatformAdmin). + НЕ использует Django Groups/Permissions - используется своя система ролей (UserRole). """ email = models.EmailField(unique=True) name = models.CharField(max_length=100) + + # Стандартные поля для совместимости с Django auth + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) # Для доступа к админке (если нужно) + is_superuser = models.BooleanField(default=False) # Для полных прав в тенанте + date_joined = models.DateTimeField(default=timezone.now) is_email_confirmed = models.BooleanField(default=False) email_confirmation_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) @@ -65,22 +70,6 @@ class CustomUser(AbstractUser): objects = CustomUserManager() - # Изменяем related_name для избежания конфликта с встроенной моделью User - groups = models.ManyToManyField( - 'auth.Group', - related_name='custom_user_set', - blank=True, - verbose_name='groups', - help_text='The groups this user belongs to.', - ) - user_permissions = models.ManyToManyField( - 'auth.Permission', - related_name='custom_user_set', - blank=True, - verbose_name='user permissions', - help_text='Specific permissions for this user.', - ) - class Meta: verbose_name = "Пользователь магазина" verbose_name_plural = "Пользователи магазина" @@ -88,6 +77,42 @@ class CustomUser(AbstractUser): def __str__(self): return self.email + def has_perm(self, perm, obj=None): + """ + Проверка разрешения через authentication backends. + Django вызывает все зарегистрированные backends по очереди. + """ + if not self.is_active: + return False + + # Импортируем здесь, чтобы избежать циклических импортов + from django.contrib.auth import get_backends + + for backend in get_backends(): + if hasattr(backend, 'has_perm'): + result = backend.has_perm(self, perm, obj) + if result is not None: # Backend обработал запрос + return result + + return False + + def has_module_perms(self, app_label): + """ + Проверка разрешений для модуля через authentication backends. + """ + if not self.is_active: + return False + + from django.contrib.auth import get_backends + + for backend in get_backends(): + if hasattr(backend, 'has_module_perms'): + result = backend.has_module_perms(self, app_label) + if result is not None: # Backend обработал запрос + return result + + return False + def generate_confirmation_token(self): """Генерирует новый токен для подтверждения email""" self.email_confirmation_token = uuid.uuid4() diff --git a/myproject/platform_admin/backends.py b/myproject/platform_admin/backends.py index e940080..edc6af2 100644 --- a/myproject/platform_admin/backends.py +++ b/myproject/platform_admin/backends.py @@ -4,15 +4,18 @@ Authentication backend для PlatformAdmin. Этот backend используется для аутентификации администраторов платформы. Работает на public домене, а также на tenant доменах для суперадминов. + +ВАЖНО: НЕ наследуется от ModelBackend! Полностью независимая реализация. """ -from django.contrib.auth.backends import ModelBackend from django.db import connection -class PlatformAdminBackend(ModelBackend): +class PlatformAdminBackend: """ Backend аутентификации для PlatformAdmin. + НЕ наследуется от ModelBackend! Полностью независимая реализация. + Особенности: - На public домене: аутентифицирует любого PlatformAdmin - На tenant домене: аутентифицирует только PlatformAdmin с is_superuser=True @@ -79,3 +82,40 @@ class PlatformAdminBackend(ModelBackend): """ is_active = getattr(user, 'is_active', None) return is_active or is_active is None + + def has_perm(self, user_obj, perm, obj=None): + """ + Проверка permissions для PlatformAdmin. + + ВАЖНО: Этот backend работает ТОЛЬКО с PlatformAdmin! + Для CustomUser возвращает None (пропускает проверку). + + Для PlatformAdmin проверяем только is_superuser (без groups/permissions). + """ + from platform_admin.models import PlatformAdmin + + # Проверяем только PlatformAdmin, CustomUser пропускаем + if not isinstance(user_obj, PlatformAdmin): + return None + + # Только суперпользователь имеет права + # (не используем groups/permissions из Django, т.к. AUTH_USER_MODEL != реальная модель) + return user_obj.is_active and user_obj.is_superuser + + def has_module_perms(self, user_obj, app_label): + """ + Проверка module permissions для PlatformAdmin. + + ВАЖНО: Этот backend работает ТОЛЬКО с PlatformAdmin! + Для CustomUser возвращает None (пропускает проверку). + + Для PlatformAdmin проверяем только is_superuser (без groups/permissions). + """ + from platform_admin.models import PlatformAdmin + + # Проверяем только PlatformAdmin, CustomUser пропускаем + if not isinstance(user_obj, PlatformAdmin): + return None + + # Только суперпользователь имеет права + return user_obj.is_active and user_obj.is_superuser diff --git a/myproject/user_roles/auth_backend.py b/myproject/user_roles/auth_backend.py index d699ac4..e48eb8f 100644 --- a/myproject/user_roles/auth_backend.py +++ b/myproject/user_roles/auth_backend.py @@ -1,4 +1,4 @@ -""" +""" Кастомный backend аутентификации для связывания ролей с Django permissions API. ВАЖНО: Этот backend НЕ использует таблицы Django permissions из public schema! @@ -7,8 +7,11 @@ ВАЖНО: Backend проверяет текущую схему перед обращением к tenant-only таблицам. В public схеме ролевые проверки пропускаются (fallback на стандартные Django permissions). + +ВАЖНО: Этот backend НЕ наследуется от ModelBackend! +Он реализует только has_perm/has_module_perms для проверки прав на основе ролей. +Аутентификацию (authenticate/get_user) выполняют другие backends. """ -from django.contrib.auth.backends import ModelBackend from django.db import connection from django_tenants.utils import get_public_schema_name from user_roles.services import RoleService @@ -32,11 +35,12 @@ def _is_tenant_user(user_obj): return isinstance(user_obj, CustomUser) -class RoleBasedPermissionBackend(ModelBackend): +class RoleBasedPermissionBackend: """ Backend, который предоставляет права на основе роли пользователя в текущем тенанте. - Расширяет стандартный ModelBackend, добавляя проверку прав на основе ролей из tenant schema. + НЕ наследуется от ModelBackend! Реализует только проверку прав (has_perm/has_module_perms). + Аутентификацию (authenticate/get_user) выполняют TenantUserBackend и PlatformAdminBackend. Как это работает: 1. Django вызывает user.has_perm('products.add_product') @@ -132,13 +136,10 @@ class RoleBasedPermissionBackend(ModelBackend): return action in app_perms else: - # Для PlatformAdmin проверяем стандартные permissions через ModelBackend - # Суперпользователь имеет все права - if user_obj.is_superuser: - return True - - # Проверяем через родительский ModelBackend - return super().has_perm(user_obj, perm, obj) + # Для PlatformAdmin - этот backend НЕ обрабатывает PlatformAdmin + # Возвращаем None, чтобы Django перешёл к другому backend + # (PlatformAdminBackend должен обработать это) + return None def has_module_perms(self, user_obj, app_label): """ @@ -179,10 +180,6 @@ class RoleBasedPermissionBackend(ModelBackend): role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {}) return app_label in role_perms and len(role_perms[app_label]) > 0 else: - # Для PlatformAdmin проверяем стандартные permissions через ModelBackend - # Суперпользователь имеет все права - if user_obj.is_superuser: - return True - - # Проверяем через родительский ModelBackend - return super().has_module_perms(user_obj, app_label) + # Для PlatformAdmin - этот backend НЕ обрабатывает PlatformAdmin + # Возвращаем None, чтобы Django перешёл к другому backend + return None