Обновления в myproject: настройки, URL и middleware доступа к админке

This commit is contained in:
2026-01-08 22:16:26 +03:00
parent 728a406b04
commit f94af70f7f
3 changed files with 89 additions and 45 deletions

View File

@@ -1,23 +1,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Middleware для ограничения доступа к админке Django на поддоменах тенантов. Middleware для ограничения доступа к админке Django.
SECURITY: Этот middleware обеспечивает дополнительный слой защиты, SECURITY: Этот middleware обеспечивает изоляцию аутентификации между
блокируя доступ владельцев тенантов к /admin/ даже если у них PlatformAdmin (public schema) и CustomUser (tenant schemas).
случайно установлен is_staff=True.
Правила доступа: Правила доступа к /admin/:
- На tenant поддоменах (shop1.localhost): только is_superuser=True может входить в /admin/ - На PUBLIC домене: только PlatformAdmin может входить
- На public схеме (localhost): обычные правила Django (is_staff=True достаточно) - На TENANT доменах: только PlatformAdmin с is_superuser=True (для поддержки)
- Суперпользователи имеют доступ везде или CustomUser с is_superuser=True (системный пользователь тенанта)
""" """
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.db import connection
class TenantAdminAccessMiddleware: class TenantAdminAccessMiddleware:
""" """
Дополнительный слой безопасности: блокирует доступ владельцев тенантов к /admin/ Middleware для контроля доступа к Django Admin.
даже если у них случайно установлен is_staff=True.
Обеспечивает разделение между:
- PlatformAdmin (администраторы платформы) на public домене
- CustomUser (пользователи тенантов) на tenant доменах
""" """
def __init__(self, get_response): def __init__(self, get_response):
@@ -26,20 +29,42 @@ class TenantAdminAccessMiddleware:
def __call__(self, request): def __call__(self, request):
# Проверяем, это admin URL? # Проверяем, это admin URL?
if request.path.startswith('/admin/'): if request.path.startswith('/admin/'):
from django.db import connection # Импортируем здесь чтобы избежать circular imports
from platform_admin.models import PlatformAdmin
# Получаем текущую schema
schema_name = getattr(connection, 'schema_name', 'public')
is_public = schema_name == 'public'
# Если мы в tenant схеме (не public)
if hasattr(connection, 'tenant') and connection.tenant:
# Проверяем: это не public схема?
if connection.tenant.schema_name != 'public':
# Проверяем наличие атрибута user (добавляется AuthenticationMiddleware) # Проверяем наличие атрибута user (добавляется AuthenticationMiddleware)
if hasattr(request, 'user'): if hasattr(request, 'user') and request.user.is_authenticated:
# Если пользователь авторизован, но НЕ суперпользователь - блокируем user = request.user
if request.user.is_authenticated and not request.user.is_superuser: is_platform_admin = isinstance(user, PlatformAdmin)
if is_public:
# PUBLIC DOMAIN: только PlatformAdmin
if not is_platform_admin:
return HttpResponseForbidden( return HttpResponseForbidden(
"Доступ запрещен. Только системные администраторы могут " "Доступ запрещен. Админка платформы доступна только "
"заходить в админ-панель на поддоменах тенантов. " "для администраторов платформы. "
"Используйте панель управления тенанта." "Если вы владелец магазина, перейдите на домен вашего магазина."
)
else:
# TENANT DOMAIN: PlatformAdmin (superuser) или CustomUser (superuser)
if is_platform_admin:
# PlatformAdmin на tenant домене - только суперадмин
if not user.is_superuser:
return HttpResponseForbidden(
"Доступ запрещен. Только суперадминистраторы платформы "
"могут входить в админку тенантов."
)
else:
# CustomUser на tenant домене
if not user.is_superuser:
return HttpResponseForbidden(
"Доступ запрещен. Только системные администраторы тенанта "
"могут входить в Django Admin. "
"Используйте панель управления магазином."
) )
response = self.get_response(request) response = self.get_response(request)

View File

@@ -58,6 +58,7 @@ CSRF_USE_SESSIONS = True # Рекомендуется для мультихос
SHARED_APPS = [ SHARED_APPS = [
'django_tenants', # ОБЯЗАТЕЛЬНО ПЕРВЫМ! 'django_tenants', # ОБЯЗАТЕЛЬНО ПЕРВЫМ!
'tenants', # Приложение с моделями тенантов 'tenants', # Приложение с моделями тенантов
'platform_admin', # Администраторы платформы (отдельная модель от CustomUser)
# Django встроенные приложения # Django встроенные приложения
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -67,12 +68,8 @@ SHARED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# Accounts должен быть в shared для CustomUser (используется в админке)
'accounts',
# Celery results (для сохранения статуса асинхронных задач) # Celery results (для сохранения статуса асинхронных задач)
'django_celery_results', 'django_celery_results',
'simple_history', # Перенесли сюда для корректной инициализации моделей
] ]
# Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные) # Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные)
@@ -80,6 +77,11 @@ TENANT_APPS = [
'django.contrib.contenttypes', # Дублируем для tenant схем 'django.contrib.contenttypes', # Дублируем для tenant схем
'django.contrib.auth', # Дублируем для tenant схем 'django.contrib.auth', # Дублируем для tenant схем
# CustomUser - TENANT-ONLY модель!
# Каждый тенант имеет свою таблицу accounts_customuser в своей схеме
# Один email в разных тенантах = разные пользователи (полная изоляция)
'accounts',
# Приложения с бизнес-логикой (изолированные для каждого магазина) # Приложения с бизнес-логикой (изолированные для каждого магазина)
'nested_admin', 'nested_admin',
'django_filters', # Фильтрация данных 'django_filters', # Фильтрация данных
@@ -90,6 +92,7 @@ TENANT_APPS = [
'inventory', # Складской учет 'inventory', # Складской учет
'pos', # POS Terminal 'pos', # POS Terminal
'system_settings', # Системные настройки компании (только для владельца) 'system_settings', # Системные настройки компании (только для владельца)
# TODO: 'simple_history' - вернуть позже для истории изменений
] ]
# Объединяем для INSTALLED_APPS # Объединяем для INSTALLED_APPS
@@ -137,11 +140,19 @@ if DEBUG_TOOLBAR_ENABLED:
# AUTHENTICATION BACKENDS # AUTHENTICATION BACKENDS
# ============================================ # ============================================
# Кастомный backend для связи ролей с Django permissions API # Authentication backends для изолированной аутентификации
# ВАЖНО: Этот backend работает с ролями из tenant schema, НЕ трогая public schema! # ВАЖНО: Порядок важен! Django пробует backends по очереди до первого успеха
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'user_roles.auth_backend.RoleBasedPermissionBackend', # Наш кастомный backend для ролей # 1. PlatformAdmin - администраторы платформы (public schema)
'django.contrib.auth.backends.ModelBackend', # Стандартный backend (для superuser и т.д.) # Работает на public домене, а также на tenant доменах для суперадминов (поддержка)
'platform_admin.backends.PlatformAdminBackend',
# 2. CustomUser - пользователи тенантов (tenant schema)
# Работает ТОЛЬКО на tenant доменах, ищет пользователя в текущей schema
'accounts.backends.TenantUserBackend',
# 3. Ролевой backend для проверки permissions (работает с RoleService)
'user_roles.auth_backend.RoleBasedPermissionBackend',
] ]
@@ -248,6 +259,9 @@ TENANT_DOMAIN_BASE = env('TENANT_DOMAIN_BASE', default='localhost:8000')
# Использовать HTTPS для ссылок (в проде True, локально False) # Использовать HTTPS для ссылок (в проде True, локально False)
USE_HTTPS = env.bool('USE_HTTPS', default=False) USE_HTTPS = env.bool('USE_HTTPS', default=False)
# Email техподдержки платформы (создаётся скрытый аккаунт в каждом тенанте)
PLATFORM_SUPPORT_EMAIL = env('PLATFORM_SUPPORT_EMAIL', default=None)
# ============================================ # ============================================
# SESSION CONFIGURATION # SESSION CONFIGURATION
@@ -491,18 +505,10 @@ DEFAULT_FROM_EMAIL = 'noreply@inventory.by'
# AUTHENTICATION # AUTHENTICATION
# ============================================ # ============================================
AUTH_USER_MODEL = 'accounts.CustomUser' # AUTH_USER_MODEL указывает на PlatformAdmin (public schema)
# Это нужно для django.contrib.admin.LogEntry и других Django-компонентов
# CustomUser живёт в TENANT_APPS и НЕ является AUTH_USER_MODEL
# ============================================ AUTH_USER_MODEL = 'platform_admin.PlatformAdmin'
# TENANT ADMIN AUTO-CREATION
# ============================================
# При создании нового тенанта автоматически создается суперпользователь
# с указанными credentials для доступа к админке тенанта
TENANT_ADMIN_EMAIL = env('TENANT_ADMIN_EMAIL')
TENANT_ADMIN_PASSWORD = env('TENANT_ADMIN_PASSWORD')
TENANT_ADMIN_NAME = env('TENANT_ADMIN_NAME')
# ============================================ # ============================================

View File

@@ -3,8 +3,12 @@
URL configuration for the PUBLIC schema (inventory.by domain). URL configuration for the PUBLIC schema (inventory.by domain).
This is the main domain where: This is the main domain where:
- Super admin can access admin panel - PlatformAdmin can access admin panel and dashboard
- Tenant registration is available - Tenant registration is available
- Password setup for new tenant owners
IMPORTANT: На public домене НЕ работает аутентификация CustomUser!
CustomUser живёт в tenant schemas и доступен только на tenant доменах.
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
@@ -14,9 +18,18 @@ from django.urls import re_path
from django.views.static import serve from django.views.static import serve
urlpatterns = [ urlpatterns = [
# Django Admin для PlatformAdmin
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('tenants.urls')), # Роуты для регистрации тенантов (/, /register/, /register/success/)
path('accounts/', include('accounts.urls')), # Роуты для установки пароля владельцами тенантов # Панель управления платформой (login/logout/dashboard для PlatformAdmin)
path('platform/', include('platform_admin.urls')),
# Регистрация тенантов (/, /register/, /register/success/)
path('', include('tenants.urls')),
# Установка пароля владельцами тенантов (только setup-password)
# НЕ включаем полный accounts.urls - на public домене нельзя логиниться как CustomUser
path('accounts/', include('accounts.urls')),
] ]
# Serve media files in development # Serve media files in development