diff --git a/myproject/myproject/admin_access_middleware.py b/myproject/myproject/admin_access_middleware.py index 7c9e970..9b7d3e7 100644 --- a/myproject/myproject/admin_access_middleware.py +++ b/myproject/myproject/admin_access_middleware.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Middleware для ограничения доступа к админке Django. +Middleware для ограничения доступа к админке. SECURITY: Этот middleware обеспечивает изоляцию аутентификации между PlatformAdmin (public schema) и CustomUser (tenant schemas). @@ -10,13 +10,13 @@ PlatformAdmin (public schema) и CustomUser (tenant schemas). - На TENANT доменах: только PlatformAdmin с is_superuser=True (для поддержки) или CustomUser с is_superuser=True (системный пользователь тенанта) """ -from django.http import HttpResponseForbidden +from django.shortcuts import render from django.db import connection class TenantAdminAccessMiddleware: """ - Middleware для контроля доступа к Django Admin. + Middleware для контроля доступа к административным разделам. Обеспечивает разделение между: - PlatformAdmin (администраторы платформы) на public домене @@ -26,6 +26,12 @@ class TenantAdminAccessMiddleware: def __init__(self, get_response): self.get_response = get_response + def _render_error(self, request, message): + """Рендерит страницу ошибки доступа.""" + return render(request, 'errors/access_denied.html', { + 'message': message + }, status=403) + def __call__(self, request): # Проверяем, это admin URL? if request.path.startswith('/admin/'): @@ -44,27 +50,30 @@ class TenantAdminAccessMiddleware: if is_public: # PUBLIC DOMAIN: только PlatformAdmin if not is_platform_admin: - return HttpResponseForbidden( - "Доступ запрещен. Админка платформы доступна только " - "для администраторов платформы. " - "Если вы владелец магазина, перейдите на домен вашего магазина." + return self._render_error( + request, + 'Административная панель платформы доступна только ' + 'для администраторов платформы. ' + 'Если вы владелец магазина, перейдите на домен вашего магазина.' ) else: # TENANT DOMAIN: PlatformAdmin (superuser) или CustomUser (superuser) if is_platform_admin: # PlatformAdmin на tenant домене - только суперадмин if not user.is_superuser: - return HttpResponseForbidden( - "Доступ запрещен. Только суперадминистраторы платформы " - "могут входить в админку тенантов." + return self._render_error( + request, + 'Только суперадминистраторы платформы ' + 'могут получить доступ к этому разделу.' ) else: # CustomUser на tenant домене if not user.is_superuser: - return HttpResponseForbidden( - "Доступ запрещен. Только системные администраторы тенанта " - "могут входить в Django Admin. " - "Используйте панель управления магазином." + return self._render_error( + request, + 'Только системные администраторы тенанта ' + 'могут получить доступ к этому разделу. ' + 'Используйте панель управления магазином.' ) response = self.get_response(request) diff --git a/myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py b/myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py new file mode 100644 index 0000000..b799a2c --- /dev/null +++ b/myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 5.0.10 on 2026-01-09 20:42 + +import django.db.models.deletion +from django.db import migrations, models + + +def clear_invalid_user_references(apps, schema_editor): + """ + Очистить ссылки на PlatformAdmin в полях archived_by/deleted_by/user, + т.к. они теперь ссылаются на CustomUser. + + Устанавливаем NULL для всех существующих ссылок на пользователей, + чтобы избежать ошибок referential integrity при изменении типа ForeignKey. + """ + ProductCategory = apps.get_model('products', 'ProductCategory') + Product = apps.get_model('products', 'Product') + ProductKit = apps.get_model('products', 'ProductKit') + ConfigurableProduct = apps.get_model('products', 'ConfigurableProduct') + ProductImportJob = apps.get_model('products', 'ProductImportJob') + + # Очищаем все существующие ссылки (ставим NULL) + ProductCategory.objects.all().update(deleted_by=None) + Product.objects.all().update(archived_by=None) + ProductKit.objects.all().update(archived_by=None) + ConfigurableProduct.objects.all().update(archived_by=None) + + # ProductImportJob.user не может быть NULL (CASCADE), поэтому удаляем записи + # если они были созданы PlatformAdmin (что маловероятно) + ProductImportJob.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ('products', '0001_initial'), + ] + + operations = [ + # Сначала очищаем некорректные ссылки + migrations.RunPython(clear_invalid_user_references, reverse_code=migrations.RunPython.noop), + # Затем изменяем типы полей + migrations.AlterField( + model_name='configurableproduct', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'), + ), + migrations.AlterField( + model_name='product', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'), + ), + migrations.AlterField( + model_name='productcategory', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to='accounts.customuser', verbose_name='Удалена пользователем'), + ), + migrations.AlterField( + model_name='productimportjob', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to='accounts.customuser', verbose_name='Пользователь'), + ), + migrations.AlterField( + model_name='productkit', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'), + ), + ] diff --git a/myproject/products/models/base.py b/myproject/products/models/base.py index 185bffa..ad7a227 100644 --- a/myproject/products/models/base.py +++ b/myproject/products/models/base.py @@ -10,13 +10,9 @@ TODO: Унификация системы soft delete from django.db import models, transaction, IntegrityError from django.db.models import Q from django.utils import timezone -from django.contrib.auth import get_user_model from django.utils.text import slugify from unidecode import unidecode -# Получаем User модель один раз для использования в ForeignKey -User = get_user_model() - class SKUCounter(models.Model): """ @@ -186,7 +182,7 @@ class BaseProductEntity(models.Model): verbose_name="Время архивирования" ) archived_by = models.ForeignKey( - User, + 'accounts.CustomUser', on_delete=models.SET_NULL, null=True, blank=True, diff --git a/myproject/products/models/categories.py b/myproject/products/models/categories.py index dc29b10..9b9eca9 100644 --- a/myproject/products/models/categories.py +++ b/myproject/products/models/categories.py @@ -5,13 +5,10 @@ from django.db import models from django.db.models import Q from django.utils import timezone from django.core.exceptions import ValidationError -from django.contrib.auth import get_user_model from .managers import ActiveManager, SoftDeleteManager, SoftDeleteQuerySet from ..services.slug_service import SlugService -User = get_user_model() - class ProductCategory(models.Model): """ @@ -30,7 +27,7 @@ class ProductCategory(models.Model): is_deleted = models.BooleanField(default=False, verbose_name="Удалена", db_index=True) deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Время удаления") deleted_by = models.ForeignKey( - User, + 'accounts.CustomUser', on_delete=models.SET_NULL, null=True, blank=True, diff --git a/myproject/products/models/import_job.py b/myproject/products/models/import_job.py index 5dfbeaa..863357d 100644 --- a/myproject/products/models/import_job.py +++ b/myproject/products/models/import_job.py @@ -2,9 +2,6 @@ Модель для отслеживания задач импорта товаров. """ from django.db import models -from django.contrib.auth import get_user_model - -User = get_user_model() class ProductImportJob(models.Model): @@ -28,7 +25,7 @@ class ProductImportJob(models.Model): # Пользователь user = models.ForeignKey( - User, + 'accounts.CustomUser', on_delete=models.CASCADE, related_name='product_import_jobs', verbose_name="Пользователь" diff --git a/myproject/templates/errors/access_denied.html b/myproject/templates/errors/access_denied.html new file mode 100644 index 0000000..4d37e56 --- /dev/null +++ b/myproject/templates/errors/access_denied.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} + +{% block title %}Доступ запрещён{% endblock %} + +{% block content %} +
+
+
+
+
+

⚠️ Доступ запрещён

+
+
+

{{ message }}

+
+
+ + ← На главную + + {% if user.is_authenticated %} + + Выйти + + {% else %} + + Войти + + {% endif %} +
+
+
+
+
+
+{% endblock %} diff --git a/myproject/user_roles/auth_backend.py b/myproject/user_roles/auth_backend.py index eed988e..81f387b 100644 --- a/myproject/user_roles/auth_backend.py +++ b/myproject/user_roles/auth_backend.py @@ -23,6 +23,15 @@ def _is_public_schema(): 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, который предоставляет права на основе роли пользователя в текущем тенанте. @@ -86,15 +95,20 @@ class RoleBasedPermissionBackend(ModelBackend): Returns: bool: True если пользователь имеет разрешение """ - # Сначала проверяем стандартные permissions через ModelBackend - # (для superuser, staff с Django permissions и т.д.) - if super().has_perm(user_obj, perm, obj): - return 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 @@ -104,7 +118,7 @@ class RoleBasedPermissionBackend(ModelBackend): if _is_public_schema(): return False - # Получаем роль пользователя в текущем тенанте + # Для CustomUser получаем роль пользователя в текущем тенанте # ВАЖНО: RoleService работает с текущей tenant schema! user_role = RoleService.get_user_role(user_obj) if not user_role: @@ -138,14 +152,18 @@ class RoleBasedPermissionBackend(ModelBackend): Returns: bool: True если пользователь имеет хотя бы одно разрешение для приложения """ - # Сначала проверяем стандартные permissions через ModelBackend - if super().has_module_perms(user_obj, app_label): - return 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 @@ -155,7 +173,7 @@ class RoleBasedPermissionBackend(ModelBackend): if _is_public_schema(): return False - # Получаем роль пользователя в текущем тенанте + # Для CustomUser получаем роль пользователя в текущем тенанте user_role = RoleService.get_user_role(user_obj) if not user_role: return False