from http import HTTPStatus
from typing import TYPE_CHECKING, final
from django.conf import settings
from django.http import HttpRequest
from django.http.request import HttpHeaders
from django.middleware.csrf import CsrfViewMiddleware
if TYPE_CHECKING:
from dmr.controller import Controller
from dmr.serializer import BaseSerializer
[docs]
def ensure_csrf(controller: 'Controller[BaseSerializer]') -> None:
"""
Raise ``APIError`` (403) if the CSRF check fails.
Does not perform the second CSRF check if it was processed before.
Since it uses the standard Django tooling, the default middleware
checks ``csrf_processing_done`` which is set on each successful check.
"""
from dmr.response import APIError # noqa: PLC0415
reason = _check_csrf_failure(controller.request)
if reason is not None:
raise APIError(
controller.format_error(reason),
status_code=HTTPStatus.FORBIDDEN,
)
def csrf_header_name() -> str:
"""
Convert ``CSRF_HEADER_NAME`` from the ``META`` form to the HTTP form.
Django stores this setting as a ``request.META`` key,
like ``HTTP_X_CSRFTOKEN``, while OpenAPI needs the header name
that a client sends, like ``X-Csrftoken``. HTTP header names
are case-insensitive, so the exact casing does not matter.
"""
header_name = HttpHeaders.parse_header_name(settings.CSRF_HEADER_NAME)
return settings.CSRF_HEADER_NAME if header_name is None else header_name
@final
class _EnsureCsrfToken(CsrfViewMiddleware):
"""
CSRF check middleware that returns the rejection reason.
Used for checking CSRF tokens manually.
"""
def _reject(self, request: HttpRequest, reason: str) -> str:
from dmr.security.csrf import csrf_message # noqa: PLC0415
# Return the failure reason instead of an ``HttpResponse``.
# Expose detailed csrf failure reason on DEBUG mode.
# Otherwise, provide default placeholder reason.
return csrf_message(reason)
def _check_csrf_failure(request: HttpRequest) -> str | None:
"""Perform CSRF validation using ``_EnsureCsrfToken``."""
check = _EnsureCsrfToken(lambda _: None) # type: ignore[arg-type]
check.process_request(request)
return check.process_view(request, None, (), {}) # type: ignore[arg-type, return-value]