""" Система гранулярных прав доступа. Определяет все доступные разрешения и функции для их проверки. """ from functools import wraps from django.http import JsonResponse, HttpResponseForbidden from django.shortcuts import redirect from django.contrib import messages # Определение всех разрешений в системе # Формат: (код, название, описание) PERMISSIONS = [ # Source List Page - Toolbar buttons ('source_create', 'Создание источника', 'Кнопка "Создать" на странице списка источников'), ('source_edit', 'Редактирование источника', 'Кнопка редактирования источника'), ('source_delete', 'Удаление источников', 'Кнопка "Удалить" на странице списка источников'), ('source_import_excel', 'Импорт из Excel', 'Кнопка "Excel" для загрузки данных'), ('source_import_csv', 'Импорт из CSV', 'Кнопка "CSV" для загрузки данных'), ('source_averaging', 'Усреднение точек', 'Кнопка "Усреднение" на странице списка источников'), ('source_tech_analyze', 'Технический анализ', 'Кнопка "Тех. анализ" на странице списка источников'), ('source_merge', 'Объединение источников', 'Кнопка "Объединить" в offcanvas списка'), # Source Requests ('request_create', 'Создание заявки', 'Создание новой заявки на источник'), ('request_edit', 'Редактирование заявки', 'Редактирование существующей заявки'), ('request_delete', 'Удаление заявки', 'Удаление заявки на источник'), ('request_import', 'Импорт заявок', 'Импорт заявок из файла'), # ObjItem (Points) ('objitem_create', 'Создание точки', 'Создание новой точки ГЛ'), ('objitem_edit', 'Редактирование точки', 'Редактирование точки ГЛ'), ('objitem_delete', 'Удаление точки', 'Удаление точки ГЛ'), # Satellites ('satellite_create', 'Создание спутника', 'Создание нового спутника'), ('satellite_edit', 'Редактирование спутника', 'Редактирование спутника'), ('satellite_delete', 'Удаление спутника', 'Удаление спутника'), # Tech Analyze ('tech_analyze_create', 'Создание тех. анализа', 'Создание записи технического анализа'), ('tech_analyze_edit', 'Редактирование тех. анализа', 'Редактирование записи технического анализа'), ('tech_analyze_delete', 'Удаление тех. анализа', 'Удаление записи технического анализа'), # Signal Marks ('mark_create', 'Создание отметки', 'Создание отметки о сигнале'), ('mark_edit', 'Редактирование отметки', 'Редактирование отметки о сигнале'), # Statistics ('statistics_view', 'Просмотр статистики', 'Доступ к странице статистики'), # Kubsat ('kubsat_view', 'Просмотр Кубсат', 'Доступ к странице Кубсат'), ('kubsat_edit', 'Редактирование Кубсат', 'Редактирование данных Кубсат'), # LyngSat ('lyngsat_parse', 'Парсинг LyngSat', 'Запуск парсинга LyngSat'), # Transponders ('transponder_create', 'Создание транспондера', 'Создание нового транспондера'), ('transponder_edit', 'Редактирование транспондера', 'Редактирование транспондера'), ('transponder_delete', 'Удаление транспондера', 'Удаление транспондера'), ('transponder_import_xml', 'Импорт транспондеров из XML', 'Загрузка транспондеров из XML файла'), # Errors Report (Журнал ошибок) ('errors_report_create', 'Создание записи журнала ошибок', 'Создание новой записи в журнале ошибок'), ('errors_report_edit', 'Редактирование записи журнала ошибок', 'Редактирование записи в журнале ошибок'), ('errors_report_delete', 'Удаление записи журнала ошибок', 'Удаление записи из журнала ошибок'), # Admin access # ('admin_access', 'Доступ к админ-панели', 'Доступ к административной панели Django'), ] # Словарь для быстрого доступа к разрешениям PERMISSION_CHOICES = [(code, name) for code, name, _ in PERMISSIONS] PERMISSION_DESCRIPTIONS = {code: desc for code, _, desc in PERMISSIONS} # Права по умолчанию для ролей DEFAULT_ROLE_PERMISSIONS = { 'admin': [code for code, _, _ in PERMISSIONS], # Все права 'moderator': [ 'source_create', 'source_edit', 'source_import_excel', 'source_import_csv', 'source_averaging', 'source_tech_analyze', 'source_merge', 'request_create', 'request_edit', 'request_import', 'objitem_create', 'objitem_edit', 'satellite_create', 'satellite_edit', 'transponder_create', 'transponder_edit', 'transponder_import_xml', 'tech_analyze_create', 'tech_analyze_edit', 'mark_create', 'mark_edit', 'statistics_view', 'kubsat_view', 'kubsat_edit', 'errors_report_create', 'errors_report_edit', ], 'user': [ 'statistics_view', 'kubsat_view', ], } def has_permission(user, permission_code): """ Проверяет, имеет ли пользователь указанное разрешение. Args: user: Объект User Django permission_code: Код разрешения (строка) Returns: bool: True если пользователь имеет разрешение """ if not user or not user.is_authenticated: return False # Суперпользователь имеет все права if user.is_superuser: return True # Получаем CustomUser custom_user = getattr(user, 'customuser', None) if not custom_user: return False # Проверяем, используются ли индивидуальные разрешения if custom_user.use_custom_permissions: # Используем индивидуальные разрешения return custom_user.user_permissions.filter(code=permission_code).exists() # Иначе используем права по умолчанию для роли role = custom_user.role default_perms = DEFAULT_ROLE_PERMISSIONS.get(role, []) return permission_code in default_perms def get_user_permissions(user): """ Возвращает список кодов разрешений пользователя. Args: user: Объект User Django Returns: list: Список кодов разрешений """ if not user or not user.is_authenticated: return [] if user.is_superuser: return [code for code, _, _ in PERMISSIONS] custom_user = getattr(user, 'customuser', None) if not custom_user: return [] # Проверяем, используются ли индивидуальные разрешения if custom_user.use_custom_permissions: return list(custom_user.user_permissions.values_list('code', flat=True)) # Права по умолчанию для роли return DEFAULT_ROLE_PERMISSIONS.get(custom_user.role, []) def permission_required(permission_code, redirect_url=None, raise_exception=False): """ Декоратор для проверки разрешения на уровне view. Args: permission_code: Код разрешения redirect_url: URL для редиректа при отсутствии прав (по умолчанию на предыдущую страницу) raise_exception: Если True, возвращает 403 вместо редиректа Usage: @permission_required('source_create') def my_view(request): ... """ def decorator(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): if has_permission(request.user, permission_code): return view_func(request, *args, **kwargs) # Для AJAX запросов возвращаем JSON if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return JsonResponse({ 'success': False, 'error': 'У вас нет прав для выполнения этого действия' }, status=403) if raise_exception: return HttpResponseForbidden('У вас нет прав для выполнения этого действия') messages.error(request, 'У вас нет прав для выполнения этого действия') if redirect_url: return redirect(redirect_url) # Редирект на предыдущую страницу или на главную referer = request.META.get('HTTP_REFERER') if referer: return redirect(referer) return redirect('mainapp:source_list') return wrapper return decorator class PermissionRequiredMixin: """ Миксин для class-based views для проверки разрешений. Usage: class MyView(PermissionRequiredMixin, View): permission_required = 'source_create' # или для нескольких разрешений (любое из них): permission_required = ['source_create', 'source_edit'] """ permission_required = None permission_denied_message = 'У вас нет прав для выполнения этого действия' def has_permission(self): """Проверяет наличие разрешения.""" perms = self.get_permission_required() if isinstance(perms, str): return has_permission(self.request.user, perms) # Для списка - проверяем наличие хотя бы одного разрешения return any(has_permission(self.request.user, perm) for perm in perms) def get_permission_required(self): """Возвращает требуемое разрешение.""" if self.permission_required is None: raise ValueError( f'{self.__class__.__name__} is missing the permission_required attribute.' ) return self.permission_required def dispatch(self, request, *args, **kwargs): if not self.has_permission(): return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) def handle_no_permission(self): """Обработка отсутствия разрешения.""" # Для AJAX запросов if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest': return JsonResponse({ 'success': False, 'error': self.permission_denied_message }, status=403) messages.error(self.request, self.permission_denied_message) referer = self.request.META.get('HTTP_REFERER') if referer: return redirect(referer) return redirect('mainapp:source_list')