Files
octopus/myproject/user_roles/auth_backend.py
Andrey Smakotin 71ca681073 fix: Исправлена ошибка ForeignKey в products - замена PlatformAdmin на CustomUser
Устранена ошибка 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>
2026-01-09 23:47:07 +03:00

184 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Кастомный 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