Рефакторинг - перенос пакета utils.drf в middleware
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет классов расширения Django REST Framework
|
||||
"""
|
||||
|
||||
from .custom_exception_handler import custom_exception_handler
|
||||
from .custom_logging_middleware import CustomLoggingMiddleware
|
||||
from .expiring_token_authentication import ExpiringTokenAuthentication
|
||||
@@ -1,150 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание функции custom_exception_handler
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from net.xeaf.rack.core import CoreException
|
||||
from net.xeaf.rack.models.responses import ErrorResponse
|
||||
from net.xeaf.rack.utils.exceptions import ForbiddenException
|
||||
from net.xeaf.rack.utils.exceptions import NotFoundException
|
||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||
from wsgiapp import settings
|
||||
|
||||
|
||||
def custom_exception_handler(exc: CoreException | Exception, context: dict) -> Response:
|
||||
"""
|
||||
Универсальный обработчик исключений
|
||||
|
||||
:param exc: Исключение
|
||||
:param context: Контекст
|
||||
|
||||
:return: Ответ
|
||||
"""
|
||||
|
||||
# Получаем информацию о запросе
|
||||
request = context.get("request")
|
||||
assert isinstance(request, Request)
|
||||
|
||||
# Это наше исключение
|
||||
if isinstance(exc, CoreException):
|
||||
return _make_error_response(
|
||||
status_code=exc.status_code,
|
||||
detail=exc.detail,
|
||||
meta=exc.meta,
|
||||
pure_status=exc.is_pure_status(),
|
||||
exc_info=exc)
|
||||
|
||||
# Проверка на стандартное исключение валидации
|
||||
if isinstance(exc, ValidationError):
|
||||
return _process_validation_exception(exc)
|
||||
|
||||
# Проверка на стандартное исключение авторизации
|
||||
if isinstance(exc, NotAuthenticated):
|
||||
return _make_error_response(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=UnauthorizedException.Msg.DEFAULT_MESSAGE,
|
||||
meta=None,
|
||||
pure_status=False,
|
||||
exc_info=exc)
|
||||
|
||||
# Проверка на стандартное исключение прав доступа
|
||||
if isinstance(exc, PermissionDenied):
|
||||
return _make_error_response(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ForbiddenException.Msg.DEFAULT_MESSAGE,
|
||||
meta=None,
|
||||
pure_status=False,
|
||||
exc_info=exc)
|
||||
|
||||
# Проверка на стандартное исключение отсутствия реализации
|
||||
if isinstance(exc, MethodNotAllowed):
|
||||
return _make_error_response(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=NotFoundException.Msg.DEFAULT_MESSAGE,
|
||||
meta=None,
|
||||
pure_status=True,
|
||||
exc_info=exc)
|
||||
|
||||
# Если ничего другое не прошло
|
||||
return _make_error_response(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(exc),
|
||||
meta=None,
|
||||
pure_status=True,
|
||||
exc_info=exc)
|
||||
|
||||
|
||||
def _process_validation_exception(exc: ValidationError) -> ErrorResponse:
|
||||
"""
|
||||
Обрабатывает частный случай стандартного исключения валидации
|
||||
|
||||
:param exc: Исключение
|
||||
|
||||
:return: Ответ
|
||||
"""
|
||||
|
||||
# Обработка списка
|
||||
if isinstance(exc.detail, list):
|
||||
if len(exc.detail) == 1:
|
||||
detail = str(exc.detail[0])
|
||||
else:
|
||||
detail = exc.detail
|
||||
|
||||
# Обработка словаря
|
||||
elif isinstance(exc.detail, dict):
|
||||
detail = {}
|
||||
for field, error_info in exc.detail.items():
|
||||
if isinstance(error_info, list):
|
||||
error = error_info[0]
|
||||
detail[field] = str(error)
|
||||
|
||||
# Обработка остальных типов
|
||||
else:
|
||||
detail = str(exc.detail)
|
||||
|
||||
return _make_error_response(status_code=status.HTTP_400_BAD_REQUEST, detail=detail, meta=None, pure_status=False, exc_info=exc)
|
||||
|
||||
|
||||
def _make_error_response(status_code: int, detail: list | dict | str | bool | None, meta: dict | None, pure_status: bool,
|
||||
exc_info: Exception) -> ErrorResponse:
|
||||
"""
|
||||
Создает ответ с ошибкой
|
||||
|
||||
:param status_code: Код статуса
|
||||
:param detail: Детали ошибки
|
||||
:param meta: Дополнительная информация
|
||||
:param pure_status: Признак отправки чистого кода состояния
|
||||
:param exc_info: Исключение
|
||||
|
||||
:return: Ответ
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(settings.LOGGER_NAME)
|
||||
|
||||
if status_code == status.HTTP_400_BAD_REQUEST:
|
||||
logger.debug(str(exc_info), exc_info=exc_info)
|
||||
elif status_code == status.HTTP_401_UNAUTHORIZED:
|
||||
logger.warning(str(exc_info), exc_info=exc_info)
|
||||
elif status_code == status.HTTP_403_FORBIDDEN:
|
||||
logger.warning(str(exc_info), exc_info=exc_info)
|
||||
elif status_code == status.HTTP_404_NOT_FOUND:
|
||||
logger.error(str(exc_info), exc_info=exc_info)
|
||||
else:
|
||||
logger.critical(str(exc_info), exc_info=exc_info)
|
||||
|
||||
return ErrorResponse(status_code=status_code, detail=detail, meta=meta, pure_status=pure_status)
|
||||
@@ -1,207 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CustomLoggingMiddleware
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class CustomLoggingMiddleware:
|
||||
"""
|
||||
Класс для логирования запросов и ответов DRF
|
||||
"""
|
||||
|
||||
DEFAULT_SENSITIVE_KEYS = {'password', 'old_password', 'new_password', 'token', 'access_token'}
|
||||
""" Словарь ключей, которые нужно замаскировать в логах """
|
||||
|
||||
MAX_LOG_LENGTH = 2048
|
||||
""" Максимальная длина объекта данных лога """
|
||||
|
||||
sensitive_keys: set = {}
|
||||
""" Актуальный словарь ключей, которые нужно замаскировать в логах """
|
||||
|
||||
logger: Logger
|
||||
""" Объект доступа к логгеру """
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""
|
||||
Инициализация
|
||||
"""
|
||||
|
||||
self.get_response = get_response
|
||||
self.logger = logging.getLogger(settings.LOGGER_NAME)
|
||||
|
||||
self.sensitive_keys = set(self.DEFAULT_SENSITIVE_KEYS)
|
||||
if settings.LOGGER_SENSITIVE_KEYS:
|
||||
self.sensitive_keys.update(settings.LOGGER_SENSITIVE_KEYS)
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
"""
|
||||
Собираем данные запроса и отправляем в лог
|
||||
"""
|
||||
|
||||
# Обрабатываем данные запроса
|
||||
path = request.path
|
||||
method = request.method
|
||||
query_params = self._process_query_params(request)
|
||||
|
||||
# Передаем запрос дальше по конвейеру Django / DRF
|
||||
response = self.get_response(request)
|
||||
|
||||
# Обрабатываем данные запроса, ответа и перехваченного исключения
|
||||
body = self._process_body(request)
|
||||
exc = getattr(request, "_raised_exception", None)
|
||||
status_code = response.status_code
|
||||
request_id = request.META.get('HTTP_X_REQUEST_ID', '')
|
||||
response_content = self._process_response(response)
|
||||
|
||||
# Обрабатываем данные пользователя
|
||||
user_id = ""
|
||||
if hasattr(request, 'user'):
|
||||
if request.user.is_authenticated:
|
||||
user_id = str(request.user.id)
|
||||
|
||||
# Формируем единое сообщение для лога
|
||||
log_parts = [
|
||||
f"{status_code} [{method}] {path}"
|
||||
]
|
||||
|
||||
if user_id:
|
||||
log_parts.append(f"User ID: {user_id}")
|
||||
if request_id:
|
||||
log_parts.append(f"Request ID: {request_id}")
|
||||
if query_params:
|
||||
log_parts.append(f"Query Params: {query_params}")
|
||||
if body:
|
||||
log_parts.append(f"Request Body: {body}")
|
||||
if response_content:
|
||||
log_parts.append(f"Response Content: {response_content}")
|
||||
|
||||
log_message = "\n".join(log_parts) + "\n"
|
||||
|
||||
# Создаем и записываем лог
|
||||
self._write_log(log_message, status_code, exc)
|
||||
|
||||
return response
|
||||
|
||||
def _process_query_params(self, request: HttpRequest) -> dict[str, Any]:
|
||||
"""
|
||||
Обрабатывает параметры запроса и маскирует конфиденциальные данные
|
||||
|
||||
:param request: Объект запроса
|
||||
|
||||
:return: Обработанные параметры запроса
|
||||
"""
|
||||
|
||||
raw_query_params = dict(request.GET.lists())
|
||||
return self._mask_sensitive_data(raw_query_params)
|
||||
|
||||
def _process_body(self, request: HttpRequest) -> str:
|
||||
"""
|
||||
Обрабатывает тело запроса и маскирует конфиденциальные данные
|
||||
|
||||
:param request: Объект запроса
|
||||
|
||||
:return: Обработанное тело запроса
|
||||
"""
|
||||
|
||||
if request.body:
|
||||
return self._process_data(request.body)
|
||||
|
||||
return ""
|
||||
|
||||
def _process_response(self, response: HttpResponse) -> str:
|
||||
"""
|
||||
Обрабатываем данные ответа
|
||||
|
||||
:param response: HttpResponse
|
||||
|
||||
:return: Строковое представление данных ответа
|
||||
"""
|
||||
|
||||
if hasattr(response, 'content'):
|
||||
return self._process_data(response.content)
|
||||
|
||||
return ""
|
||||
|
||||
def _process_data(self, data: Any) -> str:
|
||||
"""
|
||||
Обрабатывает данные запроса или ответа и возвращает их в виде строки
|
||||
|
||||
:param data: Данные запроса или ответа
|
||||
|
||||
:return: Обработанные данные в виде строки
|
||||
"""
|
||||
|
||||
data_len = len(data)
|
||||
if data_len > self.MAX_LOG_LENGTH:
|
||||
return f"<Data too large to log (len={data_len})>"
|
||||
|
||||
try:
|
||||
raw_response = data.decode('utf-8')
|
||||
try:
|
||||
data_json = json.loads(raw_response)
|
||||
masked_response = self._mask_sensitive_data(data_json)
|
||||
result = json.dumps(masked_response, ensure_ascii=False, indent=2)
|
||||
except json.JSONDecodeError:
|
||||
result = raw_response
|
||||
except UnicodeDecodeError:
|
||||
result = f"<Binary/Streaming Data (len={data_len})>"
|
||||
|
||||
return result
|
||||
|
||||
def _write_log(self, log_message: str, status_code: int, exc: Exception | None):
|
||||
"""
|
||||
Записывает лог с учетом исключения
|
||||
|
||||
:param log_message: Сообщение лога
|
||||
:param status_code: Код статуса ответа
|
||||
:param exc: Исключение
|
||||
"""
|
||||
|
||||
if exc:
|
||||
|
||||
log_message += f"\nException: {str(exc)}"
|
||||
|
||||
if status_code == status.HTTP_400_BAD_REQUEST:
|
||||
self.logger.debug(log_message, exc_info=exc)
|
||||
elif status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]: # UNAUTHORIZED, FORBIDDEN
|
||||
self.logger.warning(log_message, exc_info=exc)
|
||||
elif status_code == status.HTTP_404_NOT_FOUND:
|
||||
self.logger.error(log_message, exc_info=exc)
|
||||
else:
|
||||
self.logger.critical(log_message, exc_info=exc)
|
||||
else:
|
||||
self.logger.debug(log_message)
|
||||
|
||||
def _mask_sensitive_data(self, data: Any) -> Any:
|
||||
"""
|
||||
Рекурсивно маскирует конфиденциальные данные в словарях и списках
|
||||
|
||||
:param data: Данные для маскировки
|
||||
|
||||
:return: Маскированные данные
|
||||
"""
|
||||
|
||||
if isinstance(data, dict):
|
||||
return {
|
||||
key: '***' if key.lower() in self.sensitive_keys else self._mask_sensitive_data(value)
|
||||
for key, value in data.items()
|
||||
}
|
||||
elif isinstance(data, list):
|
||||
return [self._mask_sensitive_data(item) for item in data]
|
||||
|
||||
return data
|
||||
@@ -1,75 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExpiringTokenAuthentication
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.request import Request
|
||||
|
||||
from net.xeaf.rack.managers import AccountManager
|
||||
from net.xeaf.rack.managers import LocaleManager
|
||||
from net.xeaf.rack.managers import SessionManager
|
||||
from net.xeaf.rack.models import AccountModel
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
|
||||
|
||||
class ExpiringTokenAuthentication(TokenAuthentication):
|
||||
"""
|
||||
Реализует механизм авторизации для Токенов со сроком действия
|
||||
"""
|
||||
|
||||
AUTH_TOKEN_PARAM = "x-auth-token"
|
||||
""" Идентификатор параметра токена """
|
||||
|
||||
def authenticate(self, request: Request) -> tuple[AccountModel | None, SessionModel | None] | tuple[Any, Any] | None:
|
||||
"""
|
||||
Метод авторизации
|
||||
|
||||
:param request: Информация о запросе
|
||||
"""
|
||||
|
||||
LocaleManager.adjust_request_language(request)
|
||||
|
||||
key = request.query_params.get(self.AUTH_TOKEN_PARAM, None)
|
||||
if key is not None:
|
||||
return self.authenticate_credentials(key)
|
||||
|
||||
return super().authenticate(request)
|
||||
|
||||
def authenticate_credentials(self, key: str) -> tuple[AccountModel | None, SessionModel | None]:
|
||||
"""
|
||||
Проверка Токена авторизации
|
||||
|
||||
:param key: Токен авторизации
|
||||
"""
|
||||
|
||||
session = SessionManager[SessionModel].find_by_token(token=key)
|
||||
if not session or session.is_expired():
|
||||
return None, None
|
||||
|
||||
account = session.account
|
||||
if not self._check_account(account):
|
||||
return None, None
|
||||
|
||||
SessionManager.renew_session(session)
|
||||
return session.account, session
|
||||
|
||||
@classmethod
|
||||
def _check_account(cls, account: AccountModel) -> bool:
|
||||
"""
|
||||
Проверка учетной записи
|
||||
|
||||
:param account: Учетная запись
|
||||
"""
|
||||
|
||||
# Можно прошедшим проверку и не удаленным
|
||||
reject = AccountManager.check_account_auth(account)
|
||||
result = reject is None and not account.is_deleted()
|
||||
return result
|
||||
@@ -37,10 +37,10 @@ REST_FRAMEWORK = {
|
||||
'djangorestframework_camel_case.parser.CamelCaseJSONParser',
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'net.xeaf.rack.utils.drf.ExpiringTokenAuthentication',
|
||||
'net.xeaf.rack.middleware.ExpiringTokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 10,
|
||||
'EXCEPTION_HANDLER': 'net.xeaf.rack.utils.drf.custom_exception_handler'
|
||||
'EXCEPTION_HANDLER': 'net.xeaf.rack.middleware.custom_exception_handler'
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"""
|
||||
|
||||
MIDDLEWARE = [
|
||||
'net.xeaf.rack.utils.drf.CustomLoggingMiddleware',
|
||||
'net.xeaf.rack.middleware.CustomLoggingMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
|
||||
Reference in New Issue
Block a user