Skip to content

Commit 51beeee

Browse files
authored
Django 6.0: Add stubs for built-in CSP support (#2931)
* Add type stubs for built-in CSP support in Django 6.0
1 parent e613759 commit 51beeee

File tree

7 files changed

+85
-7
lines changed

7 files changed

+85
-7
lines changed

django-stubs/conf/global_settings.pyi

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Sequence
1+
from collections.abc import Collection, Mapping, Sequence
22
from re import Pattern
33

44
# This is defined here as a do-nothing function because we can't import
@@ -541,3 +541,9 @@ SECURE_REDIRECT_EXEMPT: list[str]
541541
SECURE_REFERRER_POLICY: str
542542
SECURE_SSL_HOST: str | None
543543
SECURE_SSL_REDIRECT: bool
544+
545+
##################
546+
# CSP MIDDLEWARE #
547+
##################
548+
SECURE_CSP: Mapping[str, Collection[str] | str]
549+
SECURE_CSP_REPORT_ONLY: Mapping[str, Collection[str] | str]

django-stubs/middleware/csp.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.http import HttpRequest, HttpResponse
2+
from django.utils.csp import CSP as CSP
3+
from django.utils.csp import LazyNonce
4+
from django.utils.deprecation import MiddlewareMixin
5+
6+
def get_nonce(request: HttpRequest) -> LazyNonce | None: ...
7+
8+
class ContentSecurityPolicyMiddleware(MiddlewareMixin):
9+
def process_request(self, request: HttpRequest) -> None: ...
10+
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: ...

django-stubs/template/context_processors.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from django.utils.functional import SimpleLazyObject
66

77
_R = TypeVar("_R", bound=HttpRequest)
88

9+
def csp(request: HttpRequest) -> dict[str, SimpleLazyObject | None]: ...
910
def csrf(request: HttpRequest) -> dict[str, SimpleLazyObject]: ...
1011
def debug(request: HttpRequest) -> dict[str, Callable | bool]: ...
1112
def i18n(request: HttpRequest) -> dict[str, list[tuple[str, str]] | bool | str]: ...

django-stubs/utils/csp.pyi

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import sys
2+
from collections.abc import Collection, Mapping
3+
4+
from django.utils.functional import SimpleLazyObject
5+
6+
if sys.version_info >= (3, 11):
7+
from enum import StrEnum as _StrEnum
8+
else:
9+
from enum import Enum
10+
11+
class _ReprEnum(Enum): ... # type: ignore[misc]
12+
class _StrEnum(str, _ReprEnum): ... # type: ignore[misc]
13+
14+
class CSP(_StrEnum):
15+
HEADER_ENFORCE = "Content-Security-Policy"
16+
HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"
17+
18+
NONE = "'none'"
19+
REPORT_SAMPLE = "'report-sample'"
20+
SELF = "'self'"
21+
STRICT_DYNAMIC = "'strict-dynamic'"
22+
UNSAFE_EVAL = "'unsafe-eval'"
23+
UNSAFE_HASHES = "'unsafe-hashes'"
24+
UNSAFE_INLINE = "'unsafe-inline'"
25+
WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'"
26+
27+
NONCE = "<CSP_NONCE_SENTINEL>"
28+
29+
class LazyNonce(SimpleLazyObject):
30+
def __init__(self) -> None: ...
31+
def __bool__(self) -> bool: ...
32+
33+
def build_policy(config: Mapping[str, Collection[str] | str], nonce: SimpleLazyObject | str | None = None) -> str: ...
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from collections.abc import Callable, Collection, Mapping
2+
from typing import Any, TypeVar
3+
4+
_F = TypeVar("_F", bound=Callable[..., Any])
5+
6+
def csp_override(config: Mapping[str, Collection[str] | str]) -> Callable[[_F], _F]: ...
7+
def csp_report_only_override(config: Mapping[str, Collection[str] | str]) -> Callable[[_F], _F]: ...

scripts/stubtest/allowlist_todo_django60.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
django.conf.FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG
22
django.conf.global_settings.FORMS_URLFIELD_ASSUME_HTTPS
3-
django.conf.global_settings.SECURE_CSP
4-
django.conf.global_settings.SECURE_CSP_REPORT_ONLY
53
django.conf.global_settings.TASKS
64
django.conf.global_settings.URLIZE_ASSUME_HTTPS
75
django.contrib.admin.AdminSite.password_change_form
@@ -212,7 +210,6 @@ django.forms.ClearableFileInput.use_fieldset
212210
django.forms.models.BaseModelForm.validate_constraints
213211
django.forms.renderers.Jinja2DivFormRenderer
214212
django.forms.widgets.ClearableFileInput.use_fieldset
215-
django.middleware.csp
216213
django.tasks
217214
django.tasks.backends
218215
django.tasks.backends.base
@@ -223,7 +220,6 @@ django.tasks.checks
223220
django.tasks.exceptions
224221
django.tasks.signals
225222
django.template.base.PartialTemplate
226-
django.template.context_processors.csp
227223
django.template.defaulttags.PartialDefNode
228224
django.template.defaulttags.PartialNode
229225
django.template.defaulttags.partial_func
@@ -232,7 +228,6 @@ django.test.runner.QueryFormatter
232228
django.test.selenium.SeleniumTestCase.get_browser_logs
233229
django.test.testcases._AssertTemplateUsedContext.rendered_template_names
234230
django.utils.copy
235-
django.utils.csp
236231
django.utils.datastructures.DeferredSubDict
237232
django.utils.deprecation.RemovedInDjango60Warning
238233
django.utils.deprecation.RemovedInDjango70Warning
@@ -245,4 +240,3 @@ django.utils.itercompat
245240
django.utils.json
246241
django.utils.log.log_message
247242
django.utils.text.acompress_sequence
248-
django.views.decorators.csp
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.http import HttpRequest, HttpResponse
2+
from django.views.decorators.csp import csp_override, csp_report_only_override
3+
from typing_extensions import assert_type
4+
5+
6+
@csp_override(
7+
{
8+
"default-src": ["'self'"],
9+
"script-src": ["'self'", "'unsafe-inline'"],
10+
"report-uri": "/path/to/reports-endpoint/",
11+
}
12+
)
13+
def my_view(request: HttpRequest) -> HttpResponse: ...
14+
15+
16+
@csp_report_only_override(
17+
{
18+
"default-src": ["'self'"],
19+
"script-src": ["'self'", "'unsafe-inline'"],
20+
"report-uri": "/path/to/reports-endpoint/",
21+
}
22+
)
23+
def my_view2(request: HttpRequest) -> HttpResponse: ...
24+
25+
26+
assert_type(my_view(HttpRequest()), HttpResponse)
27+
assert_type(my_view2(HttpRequest()), HttpResponse)

0 commit comments

Comments
 (0)