diff --git a/django-stubs/conf/global_settings.pyi b/django-stubs/conf/global_settings.pyi index 40367682c..d46010a8b 100644 --- a/django-stubs/conf/global_settings.pyi +++ b/django-stubs/conf/global_settings.pyi @@ -1,4 +1,4 @@ -from collections.abc import Sequence +from collections.abc import Collection, Mapping, Sequence from re import Pattern # This is defined here as a do-nothing function because we can't import @@ -541,3 +541,9 @@ SECURE_REDIRECT_EXEMPT: list[str] SECURE_REFERRER_POLICY: str SECURE_SSL_HOST: str | None SECURE_SSL_REDIRECT: bool + +################## +# CSP MIDDLEWARE # +################## +SECURE_CSP: Mapping[str, Collection[str] | str] +SECURE_CSP_REPORT_ONLY: Mapping[str, Collection[str] | str] diff --git a/django-stubs/middleware/csp.pyi b/django-stubs/middleware/csp.pyi new file mode 100644 index 000000000..53efcc6e3 --- /dev/null +++ b/django-stubs/middleware/csp.pyi @@ -0,0 +1,10 @@ +from django.http import HttpRequest, HttpResponse +from django.utils.csp import CSP as CSP +from django.utils.csp import LazyNonce +from django.utils.deprecation import MiddlewareMixin + +def get_nonce(request: HttpRequest) -> LazyNonce | None: ... + +class ContentSecurityPolicyMiddleware(MiddlewareMixin): + def process_request(self, request: HttpRequest) -> None: ... + def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: ... diff --git a/django-stubs/template/context_processors.pyi b/django-stubs/template/context_processors.pyi index 15020b8c8..d7ec44979 100644 --- a/django-stubs/template/context_processors.pyi +++ b/django-stubs/template/context_processors.pyi @@ -6,6 +6,7 @@ from django.utils.functional import SimpleLazyObject _R = TypeVar("_R", bound=HttpRequest) +def csp(request: HttpRequest) -> dict[str, SimpleLazyObject | None]: ... def csrf(request: HttpRequest) -> dict[str, SimpleLazyObject]: ... def debug(request: HttpRequest) -> dict[str, Callable | bool]: ... def i18n(request: HttpRequest) -> dict[str, list[tuple[str, str]] | bool | str]: ... diff --git a/django-stubs/utils/csp.pyi b/django-stubs/utils/csp.pyi new file mode 100644 index 000000000..c07366a88 --- /dev/null +++ b/django-stubs/utils/csp.pyi @@ -0,0 +1,33 @@ +import sys +from collections.abc import Collection, Mapping + +from django.utils.functional import SimpleLazyObject + +if sys.version_info >= (3, 11): + from enum import StrEnum as _StrEnum +else: + from enum import Enum + + class _ReprEnum(Enum): ... # type: ignore[misc] + class _StrEnum(str, _ReprEnum): ... # type: ignore[misc] + +class CSP(_StrEnum): + HEADER_ENFORCE = "Content-Security-Policy" + HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only" + + NONE = "'none'" + REPORT_SAMPLE = "'report-sample'" + SELF = "'self'" + STRICT_DYNAMIC = "'strict-dynamic'" + UNSAFE_EVAL = "'unsafe-eval'" + UNSAFE_HASHES = "'unsafe-hashes'" + UNSAFE_INLINE = "'unsafe-inline'" + WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'" + + NONCE = "" + +class LazyNonce(SimpleLazyObject): + def __init__(self) -> None: ... + def __bool__(self) -> bool: ... + +def build_policy(config: Mapping[str, Collection[str] | str], nonce: SimpleLazyObject | str | None = None) -> str: ... diff --git a/django-stubs/views/decorators/csp.pyi b/django-stubs/views/decorators/csp.pyi new file mode 100644 index 000000000..e2ddff3aa --- /dev/null +++ b/django-stubs/views/decorators/csp.pyi @@ -0,0 +1,7 @@ +from collections.abc import Callable, Collection, Mapping +from typing import Any, TypeVar + +_F = TypeVar("_F", bound=Callable[..., Any]) + +def csp_override(config: Mapping[str, Collection[str] | str]) -> Callable[[_F], _F]: ... +def csp_report_only_override(config: Mapping[str, Collection[str] | str]) -> Callable[[_F], _F]: ... diff --git a/scripts/stubtest/allowlist_todo_django60.txt b/scripts/stubtest/allowlist_todo_django60.txt index bbd6cc4e1..456626afd 100644 --- a/scripts/stubtest/allowlist_todo_django60.txt +++ b/scripts/stubtest/allowlist_todo_django60.txt @@ -1,7 +1,5 @@ django.conf.FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG django.conf.global_settings.FORMS_URLFIELD_ASSUME_HTTPS -django.conf.global_settings.SECURE_CSP -django.conf.global_settings.SECURE_CSP_REPORT_ONLY django.conf.global_settings.TASKS django.conf.global_settings.URLIZE_ASSUME_HTTPS django.contrib.admin.AdminSite.password_change_form @@ -212,7 +210,6 @@ django.forms.ClearableFileInput.use_fieldset django.forms.models.BaseModelForm.validate_constraints django.forms.renderers.Jinja2DivFormRenderer django.forms.widgets.ClearableFileInput.use_fieldset -django.middleware.csp django.tasks django.tasks.backends django.tasks.backends.base @@ -223,7 +220,6 @@ django.tasks.checks django.tasks.exceptions django.tasks.signals django.template.base.PartialTemplate -django.template.context_processors.csp django.template.defaulttags.PartialDefNode django.template.defaulttags.PartialNode django.template.defaulttags.partial_func @@ -232,7 +228,6 @@ django.test.runner.QueryFormatter django.test.selenium.SeleniumTestCase.get_browser_logs django.test.testcases._AssertTemplateUsedContext.rendered_template_names django.utils.copy -django.utils.csp django.utils.datastructures.DeferredSubDict django.utils.deprecation.RemovedInDjango60Warning django.utils.deprecation.RemovedInDjango70Warning @@ -245,4 +240,3 @@ django.utils.itercompat django.utils.json django.utils.log.log_message django.utils.text.acompress_sequence -django.views.decorators.csp diff --git a/tests/assert_type/views/test_decorators.py b/tests/assert_type/views/test_decorators.py new file mode 100644 index 000000000..2e4ff32a7 --- /dev/null +++ b/tests/assert_type/views/test_decorators.py @@ -0,0 +1,27 @@ +from django.http import HttpRequest, HttpResponse +from django.views.decorators.csp import csp_override, csp_report_only_override +from typing_extensions import assert_type + + +@csp_override( + { + "default-src": ["'self'"], + "script-src": ["'self'", "'unsafe-inline'"], + "report-uri": "/path/to/reports-endpoint/", + } +) +def my_view(request: HttpRequest) -> HttpResponse: ... + + +@csp_report_only_override( + { + "default-src": ["'self'"], + "script-src": ["'self'", "'unsafe-inline'"], + "report-uri": "/path/to/reports-endpoint/", + } +) +def my_view2(request: HttpRequest) -> HttpResponse: ... + + +assert_type(my_view(HttpRequest()), HttpResponse) +assert_type(my_view2(HttpRequest()), HttpResponse)