Skip to content

Commit 7153092

Browse files
jwhitlockRob Hudson
authored andcommitted
Add type hints
1 parent b0a5e45 commit 7153092

26 files changed

+436
-254
lines changed

csp/apps.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
class CspConfig(AppConfig):
88
name = "csp"
99

10-
def ready(self):
11-
checks.register(check_django_csp_lt_4_0, checks.Tags.security)
10+
def ready(self) -> None:
11+
# Ignore known issue typeddjango/django-stubs #2232
12+
# The overload of CheckRegistry.register as a function is incomplete
13+
checks.register(check_django_csp_lt_4_0, checks.Tags.security) # type: ignore

csp/checks.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
from __future__ import annotations
12
import pprint
3+
from typing import Dict, Tuple, Any, Optional, Sequence, TYPE_CHECKING, List
24

35
from django.conf import settings
46
from django.core.checks import Error
57

68
from csp.constants import NONCE
79

10+
if TYPE_CHECKING:
11+
from django.apps.config import AppConfig
12+
813

914
OUTDATED_SETTINGS = [
1015
"CSP_CHILD_SRC",
@@ -40,21 +45,21 @@
4045
]
4146

4247

43-
def migrate_settings():
48+
def migrate_settings() -> Tuple[Dict[str, Any], bool]:
4449
# This function is used to migrate settings from the old format to the new format.
45-
config = {
50+
config: Dict[str, Any] = {
4651
"DIRECTIVES": {},
4752
}
48-
REPORT_ONLY = False
4953

50-
if hasattr(settings, "CSP_REPORT_ONLY"):
51-
REPORT_ONLY = settings.CSP_REPORT_ONLY
54+
REPORT_ONLY = getattr(settings, "CSP_REPORT_ONLY", False)
5255

53-
if hasattr(settings, "CSP_EXCLUDE_URL_PREFIXES"):
54-
config["EXCLUDE_URL_PREFIXES"] = settings.CSP_EXCLUDE_URL_PREFIXES
56+
_EXCLUDE_URL_PREFIXES = getattr(settings, "CSP_EXCLUDE_URL_PREFIXES", None)
57+
if _EXCLUDE_URL_PREFIXES is not None:
58+
config["EXCLUDE_URL_PREFIXES"] = _EXCLUDE_URL_PREFIXES
5559

56-
if hasattr(settings, "CSP_REPORT_PERCENTAGE"):
57-
config["REPORT_PERCENTAGE"] = round(settings.CSP_REPORT_PERCENTAGE * 100)
60+
_REPORT_PERCENTAGE = getattr(settings, "CSP_REPORT_PERCENTAGE", None)
61+
if _REPORT_PERCENTAGE is not None:
62+
config["REPORT_PERCENTAGE"] = round(_REPORT_PERCENTAGE * 100)
5863

5964
include_nonce_in = getattr(settings, "CSP_INCLUDE_NONCE_IN", [])
6065

@@ -70,7 +75,7 @@ def migrate_settings():
7075
return config, REPORT_ONLY
7176

7277

73-
def check_django_csp_lt_4_0(app_configs, **kwargs):
78+
def check_django_csp_lt_4_0(app_configs: Optional[Sequence[AppConfig]], **kwargs: Any) -> List[Error]:
7479
check_settings = OUTDATED_SETTINGS + ["CSP_REPORT_ONLY", "CSP_EXCLUDE_URL_PREFIXES", "CSP_REPORT_PERCENTAGE"]
7580
if any(hasattr(settings, setting) for setting in check_settings):
7681
# Try to build the new config.

csp/constants.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, Type
2+
13
HEADER = "Content-Security-Policy"
24
HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"
35

@@ -15,12 +17,12 @@
1517
class Nonce:
1618
_instance = None
1719

18-
def __new__(cls, *args, **kwargs):
20+
def __new__(cls: Type["Nonce"], *args: Any, **kwargs: Any) -> "Nonce":
1921
if cls._instance is None:
2022
cls._instance = super().__new__(cls)
2123
return cls._instance
2224

23-
def __repr__(self):
25+
def __repr__(self) -> str:
2426
return "csp.constants.NONCE"
2527

2628

csp/context_processors.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
def nonce(request):
1+
from __future__ import annotations
2+
from typing import Dict, Literal, TYPE_CHECKING
3+
4+
if TYPE_CHECKING:
5+
from django.http import HttpRequest
6+
7+
8+
def nonce(request: HttpRequest) -> Dict[Literal["CSP_NONCE"], str]:
29
nonce = request.csp_nonce if hasattr(request, "csp_nonce") else ""
310

411
return {"CSP_NONCE": nonce}

csp/contrib/rate_limiting.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
13
import random
24

35
from django.conf import settings
46

57
from csp.middleware import CSPMiddleware
68
from csp.utils import build_policy
79

10+
if TYPE_CHECKING:
11+
from django.http import HttpRequest, HttpResponseBase
12+
813

914
class RateLimitedCSPMiddleware(CSPMiddleware):
1015
"""A CSP middleware that rate-limits the number of violation reports sent
1116
to report-uri by excluding it from some requests."""
1217

13-
def build_policy(self, request, response):
18+
def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str:
1419
config = getattr(response, "_csp_config", None)
1520
update = getattr(response, "_csp_update", None)
1621
replace = getattr(response, "_csp_replace", {})
@@ -28,7 +33,7 @@ def build_policy(self, request, response):
2833

2934
return build_policy(config=config, update=update, replace=replace, nonce=nonce)
3035

31-
def build_policy_ro(self, request, response):
36+
def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str:
3237
config = getattr(response, "_csp_config_ro", None)
3338
update = getattr(response, "_csp_update_ro", None)
3439
replace = getattr(response, "_csp_replace_ro", {})

csp/decorators.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from functools import wraps
2+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
23

4+
if TYPE_CHECKING:
5+
from django.http import HttpRequest, HttpResponseBase
36

4-
def csp_exempt(REPORT_ONLY=None):
7+
# A generic Django view function
8+
_VIEW_T = Callable[[HttpRequest], HttpResponseBase]
9+
_VIEW_DECORATOR_T = Callable[[_VIEW_T], _VIEW_T]
10+
11+
12+
def csp_exempt(REPORT_ONLY: Optional[bool] = None) -> _VIEW_DECORATOR_T:
513
if callable(REPORT_ONLY):
614
raise RuntimeError(
715
"Incompatible `csp_exempt` decorator usage. This decorator now requires arguments, "
@@ -10,14 +18,14 @@ def csp_exempt(REPORT_ONLY=None):
1018
"information."
1119
)
1220

13-
def decorator(f):
21+
def decorator(f: _VIEW_T) -> _VIEW_T:
1422
@wraps(f)
15-
def _wrapped(*a, **kw):
23+
def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase:
1624
resp = f(*a, **kw)
1725
if REPORT_ONLY:
18-
resp._csp_exempt_ro = True
26+
setattr(resp, "_csp_exempt_ro", True)
1927
else:
20-
resp._csp_exempt = True
28+
setattr(resp, "_csp_exempt", True)
2129
return resp
2230

2331
return _wrapped
@@ -32,58 +40,61 @@ def _wrapped(*a, **kw):
3240
)
3341

3442

35-
def csp_update(config=None, REPORT_ONLY=False, **kwargs):
43+
def csp_update(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T:
3644
if config is None and kwargs:
3745
raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_update"))
3846

39-
def decorator(f):
47+
def decorator(f: _VIEW_T) -> _VIEW_T:
4048
@wraps(f)
41-
def _wrapped(*a, **kw):
49+
def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase:
4250
resp = f(*a, **kw)
4351
if REPORT_ONLY:
44-
resp._csp_update_ro = config
52+
setattr(resp, "_csp_update_ro", config)
4553
else:
46-
resp._csp_update = config
54+
setattr(resp, "_csp_update", config)
4755
return resp
4856

4957
return _wrapped
5058

5159
return decorator
5260

5361

54-
def csp_replace(config=None, REPORT_ONLY=False, **kwargs):
62+
def csp_replace(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T:
5563
if config is None and kwargs:
5664
raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_replace"))
5765

58-
def decorator(f):
66+
def decorator(f: _VIEW_T) -> _VIEW_T:
5967
@wraps(f)
60-
def _wrapped(*a, **kw):
68+
def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase:
6169
resp = f(*a, **kw)
6270
if REPORT_ONLY:
63-
resp._csp_replace_ro = config
71+
setattr(resp, "_csp_replace_ro", config)
6472
else:
65-
resp._csp_replace = config
73+
setattr(resp, "_csp_replace", config)
6674
return resp
6775

6876
return _wrapped
6977

7078
return decorator
7179

7280

73-
def csp(config=None, REPORT_ONLY=False, **kwargs):
81+
def csp(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T:
7482
if config is None and kwargs:
7583
raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp"))
7684

77-
config = {k: [v] if isinstance(v, str) else v for k, v in config.items()}
85+
if config is None:
86+
processed_config: Dict[str, List[Any]] = {}
87+
else:
88+
processed_config = {k: [v] if isinstance(v, str) else v for k, v in config.items()}
7889

79-
def decorator(f):
90+
def decorator(f: _VIEW_T) -> _VIEW_T:
8091
@wraps(f)
81-
def _wrapped(*a, **kw):
92+
def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase:
8293
resp = f(*a, **kw)
8394
if REPORT_ONLY:
84-
resp._csp_config_ro = config
95+
setattr(resp, "_csp_config_ro", processed_config)
8596
else:
86-
resp._csp_config = config
97+
setattr(resp, "_csp_config", processed_config)
8798
return resp
8899

89100
return _wrapped

csp/extensions/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1+
from __future__ import annotations
2+
from typing import Callable, TYPE_CHECKING, Any
3+
14
from jinja2 import nodes
25
from jinja2.ext import Extension
36

47
from csp.utils import SCRIPT_ATTRS, build_script_tag
58

9+
if TYPE_CHECKING:
10+
from jinja2.parser import Parser
11+
612

713
class NoncedScript(Extension):
814
# a set of names that trigger the extension.
915
tags = {"script"}
1016

11-
def parse(self, parser):
17+
def parse(self, parser: Parser) -> nodes.Node:
1218
# the first token is the token that started the tag. In our case
1319
# we only listen to ``'script'`` so this will be a name token with
1420
# `script` as value. We get the line number so that we can give
@@ -26,13 +32,13 @@ def parse(self, parser):
2632

2733
# now we parse the body of the script block up to `endscript` and
2834
# drop the needle (which would always be `endscript` in that case)
29-
body = parser.parse_statements(["name:endscript"], drop_needle=True)
35+
body = parser.parse_statements(("name:endscript",), drop_needle=True)
3036

3137
# now return a `CallBlock` node that calls our _render_script
3238
# helper method on this extension.
3339
return nodes.CallBlock(self.call_method("_render_script", kwargs=kwargs), [], [], body).set_lineno(lineno)
3440

35-
def _render_script(self, caller, **kwargs):
41+
def _render_script(self, caller: Callable[[], str], **kwargs: Any) -> str:
3642
ctx = kwargs.pop("ctx")
3743
request = ctx.get("request")
3844
kwargs["nonce"] = request.csp_nonce

csp/middleware.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from __future__ import annotations
12
import base64
23
import http.client as http_client
34
import os
45
from functools import partial
6+
from typing import TYPE_CHECKING
57

68
from django.conf import settings
79
from django.utils.deprecation import MiddlewareMixin
@@ -10,6 +12,9 @@
1012
from csp.constants import HEADER, HEADER_REPORT_ONLY
1113
from csp.utils import build_policy
1214

15+
if TYPE_CHECKING:
16+
from django.http import HttpRequest, HttpResponseBase
17+
1318

1419
class CSPMiddleware(MiddlewareMixin):
1520
"""
@@ -21,17 +26,20 @@ class CSPMiddleware(MiddlewareMixin):
2126
2227
"""
2328

24-
def _make_nonce(self, request):
29+
def _make_nonce(self, request: HttpRequest) -> str:
2530
# Ensure that any subsequent calls to request.csp_nonce return the same value
26-
if not getattr(request, "_csp_nonce", None):
27-
request._csp_nonce = base64.b64encode(os.urandom(16)).decode("ascii")
28-
return request._csp_nonce
31+
stored_nonce = getattr(request, "_csp_nonce", None)
32+
if isinstance(stored_nonce, str):
33+
return stored_nonce
34+
nonce = base64.b64encode(os.urandom(16)).decode("ascii")
35+
setattr(request, "_csp_nonce", nonce)
36+
return nonce
2937

30-
def process_request(self, request):
38+
def process_request(self, request: HttpRequest) -> None:
3139
nonce = partial(self._make_nonce, request)
32-
request.csp_nonce = SimpleLazyObject(nonce)
40+
setattr(request, "csp_nonce", SimpleLazyObject(nonce))
3341

34-
def process_response(self, request, response):
42+
def process_response(self, request: HttpRequest, response: HttpResponseBase) -> HttpResponseBase:
3543
# Check for debug view
3644
exempted_debug_codes = (
3745
http_client.INTERNAL_SERVER_ERROR,
@@ -45,8 +53,9 @@ def process_response(self, request, response):
4553
# Only set header if not already set and not an excluded prefix and not exempted.
4654
is_not_exempt = getattr(response, "_csp_exempt", False) is False
4755
no_header = HEADER not in response
48-
prefixes = getattr(settings, "CONTENT_SECURITY_POLICY", {}).get("EXCLUDE_URL_PREFIXES", ())
49-
is_not_excluded = not request.path_info.startswith(prefixes)
56+
policy = getattr(settings, "CONTENT_SECURITY_POLICY", None) or {}
57+
prefixes = policy.get("EXCLUDE_URL_PREFIXES", None) or ()
58+
is_not_excluded = not request.path_info.startswith(tuple(prefixes))
5059
if all((no_header, is_not_exempt, is_not_excluded)):
5160
response[HEADER] = csp
5261

@@ -55,21 +64,22 @@ def process_response(self, request, response):
5564
# Only set header if not already set and not an excluded prefix and not exempted.
5665
is_not_exempt = getattr(response, "_csp_exempt_ro", False) is False
5766
no_header = HEADER_REPORT_ONLY not in response
58-
prefixes = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", {}).get("EXCLUDE_URL_PREFIXES", ())
59-
is_not_excluded = not request.path_info.startswith(prefixes)
67+
policy = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", None) or {}
68+
prefixes = policy.get("EXCLUDE_URL_PREFIXES", None) or ()
69+
is_not_excluded = not request.path_info.startswith(tuple(prefixes))
6070
if all((no_header, is_not_exempt, is_not_excluded)):
6171
response[HEADER_REPORT_ONLY] = csp_ro
6272

6373
return response
6474

65-
def build_policy(self, request, response):
75+
def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str:
6676
config = getattr(response, "_csp_config", None)
6777
update = getattr(response, "_csp_update", None)
6878
replace = getattr(response, "_csp_replace", None)
6979
nonce = getattr(request, "_csp_nonce", None)
7080
return build_policy(config=config, update=update, replace=replace, nonce=nonce)
7181

72-
def build_policy_ro(self, request, response):
82+
def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str:
7383
config = getattr(response, "_csp_config_ro", None)
7484
update = getattr(response, "_csp_update_ro", None)
7585
replace = getattr(response, "_csp_replace_ro", None)

csp/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)