""" Кастомный backend аутентификации для связывания ролей с Django permissions API. ВАЖНО: Этот backend НЕ использует таблицы Django permissions из public schema! Он только эмулирует API has_perm(), читая роли из текущей tenant schema. Это безопасно для мультитенантной архитектуры. ВАЖНО: Backend проверяет текущую схему перед обращением к tenant-only таблицам. В public схеме ролевые проверки пропускаются (fallback на стандартные Django permissions). ВАЖНО: Этот backend НЕ наследуется от ModelBackend! Он реализует только has_perm/has_module_perms для проверки прав на основе ролей. Аутентификацию (authenticate/get_user) выполняют другие backends. """ 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() def _is_tenant_user(user_obj): """ Проверяет, является ли пользователь CustomUser (пользователь тенанта). PlatformAdmin не имеет ролей в тенантах. """ from accounts.models import CustomUser return isinstance(user_obj, CustomUser) class RoleBasedPermissionBackend: """ Backend, который предоставляет права на основе роли пользователя в текущем тенанте. НЕ наследуется от ModelBackend! Реализует только проверку прав (has_perm/has_module_perms). Аутентификацию (authenticate/get_user) выполняют TenantUserBackend и PlatformAdminBackend. Как это работает: 1. Django вызывает user.has_perm('products.add_product') 2. Backend проверяет роль пользователя в ТЕКУЩЕЙ tenant schema (через RoleService) 3. На основе роли возвращает True/False 4. Никакие данные из public schema не используются! """ def authenticate(self, request, username=None, password=None, **kwargs): """ Этот backend НЕ выполняет аутентификацию. Возвращает None, чтобы Django перешёл к следующему backend. """ return None # Маппинг ролей на наборы разрешений # Формат: 'app_label': ['action1', 'action2', ...] # где action - это префикс permission: add_product -> add, change_order -> change ROLE_PERMISSIONS = { Role.OWNER: { # Владелец: полный доступ ко всем модулям 'products': ['add', 'change', 'delete', 'view'], 'inventory': ['add', 'change', 'delete', 'view'], 'orders': ['add', 'change', 'delete', 'view'], 'clients': ['add', 'change', 'delete', 'view'], 'suppliers': ['add', 'change', 'delete', 'view'], 'user_roles': ['add', 'change', 'delete', 'view'], 'payments': ['add', 'change', 'delete', 'view'], }, Role.MANAGER: { # Менеджер: доступ к основным операционным модулям (без настроек) 'products': ['add', 'change', 'delete', 'view'], 'inventory': ['add', 'change', 'delete', 'view'], 'orders': ['add', 'change', 'delete', 'view'], 'clients': ['add', 'change', 'delete', 'view'], 'suppliers': ['add', 'change', 'view'], # только просмотр и изменение, без удаления 'payments': ['view'], }, Role.FLORIST: { # Флорист: работа с заказами и просмотр товаров 'products': ['view'], 'inventory': ['view', 'change'], # может обновлять остатки при сборке заказов 'orders': ['change', 'view'], }, Role.COURIER: { # Курьер: только просмотр и обновление статуса заказов 'orders': ['change', 'view'], }, } def has_perm(self, user_obj, perm, obj=None): """ Проверяет, имеет ли пользователь указанное разрешение. Формат разрешения: 'app_label.action_modelname' Например: 'products.add_product', 'orders.change_order' Args: user_obj: Пользователь perm: Строка разрешения в формате 'app.action_model' obj: Опциональный объект для object-level permissions Returns: bool: True если пользователь имеет разрешение """ # Если пользователь не аутентифицирован, нет доступа if not user_obj.is_authenticated: return False # Определяем тип пользователя is_tenant = _is_tenant_user(user_obj) # Для CustomUser (пользователи тенантов) НЕ вызываем super().has_perm() # т.к. AUTH_USER_MODEL = PlatformAdmin, и ModelBackend будет пытаться # искать permissions в Django Permission таблице для PlatformAdmin if is_tenant: # Суперпользователь имеет все права if user_obj.is_superuser: return True # ВАЖНО: В public схеме нет таблиц UserRole/Role! if _is_public_schema(): return False # Для CustomUser получаем роль пользователя в текущем тенанте user_role = RoleService.get_user_role(user_obj) if not user_role: return False # Парсим разрешение: 'app_label.action_modelname' try: app_label, codename = perm.split('.') action = codename.split('_')[0] except (ValueError, IndexError): return False # Проверяем, есть ли у роли это разрешение role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {}) app_perms = role_perms.get(app_label, []) return action in app_perms else: # Для PlatformAdmin - этот backend НЕ обрабатывает PlatformAdmin # Возвращаем None, чтобы Django перешёл к другому backend # (PlatformAdminBackend должен обработать это) return None def has_module_perms(self, user_obj, app_label): """ Проверяет, имеет ли пользователь какие-либо разрешения для приложения. Используется в Django admin для определения, показывать ли модуль в навигации. Args: user_obj: Пользователь app_label: Название приложения (например, 'products', 'orders') Returns: bool: True если пользователь имеет хотя бы одно разрешение для приложения """ # Если пользователь не аутентифицирован, нет доступа if not user_obj.is_authenticated: return False # Определяем тип пользователя is_tenant = _is_tenant_user(user_obj) # Для CustomUser (пользователи тенантов) НЕ вызываем super().has_module_perms() if is_tenant: # Суперпользователь имеет все права if user_obj.is_superuser: return True # ВАЖНО: В public схеме нет таблиц UserRole/Role! if _is_public_schema(): return False # Для CustomUser получаем роль пользователя в текущем тенанте user_role = RoleService.get_user_role(user_obj) if not user_role: return False # Проверяем, есть ли у роли какие-либо разрешения для этого приложения role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {}) return app_label in role_perms and len(role_perms[app_label]) > 0 else: # Для PlatformAdmin - этот backend НЕ обрабатывает PlatformAdmin # Возвращаем None, чтобы Django перешёл к другому backend return None