Рефакторинг: убрана зависимость от 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:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,9 +49,16 @@ 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)
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Кастомный backend аутентификации для связывания ролей с Django permissions API.
|
Кастомный backend аутентификации для связывания ролей с Django permissions API.
|
||||||
|
|
||||||
ВАЖНО: Этот backend НЕ использует таблицы Django permissions из public schema!
|
ВАЖНО: Этот backend НЕ использует таблицы Django permissions из public schema!
|
||||||
@@ -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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user