SECURITY: Ограничен доступ владельцев тенантов к админке Django
Исправлена критическая уязвимость безопасности, которая потенциально позволяла владельцам тенантов получить доступ к админ-панели Django. Изменения: - Добавлены явные setdefault для is_staff=False и is_superuser=False в CustomUserManager.create_user() - Добавлены явные флаги безопасности при создании владельца тенанта - Добавлены явные флаги безопасности при создании пользователей через систему ролей - Создан TenantAdminAccessMiddleware для защиты /admin/ на уровне middleware - Создана миграция данных для исправления флагов у существующих пользователей Реализована трёхуровневая защита (Defense-in-Depth): 1. Уровень модели: явные дефолты в create_user() 2. Уровень views: явные флаги при создании 3. Уровень middleware: runtime блокировка доступа Файлы: - accounts/models.py: явные флаги в create_user() - tenants/admin.py: явные флаги при создании владельца - user_roles/views.py: явные флаги при создании через роли - myproject/admin_access_middleware.py: новый middleware - myproject/settings.py: регистрация middleware - accounts/migrations/0002_fix_owner_staff_flags.py: миграция данных ВАЖНО: После применения этого коммита необходимо выполнить: 1. python manage.py migrate accounts 2. python manage.py migrate_schemas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
94
myproject/accounts/migrations/0002_fix_owner_staff_flags.py
Normal file
94
myproject/accounts/migrations/0002_fix_owner_staff_flags.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Миграция данных для исправления флагов is_staff/is_superuser у существующих пользователей.
|
||||||
|
|
||||||
|
SECURITY FIX: Убираем флаги is_staff и is_superuser у владельцев тенантов и обычных пользователей,
|
||||||
|
которые могли быть созданы до внедрения явных проверок безопасности.
|
||||||
|
|
||||||
|
Эта миграция должна быть запущена:
|
||||||
|
1. На public схеме: python manage.py migrate accounts
|
||||||
|
2. На всех tenant схемах: python manage.py migrate_schemas
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def fix_owner_staff_flags(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Исправляет флаги is_staff/is_superuser у существующих владельцев тенантов и обычных пользователей.
|
||||||
|
"""
|
||||||
|
User = apps.get_model('accounts', 'CustomUser')
|
||||||
|
|
||||||
|
# Пытаемся получить модели системы ролей (могут не существовать на момент миграции)
|
||||||
|
try:
|
||||||
|
UserRole = apps.get_model('user_roles', 'UserRole')
|
||||||
|
Role = apps.get_model('user_roles', 'Role')
|
||||||
|
|
||||||
|
# Находим роль owner
|
||||||
|
try:
|
||||||
|
owner_role = Role.objects.get(code='owner')
|
||||||
|
|
||||||
|
# Получаем всех пользователей с ролью owner
|
||||||
|
owner_user_ids = UserRole.objects.filter(
|
||||||
|
role=owner_role,
|
||||||
|
is_active=True
|
||||||
|
).values_list('user_id', flat=True)
|
||||||
|
|
||||||
|
# Убираем is_staff и is_superuser у всех владельцев
|
||||||
|
updated_staff = User.objects.filter(
|
||||||
|
id__in=owner_user_ids,
|
||||||
|
is_staff=True
|
||||||
|
).update(is_staff=False)
|
||||||
|
|
||||||
|
updated_super = User.objects.filter(
|
||||||
|
id__in=owner_user_ids,
|
||||||
|
is_superuser=True
|
||||||
|
).update(is_superuser=False)
|
||||||
|
|
||||||
|
if updated_staff > 0 or updated_super > 0:
|
||||||
|
print(f"[SECURITY FIX] Исправлено владельцев: is_staff={updated_staff}, is_superuser={updated_super}")
|
||||||
|
|
||||||
|
except Role.DoesNotExist:
|
||||||
|
print("[SECURITY FIX] Роль 'owner' не найдена, пропускаем исправление владельцев")
|
||||||
|
|
||||||
|
except LookupError:
|
||||||
|
# Модели user_roles еще не существуют - это нормально для новых инсталляций
|
||||||
|
print("[SECURITY FIX] Модели user_roles не найдены (это нормально для новых инсталляций)")
|
||||||
|
|
||||||
|
# Убираем is_staff у всех НЕ-суперпользователей (дополнительная безопасность)
|
||||||
|
# Только суперпользователи должны иметь is_staff=True
|
||||||
|
updated = User.objects.filter(
|
||||||
|
is_staff=True,
|
||||||
|
is_superuser=False
|
||||||
|
).update(is_staff=False)
|
||||||
|
|
||||||
|
if updated > 0:
|
||||||
|
print(f"[SECURITY FIX] Убран is_staff у {updated} НЕ-суперпользователей")
|
||||||
|
|
||||||
|
# Итоговая статистика
|
||||||
|
total_staff = User.objects.filter(is_staff=True).count()
|
||||||
|
total_super = User.objects.filter(is_superuser=True).count()
|
||||||
|
print(f"[SECURITY FIX] Текущее состояние: is_staff={total_staff}, is_superuser={total_super}")
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_fix(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Откат не требуется - мы только исправляем безопасность, не меняем структуру данных.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
"""
|
||||||
|
Миграция данных для исправления флагов безопасности.
|
||||||
|
|
||||||
|
ВАЖНО: Эту миграцию нужно запустить на всех тенантах командой:
|
||||||
|
python manage.py migrate_schemas
|
||||||
|
"""
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_owner_staff_flags, reverse_fix),
|
||||||
|
]
|
||||||
@@ -11,6 +11,12 @@ class CustomUserManager(BaseUserManager):
|
|||||||
email = self.normalize_email(email)
|
email = self.normalize_email(email)
|
||||||
# Generate a unique username based on email to satisfy the AbstractUser constraint
|
# Generate a unique username based on email to satisfy the AbstractUser constraint
|
||||||
username = email
|
username = email
|
||||||
|
|
||||||
|
# SECURITY FIX: Явно устанавливаем флаги безопасности в False по умолчанию
|
||||||
|
# Обычные пользователи НЕ должны иметь доступ к админке
|
||||||
|
extra_fields.setdefault('is_staff', False)
|
||||||
|
extra_fields.setdefault('is_superuser', False)
|
||||||
|
|
||||||
user = self.model(email=email, name=name, username=username, **extra_fields)
|
user = self.model(email=email, name=name, username=username, **extra_fields)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save(using=self._db)
|
user.save(using=self._db)
|
||||||
|
|||||||
44
myproject/myproject/admin_access_middleware.py
Normal file
44
myproject/myproject/admin_access_middleware.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Middleware для ограничения доступа к админке Django на поддоменах тенантов.
|
||||||
|
|
||||||
|
SECURITY: Этот middleware обеспечивает дополнительный слой защиты,
|
||||||
|
блокируя доступ владельцев тенантов к /admin/ даже если у них
|
||||||
|
случайно установлен is_staff=True.
|
||||||
|
|
||||||
|
Правила доступа:
|
||||||
|
- На tenant поддоменах (shop1.localhost): только is_superuser=True может входить в /admin/
|
||||||
|
- На public схеме (localhost): обычные правила Django (is_staff=True достаточно)
|
||||||
|
- Суперпользователи имеют доступ везде
|
||||||
|
"""
|
||||||
|
from django.http import HttpResponseForbidden
|
||||||
|
|
||||||
|
|
||||||
|
class TenantAdminAccessMiddleware:
|
||||||
|
"""
|
||||||
|
Дополнительный слой безопасности: блокирует доступ владельцев тенантов к /admin/
|
||||||
|
даже если у них случайно установлен is_staff=True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
# Проверяем, это admin URL?
|
||||||
|
if request.path.startswith('/admin/'):
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
# Если мы в tenant схеме (не public)
|
||||||
|
if hasattr(connection, 'tenant') and connection.tenant:
|
||||||
|
# Проверяем: это не public схема?
|
||||||
|
if connection.tenant.schema_name != 'public':
|
||||||
|
# Если пользователь авторизован, но НЕ суперпользователь - блокируем
|
||||||
|
if request.user.is_authenticated and not request.user.is_superuser:
|
||||||
|
return HttpResponseForbidden(
|
||||||
|
"Доступ запрещен. Только системные администраторы могут "
|
||||||
|
"заходить в админ-панель на поддоменах тенантов. "
|
||||||
|
"Используйте панель управления тенанта."
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
@@ -95,6 +95,7 @@ SHOW_PUBLIC_IF_NO_TENANT_FOUND = True
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django_tenants.middleware.main.TenantMainMiddleware', # ОБЯЗАТЕЛЬНО ПЕРВЫМ!
|
'django_tenants.middleware.main.TenantMainMiddleware', # ОБЯЗАТЕЛЬНО ПЕРВЫМ!
|
||||||
|
'myproject.admin_access_middleware.TenantAdminAccessMiddleware', # SECURITY: Ограничение доступа к админке
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|||||||
@@ -340,7 +340,9 @@ class TenantRegistrationAdmin(admin.ModelAdmin):
|
|||||||
owner = User.objects.create_user(
|
owner = User.objects.create_user(
|
||||||
email=registration.owner_email,
|
email=registration.owner_email,
|
||||||
name=registration.owner_name,
|
name=registration.owner_name,
|
||||||
password=None # Пароль будет установлен через ссылку
|
password=None, # Пароль будет установлен через ссылку
|
||||||
|
is_staff=False, # SECURITY: Владелец НЕ может входить в админку
|
||||||
|
is_superuser=False # SECURITY: Владелец НЕ суперпользователь
|
||||||
)
|
)
|
||||||
# Помечаем email как подтвержденный, так как владелец регистрировался с ним
|
# Помечаем email как подтвержденный, так как владелец регистрировался с ним
|
||||||
owner.is_email_confirmed = True
|
owner.is_email_confirmed = True
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ def user_role_create(request):
|
|||||||
email=email,
|
email=email,
|
||||||
name=name,
|
name=name,
|
||||||
password=password,
|
password=password,
|
||||||
is_email_confirmed=True
|
is_email_confirmed=True,
|
||||||
|
is_staff=False, # SECURITY: Пользователи ролей НЕ имеют доступа к админке
|
||||||
|
is_superuser=False # SECURITY: Пользователи ролей НЕ суперпользователи
|
||||||
)
|
)
|
||||||
|
|
||||||
# Назначаем роль
|
# Назначаем роль
|
||||||
|
|||||||
Reference in New Issue
Block a user