Рефакторинг: убрана зависимость от Django Groups/Permissions для CustomUser

- 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 приложения
This commit is contained in:
2026-01-10 00:10:25 +03:00
parent d90b0162c5
commit b63162b1cb
5 changed files with 162 additions and 43 deletions

View File

@@ -7,15 +7,18 @@ Authentication backend для CustomUser (пользователей тенан
ВАЖНО: CustomUser теперь в TENANT_APPS - каждый тенант имеет свою таблицу! ВАЖНО: CustomUser теперь в TENANT_APPS - каждый тенант имеет свою таблицу!
Backend работает с таблицей accounts_customuser в текущей tenant schema. Backend работает с таблицей accounts_customuser в текущей tenant schema.
ВАЖНО: НЕ наследуется от ModelBackend! Полностью независимая реализация.
""" """
from django.contrib.auth.backends import ModelBackend
from django.db import connection from django.db import connection
class TenantUserBackend(ModelBackend): class TenantUserBackend:
""" """
Backend аутентификации для CustomUser (tenant-only). Backend аутентификации для CustomUser (tenant-only).
НЕ наследуется от ModelBackend! Полностью независимая реализация.
Особенности: Особенности:
- Работает ТОЛЬКО на tenant доменах (не на public) - Работает ТОЛЬКО на tenant доменах (не на public)
- Ищет пользователя в таблице accounts_customuser текущей tenant schema - Ищет пользователя в таблице accounts_customuser текущей tenant schema

View File

@@ -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),
),
]

View File

@@ -1,5 +1,5 @@
from django.db import models 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 from django.utils import timezone
import uuid import uuid
@@ -9,15 +9,13 @@ class CustomUserManager(BaseUserManager):
if not email: if not email:
raise ValueError('Email обязателен') raise ValueError('Email обязателен')
email = self.normalize_email(email) email = self.normalize_email(email)
# Generate a unique username based on email to satisfy the AbstractUser constraint
username = email
# SECURITY FIX: Явно устанавливаем флаги безопасности в False по умолчанию # SECURITY FIX: Явно устанавливаем флаги безопасности в False по умолчанию
# Обычные пользователи НЕ должны иметь доступ к админке # Обычные пользователи НЕ должны иметь доступ к админке
extra_fields.setdefault('is_staff', False) extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', 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.set_password(password)
user.save(using=self._db) user.save(using=self._db)
return user return user
@@ -42,7 +40,7 @@ class CustomUserManager(BaseUserManager):
return user return user
class CustomUser(AbstractUser): class CustomUser(AbstractBaseUser):
""" """
Пользователь тенанта (магазина). Пользователь тенанта (магазина).
@@ -51,10 +49,17 @@ class CustomUser(AbstractUser):
Полная изоляция обеспечивается на уровне PostgreSQL schemas. Полная изоляция обеспечивается на уровне PostgreSQL schemas.
НЕ является AUTH_USER_MODEL (это PlatformAdmin). НЕ является AUTH_USER_MODEL (это PlatformAdmin).
НЕ использует Django Groups/Permissions - используется своя система ролей (UserRole).
""" """
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
name = models.CharField(max_length=100) 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) is_email_confirmed = models.BooleanField(default=False)
email_confirmation_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) email_confirmation_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
email_confirmed_at = models.DateTimeField(null=True, blank=True) email_confirmed_at = models.DateTimeField(null=True, blank=True)
@@ -65,22 +70,6 @@ class CustomUser(AbstractUser):
objects = CustomUserManager() 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: class Meta:
verbose_name = "Пользователь магазина" verbose_name = "Пользователь магазина"
verbose_name_plural = "Пользователи магазина" verbose_name_plural = "Пользователи магазина"
@@ -88,6 +77,42 @@ class CustomUser(AbstractUser):
def __str__(self): def __str__(self):
return self.email 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): def generate_confirmation_token(self):
"""Генерирует новый токен для подтверждения email""" """Генерирует новый токен для подтверждения email"""
self.email_confirmation_token = uuid.uuid4() self.email_confirmation_token = uuid.uuid4()

View File

@@ -4,15 +4,18 @@ Authentication backend для PlatformAdmin.
Этот backend используется для аутентификации администраторов платформы. Этот backend используется для аутентификации администраторов платформы.
Работает на public домене, а также на tenant доменах для суперадминов. Работает на public домене, а также на tenant доменах для суперадминов.
ВАЖНО: НЕ наследуется от ModelBackend! Полностью независимая реализация.
""" """
from django.contrib.auth.backends import ModelBackend
from django.db import connection from django.db import connection
class PlatformAdminBackend(ModelBackend): class PlatformAdminBackend:
""" """
Backend аутентификации для PlatformAdmin. Backend аутентификации для PlatformAdmin.
НЕ наследуется от ModelBackend! Полностью независимая реализация.
Особенности: Особенности:
- На public домене: аутентифицирует любого PlatformAdmin - На public домене: аутентифицирует любого PlatformAdmin
- На tenant домене: аутентифицирует только PlatformAdmin с is_superuser=True - На tenant домене: аутентифицирует только PlatformAdmin с is_superuser=True
@@ -79,3 +82,40 @@ class PlatformAdminBackend(ModelBackend):
""" """
is_active = getattr(user, 'is_active', None) is_active = getattr(user, 'is_active', None)
return is_active or is_active is 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

View File

@@ -7,8 +7,11 @@
ВАЖНО: Backend проверяет текущую схему перед обращением к tenant-only таблицам. ВАЖНО: Backend проверяет текущую схему перед обращением к tenant-only таблицам.
В public схеме ролевые проверки пропускаются (fallback на стандартные Django permissions). В 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.db import connection
from django_tenants.utils import get_public_schema_name from django_tenants.utils import get_public_schema_name
from user_roles.services import RoleService from user_roles.services import RoleService
@@ -32,11 +35,12 @@ def _is_tenant_user(user_obj):
return isinstance(user_obj, CustomUser) return isinstance(user_obj, CustomUser)
class RoleBasedPermissionBackend(ModelBackend): class RoleBasedPermissionBackend:
""" """
Backend, который предоставляет права на основе роли пользователя в текущем тенанте. Backend, который предоставляет права на основе роли пользователя в текущем тенанте.
Расширяет стандартный ModelBackend, добавляя проверку прав на основе ролей из tenant schema. НЕ наследуется от ModelBackend! Реализует только проверку прав (has_perm/has_module_perms).
Аутентификацию (authenticate/get_user) выполняют TenantUserBackend и PlatformAdminBackend.
Как это работает: Как это работает:
1. Django вызывает user.has_perm('products.add_product') 1. Django вызывает user.has_perm('products.add_product')
@@ -132,13 +136,10 @@ class RoleBasedPermissionBackend(ModelBackend):
return action in app_perms return action in app_perms
else: else:
# Для PlatformAdmin проверяем стандартные permissions через ModelBackend # Для PlatformAdmin - этот backend НЕ обрабатывает PlatformAdmin
# Суперпользователь имеет все права # Возвращаем None, чтобы Django перешёл к другому backend
if user_obj.is_superuser: # (PlatformAdminBackend должен обработать это)
return True return None
# Проверяем через родительский ModelBackend
return super().has_perm(user_obj, perm, obj)
def has_module_perms(self, user_obj, app_label): 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, {}) role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {})
return app_label in role_perms and len(role_perms[app_label]) > 0 return app_label in role_perms and len(role_perms[app_label]) > 0
else: else:
# Для PlatformAdmin проверяем стандартные permissions через ModelBackend # Для PlatformAdmin - этот backend НЕ обрабатывает PlatformAdmin
# Суперпользователь имеет все права # Возвращаем None, чтобы Django перешёл к другому backend
if user_obj.is_superuser: return None
return True
# Проверяем через родительский ModelBackend
return super().has_module_perms(user_obj, app_label)