Исправлена ошибка public admin для мультитенантной архитектуры
Проблема: при входе в localhost/admin/ (public схема) возникала ошибка "relation user_roles_userrole does not exist", так как tenant-only таблицы не существуют в public схеме. Решение: - Создан TenantAdminOnlyMixin для скрытия tenant-only моделей от public admin - Применён миксин ко всем ModelAdmin классам в tenant-only приложениях: user_roles, customers, orders, inventory, products - Добавлена проверка _is_public_schema() в RoleBasedPermissionBackend для предотвращения запросов к tenant-only таблицам в public схеме Теперь: - localhost/admin/ показывает только public модели (Client, Domain, User) - shop.localhost/admin/ показывает все модели магазина 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
from .models import Customer, WalletTransaction, ContactChannel
|
||||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||||
|
||||
|
||||
class IsSystemCustomerFilter(admin.SimpleListFilter):
|
||||
@@ -23,8 +24,11 @@ class IsSystemCustomerFilter(admin.SimpleListFilter):
|
||||
|
||||
|
||||
@admin.register(Customer)
|
||||
class CustomerAdmin(admin.ModelAdmin):
|
||||
"""Административный интерфейс для управления клиентами цветочного магазина"""
|
||||
class CustomerAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Административный интерфейс для управления клиентами цветочного магазина.
|
||||
TenantAdminOnlyMixin - скрывает от public admin (таблица только в tenant схемах).
|
||||
"""
|
||||
list_display = (
|
||||
'full_name',
|
||||
'email',
|
||||
@@ -124,8 +128,11 @@ CustomerAdmin.inlines = [ContactChannelInline, WalletTransactionInline]
|
||||
|
||||
|
||||
@admin.register(WalletTransaction)
|
||||
class WalletTransactionAdmin(admin.ModelAdmin):
|
||||
"""Админка для просмотра всех транзакций кошелька"""
|
||||
class WalletTransactionAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админка для просмотра всех транзакций кошелька.
|
||||
TenantAdminOnlyMixin - скрывает от public admin.
|
||||
"""
|
||||
list_display = ('created_at', 'customer', 'transaction_type', 'amount_display', 'balance_after', 'order', 'created_by')
|
||||
list_filter = ('transaction_type', 'balance_category', 'created_at')
|
||||
search_fields = ('customer__name', 'customer__email', 'customer__phone', 'description')
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Админка для приложения inventory.
|
||||
Все модели tenant-only, поэтому используют TenantAdminOnlyMixin
|
||||
для скрытия от public admin (localhost/admin/).
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
@@ -11,11 +16,12 @@ from inventory.models import (
|
||||
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,
|
||||
TransformationOutput, TransferDocument, TransferDocumentItem
|
||||
)
|
||||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||||
|
||||
|
||||
# ===== SHOWCASE =====
|
||||
@admin.register(Showcase)
|
||||
class ShowcaseAdmin(admin.ModelAdmin):
|
||||
class ShowcaseAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('name', 'warehouse', 'is_default', 'is_active', 'created_at')
|
||||
list_filter = ('is_active', 'is_default', 'warehouse', 'created_at')
|
||||
search_fields = ('name', 'warehouse__name')
|
||||
@@ -34,7 +40,7 @@ class ShowcaseAdmin(admin.ModelAdmin):
|
||||
|
||||
# ===== WAREHOUSE =====
|
||||
@admin.register(Warehouse)
|
||||
class WarehouseAdmin(admin.ModelAdmin):
|
||||
class WarehouseAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('name', 'is_default_display', 'is_active', 'created_at')
|
||||
list_filter = ('is_active', 'is_default', 'created_at')
|
||||
search_fields = ('name',)
|
||||
@@ -58,7 +64,7 @@ class WarehouseAdmin(admin.ModelAdmin):
|
||||
|
||||
# ===== STOCK BATCH =====
|
||||
@admin.register(StockBatch)
|
||||
class StockBatchAdmin(admin.ModelAdmin):
|
||||
class StockBatchAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('product', 'warehouse', 'quantity_display', 'cost_price', 'created_at', 'is_active')
|
||||
list_filter = ('warehouse', 'is_active', 'created_at')
|
||||
search_fields = ('product__name', 'product__sku', 'warehouse__name')
|
||||
@@ -104,7 +110,7 @@ class SaleBatchAllocationInline(admin.TabularInline):
|
||||
|
||||
# ===== SALE =====
|
||||
@admin.register(Sale)
|
||||
class SaleAdmin(admin.ModelAdmin):
|
||||
class SaleAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('product', 'warehouse', 'quantity', 'sale_price', 'order_display', 'processed_display', 'date')
|
||||
list_filter = ('warehouse', 'processed', 'date')
|
||||
search_fields = ('product__name', 'order__order_number')
|
||||
@@ -142,7 +148,7 @@ class SaleAdmin(admin.ModelAdmin):
|
||||
|
||||
# ===== WRITE OFF =====
|
||||
@admin.register(WriteOff)
|
||||
class WriteOffAdmin(admin.ModelAdmin):
|
||||
class WriteOffAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('batch', 'quantity', 'reason_display', 'cost_price', 'date')
|
||||
list_filter = ('reason', 'date', 'batch__warehouse')
|
||||
search_fields = ('batch__product__name', 'document_number')
|
||||
@@ -176,7 +182,7 @@ class InventoryLineInline(admin.TabularInline):
|
||||
|
||||
# ===== INVENTORY =====
|
||||
@admin.register(Inventory)
|
||||
class InventoryAdmin(admin.ModelAdmin):
|
||||
class InventoryAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('warehouse', 'status_display', 'date', 'conducted_by')
|
||||
list_filter = ('status', 'date', 'warehouse')
|
||||
search_fields = ('warehouse__name', 'conducted_by')
|
||||
@@ -234,7 +240,7 @@ class InventoryAdmin(admin.ModelAdmin):
|
||||
|
||||
# ===== RESERVATION =====
|
||||
@admin.register(Reservation)
|
||||
class ReservationAdmin(admin.ModelAdmin):
|
||||
class ReservationAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('product', 'warehouse', 'quantity', 'status_display', 'context_info', 'reserved_at')
|
||||
list_filter = ('status', 'reserved_at', 'warehouse', 'showcase')
|
||||
search_fields = ('product__name', 'order_item__order__order_number', 'showcase__name')
|
||||
@@ -279,7 +285,7 @@ class ReservationAdmin(admin.ModelAdmin):
|
||||
|
||||
# ===== STOCK =====
|
||||
@admin.register(Stock)
|
||||
class StockAdmin(admin.ModelAdmin):
|
||||
class StockAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('product', 'warehouse', 'quantity_available', 'quantity_reserved', 'quantity_free', 'updated_at')
|
||||
list_filter = ('warehouse', 'updated_at')
|
||||
search_fields = ('product__name', 'product__sku', 'warehouse__name')
|
||||
@@ -305,7 +311,7 @@ class WriteOffDocumentItemInline(admin.TabularInline):
|
||||
|
||||
|
||||
@admin.register(WriteOffDocument)
|
||||
class WriteOffDocumentAdmin(admin.ModelAdmin):
|
||||
class WriteOffDocumentAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('document_number', 'warehouse', 'status_display', 'date', 'items_count', 'total_quantity_display', 'created_by', 'created_at')
|
||||
list_filter = ('status', 'warehouse', 'date', 'created_at')
|
||||
search_fields = ('document_number', 'warehouse__name')
|
||||
@@ -346,7 +352,7 @@ class WriteOffDocumentAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(WriteOffDocumentItem)
|
||||
class WriteOffDocumentItemAdmin(admin.ModelAdmin):
|
||||
class WriteOffDocumentItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('document', 'product', 'quantity', 'reason', 'created_at')
|
||||
list_filter = ('reason', 'document__status', 'created_at')
|
||||
search_fields = ('product__name', 'document__document_number')
|
||||
@@ -362,7 +368,7 @@ class IncomingDocumentItemInline(admin.TabularInline):
|
||||
|
||||
|
||||
@admin.register(IncomingDocument)
|
||||
class IncomingDocumentAdmin(admin.ModelAdmin):
|
||||
class IncomingDocumentAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('document_number', 'warehouse', 'status_display', 'receipt_type_display', 'date', 'items_count', 'total_quantity_display', 'created_by', 'created_at')
|
||||
list_filter = ('status', 'warehouse', 'receipt_type', 'date', 'created_at')
|
||||
search_fields = ('document_number', 'warehouse__name', 'supplier_name')
|
||||
@@ -416,7 +422,7 @@ class IncomingDocumentAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(IncomingDocumentItem)
|
||||
class IncomingDocumentItemAdmin(admin.ModelAdmin):
|
||||
class IncomingDocumentItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('document', 'product', 'quantity', 'cost_price', 'total_cost_display', 'created_at')
|
||||
list_filter = ('document__status', 'document__receipt_type', 'created_at')
|
||||
search_fields = ('product__name', 'document__document_number')
|
||||
@@ -445,7 +451,7 @@ class TransformationOutputInline(admin.TabularInline):
|
||||
|
||||
|
||||
@admin.register(Transformation)
|
||||
class TransformationAdmin(admin.ModelAdmin):
|
||||
class TransformationAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ['document_number', 'warehouse', 'status_display', 'date', 'employee', 'inputs_count', 'outputs_count']
|
||||
list_filter = ['status', 'warehouse', 'date']
|
||||
search_fields = ['document_number', 'comment']
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Админка для приложения orders.
|
||||
Все модели tenant-only, поэтому используют TenantAdminOnlyMixin
|
||||
для скрытия от public admin (localhost/admin/).
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from .models import Order, OrderItem, Transaction, PaymentMethod, Address, OrderStatus, Recipient, Delivery
|
||||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||||
|
||||
|
||||
class TransactionInline(admin.TabularInline):
|
||||
@@ -44,10 +50,8 @@ class DeliveryInline(admin.StackedInline):
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления заказами.
|
||||
"""
|
||||
class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""Админ-панель для управления заказами."""
|
||||
list_display = [
|
||||
'order_number',
|
||||
'customer',
|
||||
@@ -150,7 +154,7 @@ class OrderAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(Transaction)
|
||||
class TransactionAdmin(admin.ModelAdmin):
|
||||
class TransactionAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления транзакциями.
|
||||
"""
|
||||
@@ -189,7 +193,7 @@ class TransactionAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(OrderItem)
|
||||
class OrderItemAdmin(admin.ModelAdmin):
|
||||
class OrderItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления позициями заказов.
|
||||
"""
|
||||
@@ -232,7 +236,7 @@ class OrderItemAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(Address)
|
||||
class AddressAdmin(admin.ModelAdmin):
|
||||
class AddressAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления адресами доставки заказов.
|
||||
"""
|
||||
@@ -276,7 +280,7 @@ class AddressAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(Recipient)
|
||||
class RecipientAdmin(admin.ModelAdmin):
|
||||
class RecipientAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления получателями заказов.
|
||||
"""
|
||||
@@ -309,7 +313,7 @@ class RecipientAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(OrderStatus)
|
||||
class OrderStatusAdmin(admin.ModelAdmin):
|
||||
class OrderStatusAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления статусами заказов.
|
||||
"""
|
||||
@@ -406,7 +410,7 @@ class OrderStatusAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(PaymentMethod)
|
||||
class PaymentMethodAdmin(admin.ModelAdmin):
|
||||
class PaymentMethodAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления способами оплаты.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Админка для приложения products.
|
||||
Все модели tenant-only, поэтому используют TenantAdminOnlyMixin
|
||||
для скрытия от public admin (localhost/admin/).
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils import timezone
|
||||
@@ -14,6 +19,7 @@ from .admin_displays import (
|
||||
format_photo_inline_quality,
|
||||
format_photo_preview_with_quality,
|
||||
)
|
||||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||||
|
||||
|
||||
class DeletedFilter(admin.SimpleListFilter):
|
||||
@@ -292,7 +298,7 @@ def disable_delete_selected(admin_class):
|
||||
|
||||
|
||||
@admin.register(ProductVariantGroup)
|
||||
class ProductVariantGroupAdmin(admin.ModelAdmin):
|
||||
class ProductVariantGroupAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ['name', 'get_products_count', 'created_at']
|
||||
search_fields = ['name', 'description']
|
||||
list_filter = ['created_at']
|
||||
@@ -303,7 +309,7 @@ class ProductVariantGroupAdmin(admin.ModelAdmin):
|
||||
get_products_count.short_description = 'Товаров'
|
||||
|
||||
|
||||
class ProductCategoryAdmin(admin.ModelAdmin):
|
||||
class ProductCategoryAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('photo_with_quality', 'name', 'sku', 'slug', 'parent', 'is_active', 'get_deleted_status')
|
||||
list_filter = (DeletedFilter, 'is_active', QualityLevelFilter, 'parent')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
@@ -376,14 +382,14 @@ class ProductCategoryAdmin(admin.ModelAdmin):
|
||||
photo_preview_large.short_description = "Превью основного фото"
|
||||
|
||||
|
||||
class ProductTagAdmin(admin.ModelAdmin):
|
||||
class ProductTagAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('name', 'slug', 'is_active')
|
||||
list_filter = ('is_active',)
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
class ProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('photo_with_quality', 'name', 'sku', 'get_categories_display', 'cost_price', 'price', 'sale_price', 'get_variant_groups_display', 'get_status_display')
|
||||
list_filter = (DeletedFilter, QualityLevelFilter, 'categories', 'tags', 'variant_groups')
|
||||
search_fields = ('name', 'sku', 'description', 'search_keywords')
|
||||
@@ -576,7 +582,7 @@ class ProductAdmin(admin.ModelAdmin):
|
||||
photo_preview_large.short_description = "Превью основного фото"
|
||||
|
||||
|
||||
class ProductKitAdmin(admin.ModelAdmin):
|
||||
class ProductKitAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ('photo_with_quality', 'name', 'slug', 'get_categories_display', 'get_price_display', 'is_temporary', 'get_order_link', 'get_status_display')
|
||||
list_filter = (DeletedFilter, 'is_temporary', QualityLevelFilter, 'categories', 'tags')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
@@ -836,7 +842,7 @@ class ProductCategoryAdminWithPhotos(ProductCategoryAdmin):
|
||||
|
||||
|
||||
@admin.register(KitItem)
|
||||
class KitItemAdmin(admin.ModelAdmin):
|
||||
class KitItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ['__str__', 'kit', 'get_type', 'quantity', 'has_priorities']
|
||||
list_filter = ['kit']
|
||||
list_select_related = ['kit', 'product', 'variant_group']
|
||||
@@ -856,7 +862,7 @@ class KitItemAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(SKUCounter)
|
||||
class SKUCounterAdmin(admin.ModelAdmin):
|
||||
class SKUCounterAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ['counter_type', 'current_value', 'get_next_preview']
|
||||
list_filter = ['counter_type']
|
||||
readonly_fields = ['get_next_preview']
|
||||
@@ -879,7 +885,7 @@ class SKUCounterAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(CostPriceHistory)
|
||||
class CostPriceHistoryAdmin(admin.ModelAdmin):
|
||||
class CostPriceHistoryAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
list_display = ['product', 'get_price_change', 'reason', 'created_at']
|
||||
list_filter = ['reason', 'created_at', 'product']
|
||||
search_fields = ['product__name', 'product__sku', 'notes']
|
||||
@@ -965,7 +971,7 @@ class ConfigurableProductAttributeInline(admin.TabularInline):
|
||||
|
||||
|
||||
@admin.register(ConfigurableProduct)
|
||||
class ConfigurableProductAdmin(admin.ModelAdmin):
|
||||
class ConfigurableProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""Админка для вариативных товаров"""
|
||||
list_display = ('name', 'sku', 'status', 'get_options_count', 'created_at')
|
||||
list_filter = ('status', 'created_at')
|
||||
|
||||
91
myproject/tenants/admin_mixins.py
Normal file
91
myproject/tenants/admin_mixins.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Миксины для Django Admin в мультитенантной архитектуре.
|
||||
|
||||
ЗАЧЕМ ЭТО НУЖНО:
|
||||
================
|
||||
В django-tenants есть две схемы БД:
|
||||
- public: общая схема (тенанты, домены, пользователи)
|
||||
- tenant: отдельная схема для каждого магазина (товары, заказы, клиенты)
|
||||
|
||||
Проблема: Django Admin регистрирует ВСЕ модели глобально.
|
||||
Когда суперадмин заходит на localhost/admin/ (public схема),
|
||||
Django пытается показать sidebar со ВСЕМИ моделями, включая
|
||||
tenant-only модели (Order, Product, UserRole).
|
||||
|
||||
Но таблицы этих моделей существуют только в схемах тенантов!
|
||||
В public схеме их нет -> ошибка "relation does not exist".
|
||||
|
||||
РЕШЕНИЕ:
|
||||
========
|
||||
Миксин TenantAdminOnlyMixin скрывает модель от админки,
|
||||
если мы находимся в public схеме. Модель просто не показывается
|
||||
в sidebar и недоступна по URL.
|
||||
|
||||
ИСПОЛЬЗОВАНИЕ:
|
||||
==============
|
||||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
...
|
||||
|
||||
Теперь Order будет виден только в админке тенанта (shop.localhost/admin/),
|
||||
но не в public админке (localhost/admin/).
|
||||
"""
|
||||
from django_tenants.utils import get_public_schema_name
|
||||
|
||||
|
||||
class TenantAdminOnlyMixin:
|
||||
"""
|
||||
Миксин для скрытия tenant-only моделей от public admin.
|
||||
|
||||
Как работает:
|
||||
- has_module_permission() вызывается Django для проверки,
|
||||
показывать ли модель в sidebar админки
|
||||
- Мы проверяем текущую схему БД
|
||||
- Если это public схема -> возвращаем False (скрыть)
|
||||
- Если это схема тенанта -> возвращаем True (показать)
|
||||
"""
|
||||
|
||||
def has_module_permission(self, request):
|
||||
"""
|
||||
Скрыть модуль (группу моделей) от public schema админки.
|
||||
|
||||
Вызывается при отрисовке sidebar и при доступе к URL модели.
|
||||
"""
|
||||
from django.db import connection
|
||||
|
||||
# Получаем имя public схемы (обычно 'public')
|
||||
public_schema = get_public_schema_name()
|
||||
|
||||
# Если мы в public схеме - скрываем модель
|
||||
if connection.schema_name == public_schema:
|
||||
return False
|
||||
|
||||
# В схеме тенанта - показываем (используем стандартную проверку)
|
||||
return super().has_module_permission(request)
|
||||
|
||||
|
||||
class PublicAdminOnlyMixin:
|
||||
"""
|
||||
Обратный миксин - показывать модель ТОЛЬКО в public admin.
|
||||
|
||||
Полезно для моделей типа Client, Domain, TenantRegistration,
|
||||
которые должны быть доступны только суперадмину на главном домене.
|
||||
|
||||
Примечание: обычно не нужен, так как эти модели и так
|
||||
существуют только в public схеме.
|
||||
"""
|
||||
|
||||
def has_module_permission(self, request):
|
||||
"""Показать модуль только в public schema админке."""
|
||||
from django.db import connection
|
||||
|
||||
public_schema = get_public_schema_name()
|
||||
|
||||
# Показываем только в public схеме
|
||||
if connection.schema_name == public_schema:
|
||||
return super().has_module_permission(request)
|
||||
|
||||
return False
|
||||
@@ -1,10 +1,16 @@
|
||||
from django.contrib import admin
|
||||
from user_roles.models import Role, UserRole
|
||||
from user_roles.mixins import OwnerOnlyAdminMixin
|
||||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
class RoleAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админка ролей.
|
||||
TenantAdminOnlyMixin - скрывает от public admin (localhost/admin/),
|
||||
так как таблица Role существует только в схемах тенантов.
|
||||
"""
|
||||
list_display = ['code', 'name', 'is_system']
|
||||
list_filter = ['is_system']
|
||||
search_fields = ['code', 'name']
|
||||
@@ -18,10 +24,12 @@ class RoleAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(UserRole)
|
||||
class UserRoleAdmin(OwnerOnlyAdminMixin, admin.ModelAdmin):
|
||||
class UserRoleAdmin(TenantAdminOnlyMixin, OwnerOnlyAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админка ролей пользователей.
|
||||
Доступна только владельцу.
|
||||
|
||||
TenantAdminOnlyMixin - скрывает от public admin (таблица только в tenant схемах)
|
||||
OwnerOnlyAdminMixin - доступна только владельцу магазина
|
||||
|
||||
ВАЖНО: UserRole изолирован по тенантам автоматически через django-tenants,
|
||||
поэтому владелец видит только пользователей своего магазина!
|
||||
|
||||
@@ -4,12 +4,25 @@
|
||||
ВАЖНО: Этот 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()
|
||||
|
||||
|
||||
class RoleBasedPermissionBackend(ModelBackend):
|
||||
"""
|
||||
Backend, который предоставляет права на основе роли пользователя в текущем тенанте.
|
||||
@@ -86,6 +99,11 @@ class RoleBasedPermissionBackend(ModelBackend):
|
||||
if user_obj.is_superuser:
|
||||
return True
|
||||
|
||||
# ВАЖНО: В public схеме нет таблиц UserRole/Role!
|
||||
# Используем только стандартные Django permissions.
|
||||
if _is_public_schema():
|
||||
return False
|
||||
|
||||
# Получаем роль пользователя в текущем тенанте
|
||||
# ВАЖНО: RoleService работает с текущей tenant schema!
|
||||
user_role = RoleService.get_user_role(user_obj)
|
||||
@@ -132,6 +150,11 @@ class RoleBasedPermissionBackend(ModelBackend):
|
||||
if user_obj.is_superuser:
|
||||
return True
|
||||
|
||||
# ВАЖНО: В public схеме нет таблиц UserRole/Role!
|
||||
# Используем только стандартные Django permissions.
|
||||
if _is_public_schema():
|
||||
return False
|
||||
|
||||
# Получаем роль пользователя в текущем тенанте
|
||||
user_role = RoleService.get_user_role(user_obj)
|
||||
if not user_role:
|
||||
|
||||
Reference in New Issue
Block a user