feat: implement user roles system with tenant isolation
Добавлена система ролей пользователей для управления доступом в multi-tenant приложении. Новые роли: - Владелец (Owner): полный доступ, управление пользователями - Менеджер (Manager): управление заказами, клиентами, товарами, складом - Флорист (Florist): работа с заказами и складскими операциями - Курьер (Courier): роль создана, права будут определены позже Архитектура: - Роли автоматически изолируются по тенантам через django-tenants (TENANT_APPS) - Не требуется FK на Client/Tenant - изоляция через PostgreSQL schemas - Роли автоматически создаются при создании нового тенанта Компоненты: - user_roles/models.py: модели Role и UserRole - user_roles/services.py: RoleService для управления ролями - user_roles/decorators.py: @role_required, @owner_required - user_roles/mixins.py: RoleBasedAdminMixin, OwnerOnlyAdminMixin - user_roles/admin.py: админка для управления ролями - user_roles/management/commands/init_roles.py: команда для инициализации Изменения: - accounts/models.py: добавлены helper методы (is_owner, has_role, etc) - settings.py: добавлен user_roles в TENANT_APPS - tenants/admin.py: автосоздание ролей при создании тенанта 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -79,3 +79,33 @@ class CustomUser(AbstractUser):
|
||||
self.is_email_confirmed = True
|
||||
self.email_confirmed_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
def get_tenant_role(self):
|
||||
"""Получить роль пользователя в текущем тенанте"""
|
||||
from user_roles.services import RoleService
|
||||
return RoleService.get_user_role(self)
|
||||
|
||||
def has_role(self, *role_codes):
|
||||
"""Проверить, имеет ли пользователь одну из указанных ролей"""
|
||||
from user_roles.services import RoleService
|
||||
return RoleService.user_has_role(self, *role_codes)
|
||||
|
||||
@property
|
||||
def is_owner(self):
|
||||
"""Является ли пользователь владельцем"""
|
||||
return self.has_role('owner')
|
||||
|
||||
@property
|
||||
def is_manager(self):
|
||||
"""Является ли пользователь менеджером"""
|
||||
return self.has_role('manager')
|
||||
|
||||
@property
|
||||
def is_florist(self):
|
||||
"""Является ли пользователь флористом"""
|
||||
return self.has_role('florist')
|
||||
|
||||
@property
|
||||
def is_courier(self):
|
||||
"""Является ли пользователь курьером"""
|
||||
return self.has_role('courier')
|
||||
|
||||
@@ -70,6 +70,7 @@ TENANT_APPS = [
|
||||
'simple_history', # История изменений для каждого тенанта
|
||||
'nested_admin',
|
||||
'django_filters', # Фильтрация данных
|
||||
'user_roles', # Роли пользователей
|
||||
'customers', # Клиенты магазина
|
||||
'products', # Товары и категории
|
||||
'orders', # Заказы
|
||||
|
||||
@@ -310,6 +310,17 @@ class TenantRegistrationAdmin(admin.ModelAdmin):
|
||||
logger.error(f"Ошибка при создании статусов заказов: {e}", exc_info=True)
|
||||
# Не прерываем процесс, т.к. это не критично
|
||||
|
||||
# Создаем системные роли пользователей
|
||||
logger.info(f"Создание системных ролей для тенанта: {client.id}")
|
||||
from user_roles.services import RoleService
|
||||
|
||||
try:
|
||||
RoleService.create_default_roles()
|
||||
logger.info("Системные роли успешно созданы")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании ролей: {e}", exc_info=True)
|
||||
# Не прерываем процесс, т.к. это не критично
|
||||
|
||||
# Создаем системные способы оплаты
|
||||
logger.info(f"Создание системных способов оплаты для тенанта: {client.id}")
|
||||
from django.core.management import call_command
|
||||
|
||||
0
myproject/user_roles/__init__.py
Normal file
0
myproject/user_roles/__init__.py
Normal file
39
myproject/user_roles/admin.py
Normal file
39
myproject/user_roles/admin.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.contrib import admin
|
||||
from user_roles.models import Role, UserRole
|
||||
from user_roles.mixins import OwnerOnlyAdminMixin
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['code', 'name', 'is_system']
|
||||
list_filter = ['is_system']
|
||||
search_fields = ['code', 'name']
|
||||
readonly_fields = ['created_at']
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Запрет удаления системных ролей"""
|
||||
if obj and obj.is_system:
|
||||
return False
|
||||
return super().has_delete_permission(request, obj)
|
||||
|
||||
|
||||
@admin.register(UserRole)
|
||||
class UserRoleAdmin(OwnerOnlyAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Админка ролей пользователей.
|
||||
Доступна только владельцу.
|
||||
|
||||
ВАЖНО: UserRole изолирован по тенантам автоматически через django-tenants,
|
||||
поэтому владелец видит только пользователей своего магазина!
|
||||
"""
|
||||
list_display = ['user', 'role', 'is_active', 'created_at']
|
||||
list_filter = ['role', 'is_active']
|
||||
search_fields = ['user__email', 'user__name']
|
||||
readonly_fields = ['created_at', 'created_by']
|
||||
autocomplete_fields = ['user']
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Автоматически устанавливаем created_by"""
|
||||
if not change:
|
||||
obj.created_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
6
myproject/user_roles/apps.py
Normal file
6
myproject/user_roles/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UserRolesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'user_roles'
|
||||
40
myproject/user_roles/decorators.py
Normal file
40
myproject/user_roles/decorators.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from functools import wraps
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
from user_roles.services import RoleService
|
||||
|
||||
|
||||
def role_required(*role_codes):
|
||||
"""
|
||||
Декоратор для проверки роли пользователя.
|
||||
|
||||
Использование:
|
||||
@role_required('owner', 'manager')
|
||||
def my_view(request):
|
||||
...
|
||||
"""
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return redirect('login')
|
||||
|
||||
if RoleService.user_has_role(request.user, *role_codes):
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
messages.error(request, 'У вас нет прав для выполнения этого действия.')
|
||||
return HttpResponseForbidden('Access denied')
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def owner_required(view_func):
|
||||
"""Декоратор для проверки роли Владелец"""
|
||||
return role_required('owner')(view_func)
|
||||
|
||||
|
||||
def manager_or_owner_required(view_func):
|
||||
"""Декоратор для проверки роли Менеджер или Владелец"""
|
||||
return role_required('owner', 'manager')(view_func)
|
||||
0
myproject/user_roles/management/__init__.py
Normal file
0
myproject/user_roles/management/__init__.py
Normal file
15
myproject/user_roles/management/commands/init_roles.py
Normal file
15
myproject/user_roles/management/commands/init_roles.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from user_roles.services import RoleService
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Создает дефолтные роли для текущего тенанта'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Создание дефолтных ролей...')
|
||||
|
||||
RoleService.create_default_roles()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('✓ Роли успешно созданы')
|
||||
)
|
||||
48
myproject/user_roles/migrations/0001_initial.py
Normal file
48
myproject/user_roles/migrations/0001_initial.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.0.10 on 2025-12-01 15:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(choices=[('owner', 'Владелец'), ('manager', 'Менеджер'), ('florist', 'Флорист'), ('courier', 'Курьер')], max_length=20, unique=True, verbose_name='Код роли')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('is_system', models.BooleanField(default=True, help_text='Системные роли нельзя удалить', verbose_name='Системная роль')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Роль',
|
||||
'verbose_name_plural': 'Роли',
|
||||
'ordering': ['code'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_user_roles', to=settings.AUTH_USER_MODEL, verbose_name='Создал')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='users', to='user_roles.role', verbose_name='Роль')),
|
||||
('user', models.OneToOneField(help_text='Пользователь из public schema (SHARED_APPS)', on_delete=django.db.models.deletion.CASCADE, related_name='tenant_role', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Роль пользователя',
|
||||
'verbose_name_plural': 'Роли пользователей',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
myproject/user_roles/migrations/__init__.py
Normal file
0
myproject/user_roles/migrations/__init__.py
Normal file
74
myproject/user_roles/mixins.py
Normal file
74
myproject/user_roles/mixins.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from user_roles.services import RoleService
|
||||
|
||||
|
||||
class RoleBasedAdminMixin:
|
||||
"""
|
||||
Миксин для ModelAdmin с проверкой ролей.
|
||||
|
||||
Использование:
|
||||
class MyAdmin(RoleBasedAdminMixin, admin.ModelAdmin):
|
||||
required_roles = ['owner', 'manager']
|
||||
"""
|
||||
required_roles = [] # Роли, которые имеют доступ
|
||||
|
||||
def has_module_permission(self, request):
|
||||
"""Проверка доступа к модулю"""
|
||||
if not super().has_module_permission(request):
|
||||
return False
|
||||
|
||||
if not self.required_roles:
|
||||
return True # Нет ограничений
|
||||
|
||||
return RoleService.user_has_role(request.user, *self.required_roles)
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Проверка доступа на просмотр"""
|
||||
if not super().has_view_permission(request, obj):
|
||||
return False
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
return RoleService.user_has_role(request.user, *self.required_roles)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Проверка доступа на добавление"""
|
||||
if not super().has_add_permission(request):
|
||||
return False
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
return RoleService.user_has_role(request.user, *self.required_roles)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Проверка доступа на изменение"""
|
||||
if not super().has_change_permission(request, obj):
|
||||
return False
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
return RoleService.user_has_role(request.user, *self.required_roles)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Проверка доступа на удаление"""
|
||||
if not super().has_delete_permission(request, obj):
|
||||
return False
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
return RoleService.user_has_role(request.user, *self.required_roles)
|
||||
|
||||
|
||||
class OwnerOnlyAdminMixin(RoleBasedAdminMixin):
|
||||
"""Миксин для админки, доступной только владельцу"""
|
||||
required_roles = ['owner']
|
||||
|
||||
|
||||
class ManagerOwnerAdminMixin(RoleBasedAdminMixin):
|
||||
"""Миксин для админки, доступной менеджеру и владельцу"""
|
||||
required_roles = ['owner', 'manager']
|
||||
101
myproject/user_roles/models.py
Normal file
101
myproject/user_roles/models.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
"""
|
||||
Роль пользователя.
|
||||
|
||||
ВАЖНО: Модель находится в TENANT_APPS, поэтому автоматически изолируется
|
||||
по тенантам через django-tenants. Каждый тенант имеет свой набор ролей
|
||||
в своей PostgreSQL schema.
|
||||
|
||||
Не нужно явно связывать с тенантом через FK - изоляция происходит автоматически!
|
||||
"""
|
||||
OWNER = 'owner'
|
||||
MANAGER = 'manager'
|
||||
FLORIST = 'florist'
|
||||
COURIER = 'courier'
|
||||
|
||||
ROLE_CHOICES = [
|
||||
(OWNER, 'Владелец'),
|
||||
(MANAGER, 'Менеджер'),
|
||||
(FLORIST, 'Флорист'),
|
||||
(COURIER, 'Курьер'),
|
||||
]
|
||||
|
||||
code = models.CharField(
|
||||
max_length=20,
|
||||
choices=ROLE_CHOICES,
|
||||
unique=True,
|
||||
verbose_name="Код роли"
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Название"
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Описание"
|
||||
)
|
||||
is_system = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Системная роль",
|
||||
help_text="Системные роли нельзя удалить"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Роль"
|
||||
verbose_name_plural = "Роли"
|
||||
ordering = ['code']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UserRole(models.Model):
|
||||
"""
|
||||
Роль пользователя в текущем тенанте.
|
||||
|
||||
ВАЖНО: Эта модель НЕ связывает пользователя с тенантом!
|
||||
Связь с тенантом обеспечивается автоматически через django-tenants
|
||||
(модель в TENANT_APPS = находится в schema тенанта).
|
||||
|
||||
UserRole просто говорит: "этот пользователь имеет эту роль" (в рамках текущего тенанта).
|
||||
Один пользователь = одна роль в рамках одного тенанта.
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='tenant_role',
|
||||
verbose_name="Пользователь",
|
||||
help_text="Пользователь из public schema (SHARED_APPS)"
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
Role,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='users',
|
||||
verbose_name="Роль"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='created_user_roles',
|
||||
verbose_name="Создал"
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Активен"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Роль пользователя"
|
||||
verbose_name_plural = "Роли пользователей"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email} - {self.role.name}"
|
||||
100
myproject/user_roles/services.py
Normal file
100
myproject/user_roles/services.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from django.db import transaction
|
||||
from user_roles.models import Role, UserRole
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""Сервис для управления ролями"""
|
||||
|
||||
@staticmethod
|
||||
def create_default_roles():
|
||||
"""
|
||||
Создает системные роли для тенанта.
|
||||
Вызывается при создании тенанта (автоматически через signals или admin).
|
||||
|
||||
ВАЖНО: Эта функция вызывается в контексте schema_context(tenant.schema_name),
|
||||
поэтому роли создаются в schema конкретного тенанта автоматически!
|
||||
"""
|
||||
default_roles = [
|
||||
{
|
||||
'code': Role.OWNER,
|
||||
'name': 'Владелец',
|
||||
'description': 'Полный доступ ко всем функциям системы. Управление пользователями и настройками.',
|
||||
'is_system': True,
|
||||
},
|
||||
{
|
||||
'code': Role.MANAGER,
|
||||
'name': 'Менеджер',
|
||||
'description': 'Управление заказами, клиентами, товарами и складом. Без доступа к настройкам.',
|
||||
'is_system': True,
|
||||
},
|
||||
{
|
||||
'code': Role.FLORIST,
|
||||
'name': 'Флорист',
|
||||
'description': 'Работа с заказами и складскими операциями для заказов.',
|
||||
'is_system': True,
|
||||
},
|
||||
{
|
||||
'code': Role.COURIER,
|
||||
'name': 'Курьер',
|
||||
'description': 'Доставка заказов (права будут определены позже).',
|
||||
'is_system': True,
|
||||
},
|
||||
]
|
||||
|
||||
for role_data in default_roles:
|
||||
Role.objects.get_or_create(
|
||||
code=role_data['code'],
|
||||
defaults=role_data
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_role_by_code(code):
|
||||
"""Получить роль по коду"""
|
||||
try:
|
||||
return Role.objects.get(code=code, is_system=True)
|
||||
except Role.DoesNotExist:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def assign_role_to_user(user, role_code, created_by=None):
|
||||
"""
|
||||
Назначить роль пользователю в текущем тенанте.
|
||||
Если у пользователя уже есть роль - обновляет её.
|
||||
|
||||
ВАЖНО: Вызывается в контексте текущего тенанта (через middleware),
|
||||
поэтому UserRole создается в schema текущего тенанта автоматически!
|
||||
"""
|
||||
role = RoleService.get_role_by_code(role_code)
|
||||
if not role:
|
||||
raise ValueError(f"Роль с кодом '{role_code}' не найдена")
|
||||
|
||||
user_role, created = UserRole.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
'role': role,
|
||||
'created_by': created_by,
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
return user_role
|
||||
|
||||
@staticmethod
|
||||
def get_user_role(user):
|
||||
"""Получить роль пользователя в текущем тенанте"""
|
||||
try:
|
||||
user_role = UserRole.objects.get(user=user, is_active=True)
|
||||
return user_role.role
|
||||
except UserRole.DoesNotExist:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def user_has_role(user, *role_codes):
|
||||
"""Проверить, имеет ли пользователь одну из указанных ролей"""
|
||||
user_role = RoleService.get_user_role(user)
|
||||
if not user_role:
|
||||
return False
|
||||
return user_role.code in role_codes
|
||||
3
myproject/user_roles/tests.py
Normal file
3
myproject/user_roles/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
myproject/user_roles/views.py
Normal file
3
myproject/user_roles/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user