Устранена ошибка ValueError "Cannot query 'email': Must be 'PlatformAdmin' instance"
при доступе CustomUser к странице /products/.
Проблема:
- Модели products (TENANT_APPS) использовали get_user_model() для ForeignKey
- get_user_model() возвращал PlatformAdmin (AUTH_USER_MODEL)
- Но tenant модели должны ссылаться на CustomUser (tenant пользователей)
- Это создавало конфликт типов при запросах от CustomUser
Изменения:
1. products/models/base.py:
- Убран get_user_model()
- BaseProductEntity.archived_by теперь ForeignKey('accounts.CustomUser')
2. products/models/categories.py:
- Убран get_user_model()
- ProductCategory.deleted_by теперь ForeignKey('accounts.CustomUser')
3. products/models/import_job.py:
- Убран get_user_model()
- ProductImportJob.user теперь ForeignKey('accounts.CustomUser')
4. Создана миграция 0002 с data migration:
- Очистка некорректных ссылок (установка NULL)
- Изменение типа ForeignKey полей с PlatformAdmin на CustomUser
5. user_roles/auth_backend.py:
- Добавлена функция _is_tenant_user() для проверки типа пользователя
- Исправлена логика has_perm() и has_module_perms()
- CustomUser теперь не проверяется через ModelBackend.has_perm()
6. admin_access_middleware.py:
- Улучшены сообщения об ошибках доступа
- Добавлен рендеринг через шаблон access_denied.html
7. templates/errors/access_denied.html:
- Новый шаблон для красивого отображения ошибок доступа
Результат:
- CustomUser может без ошибок работать со страницей /products/
- Корректная архитектура: tenant модели ссылаются на tenant пользователей
- PlatformAdmin продолжает работать корректно
- Чистое решение без костылей
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
184 lines
8.9 KiB
Python
184 lines
8.9 KiB
Python
"""
|
||
Кастомный backend аутентификации для связывания ролей с Django permissions API.
|
||
|
||
ВАЖНО: Этот backend НЕ использует таблицы Django permissions из public schema!
|
||
Он только эмулирует API has_perm(), читая роли из текущей tenant schema.
|
||
Это безопасно для мультитенантной архитектуры.
|
||
|
||
ВАЖНО: Backend проверяет текущую схему перед обращением к tenant-only таблицам.
|
||
В public схеме ролевые проверки пропускаются (fallback на стандартные Django permissions).
|
||
"""
|
||
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
|
||
from user_roles.models import Role
|
||
|
||
|
||
def _is_public_schema():
|
||
"""
|
||
Проверяет, находимся ли мы в public схеме.
|
||
В public схеме нет таблиц tenant-only моделей (UserRole, Role).
|
||
"""
|
||
return connection.schema_name == get_public_schema_name()
|
||
|
||
|
||
def _is_tenant_user(user_obj):
|
||
"""
|
||
Проверяет, является ли пользователь CustomUser (пользователь тенанта).
|
||
PlatformAdmin не имеет ролей в тенантах.
|
||
"""
|
||
from accounts.models import CustomUser
|
||
return isinstance(user_obj, CustomUser)
|
||
|
||
|
||
class RoleBasedPermissionBackend(ModelBackend):
|
||
"""
|
||
Backend, который предоставляет права на основе роли пользователя в текущем тенанте.
|
||
|
||
Расширяет стандартный ModelBackend, добавляя проверку прав на основе ролей из tenant schema.
|
||
|
||
Как это работает:
|
||
1. Django вызывает user.has_perm('products.add_product')
|
||
2. Backend проверяет роль пользователя в ТЕКУЩЕЙ tenant schema (через RoleService)
|
||
3. На основе роли возвращает True/False
|
||
4. Никакие данные из public schema не используются!
|
||
"""
|
||
|
||
# Маппинг ролей на наборы разрешений
|
||
# Формат: 'app_label': ['action1', 'action2', ...]
|
||
# где action - это префикс permission: add_product -> add, change_order -> change
|
||
ROLE_PERMISSIONS = {
|
||
Role.OWNER: {
|
||
# Владелец: полный доступ ко всем модулям
|
||
'products': ['add', 'change', 'delete', 'view'],
|
||
'inventory': ['add', 'change', 'delete', 'view'],
|
||
'orders': ['add', 'change', 'delete', 'view'],
|
||
'clients': ['add', 'change', 'delete', 'view'],
|
||
'suppliers': ['add', 'change', 'delete', 'view'],
|
||
'user_roles': ['add', 'change', 'delete', 'view'],
|
||
'payments': ['add', 'change', 'delete', 'view'],
|
||
},
|
||
Role.MANAGER: {
|
||
# Менеджер: доступ к основным операционным модулям (без настроек)
|
||
'products': ['add', 'change', 'delete', 'view'],
|
||
'inventory': ['add', 'change', 'delete', 'view'],
|
||
'orders': ['add', 'change', 'delete', 'view'],
|
||
'clients': ['add', 'change', 'delete', 'view'],
|
||
'suppliers': ['add', 'change', 'view'], # только просмотр и изменение, без удаления
|
||
'payments': ['view'],
|
||
},
|
||
Role.FLORIST: {
|
||
# Флорист: работа с заказами и просмотр товаров
|
||
'products': ['view'],
|
||
'inventory': ['view', 'change'], # может обновлять остатки при сборке заказов
|
||
'orders': ['change', 'view'],
|
||
},
|
||
Role.COURIER: {
|
||
# Курьер: только просмотр и обновление статуса заказов
|
||
'orders': ['change', 'view'],
|
||
},
|
||
}
|
||
|
||
def has_perm(self, user_obj, perm, obj=None):
|
||
"""
|
||
Проверяет, имеет ли пользователь указанное разрешение.
|
||
|
||
Формат разрешения: 'app_label.action_modelname'
|
||
Например: 'products.add_product', 'orders.change_order'
|
||
|
||
Args:
|
||
user_obj: Пользователь
|
||
perm: Строка разрешения в формате 'app.action_model'
|
||
obj: Опциональный объект для object-level permissions
|
||
|
||
Returns:
|
||
bool: True если пользователь имеет разрешение
|
||
"""
|
||
# Если пользователь не аутентифицирован, нет доступа
|
||
if not user_obj.is_authenticated:
|
||
return False
|
||
|
||
# Для CustomUser (пользователи тенантов) пропускаем стандартную проверку Django permissions
|
||
# т.к. AUTH_USER_MODEL = PlatformAdmin, и super().has_perm() будет пытаться
|
||
# искать permissions для PlatformAdmin, а не для CustomUser
|
||
is_tenant = _is_tenant_user(user_obj)
|
||
|
||
if not is_tenant:
|
||
# Для PlatformAdmin проверяем стандартные permissions через ModelBackend
|
||
if super().has_perm(user_obj, perm, obj):
|
||
return True
|
||
|
||
# Суперпользователь имеет все права
|
||
if user_obj.is_superuser:
|
||
return True
|
||
|
||
# ВАЖНО: В public схеме нет таблиц UserRole/Role!
|
||
# Используем только стандартные Django permissions.
|
||
if _is_public_schema():
|
||
return False
|
||
|
||
# Для CustomUser получаем роль пользователя в текущем тенанте
|
||
# ВАЖНО: RoleService работает с текущей tenant schema!
|
||
user_role = RoleService.get_user_role(user_obj)
|
||
if not user_role:
|
||
return False
|
||
|
||
# Парсим разрешение: 'app_label.action_modelname'
|
||
# Например: 'products.add_product' -> app_label='products', action='add'
|
||
try:
|
||
app_label, codename = perm.split('.')
|
||
# Извлекаем действие из codename (add_product -> add, change_order -> change)
|
||
action = codename.split('_')[0]
|
||
except (ValueError, IndexError):
|
||
return False
|
||
|
||
# Проверяем, есть ли у роли это разрешение
|
||
role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {})
|
||
app_perms = role_perms.get(app_label, [])
|
||
|
||
return action in app_perms
|
||
|
||
def has_module_perms(self, user_obj, app_label):
|
||
"""
|
||
Проверяет, имеет ли пользователь какие-либо разрешения для приложения.
|
||
|
||
Используется в Django admin для определения, показывать ли модуль в навигации.
|
||
|
||
Args:
|
||
user_obj: Пользователь
|
||
app_label: Название приложения (например, 'products', 'orders')
|
||
|
||
Returns:
|
||
bool: True если пользователь имеет хотя бы одно разрешение для приложения
|
||
"""
|
||
# Если пользователь не аутентифицирован, нет доступа
|
||
if not user_obj.is_authenticated:
|
||
return False
|
||
|
||
# Для CustomUser (пользователи тенантов) пропускаем стандартную проверку Django permissions
|
||
is_tenant = _is_tenant_user(user_obj)
|
||
|
||
if not is_tenant:
|
||
# Для PlatformAdmin проверяем стандартные permissions через ModelBackend
|
||
if super().has_module_perms(user_obj, app_label):
|
||
return True
|
||
|
||
# Суперпользователь имеет все права
|
||
if user_obj.is_superuser:
|
||
return True
|
||
|
||
# ВАЖНО: В public схеме нет таблиц UserRole/Role!
|
||
# Используем только стандартные Django permissions.
|
||
if _is_public_schema():
|
||
return False
|
||
|
||
# Для CustomUser получаем роль пользователя в текущем тенанте
|
||
user_role = RoleService.get_user_role(user_obj)
|
||
if not user_role:
|
||
return False
|
||
|
||
# Проверяем, есть ли у роли какие-либо разрешения для этого приложения
|
||
role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {})
|
||
return app_label in role_perms and len(role_perms[app_label]) > 0
|