diff --git a/csp/checks.py b/csp/checks.py index 9c15b0a..6e7e0c4 100644 --- a/csp/checks.py +++ b/csp/checks.py @@ -1,6 +1,7 @@ from __future__ import annotations + import pprint -from typing import Dict, Tuple, Any, Optional, Sequence, TYPE_CHECKING, List +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple from django.conf import settings from django.core.checks import Error diff --git a/csp/context_processors.py b/csp/context_processors.py index 4c34e0a..13f3808 100644 --- a/csp/context_processors.py +++ b/csp/context_processors.py @@ -1,5 +1,6 @@ from __future__ import annotations -from typing import Dict, Literal, TYPE_CHECKING + +from typing import TYPE_CHECKING, Dict, Literal if TYPE_CHECKING: from django.http import HttpRequest diff --git a/csp/contrib/rate_limiting.py b/csp/contrib/rate_limiting.py index f26e86e..9633a87 100644 --- a/csp/contrib/rate_limiting.py +++ b/csp/contrib/rate_limiting.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING + import random +from typing import TYPE_CHECKING from django.conf import settings @@ -8,14 +9,14 @@ from csp.utils import build_policy if TYPE_CHECKING: - from django.http import HttpRequest, HttpResponse + from django.http import HttpRequest, HttpResponseBase class RateLimitedCSPMiddleware(CSPMiddleware): """A CSP middleware that rate-limits the number of violation reports sent to report-uri by excluding it from some requests.""" - def build_policy(self, request: HttpRequest, response: HttpResponse) -> str: + def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str: config = getattr(response, "_csp_config", None) update = getattr(response, "_csp_update", None) replace = getattr(response, "_csp_replace", {}) @@ -33,7 +34,7 @@ def build_policy(self, request: HttpRequest, response: HttpResponse) -> str: return build_policy(config=config, update=update, replace=replace, nonce=nonce) - def build_policy_ro(self, request: HttpRequest, response: HttpResponse) -> str: + def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str: config = getattr(response, "_csp_config_ro", None) update = getattr(response, "_csp_update_ro", None) replace = getattr(response, "_csp_replace_ro", {}) diff --git a/csp/decorators.py b/csp/decorators.py index 890d819..e36b0ab 100644 --- a/csp/decorators.py +++ b/csp/decorators.py @@ -1,10 +1,12 @@ from functools import wraps -from typing import Callable, Optional, Any, Dict, List -from django.http import HttpRequest, HttpResponse +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional -# A generic Django view function -_VIEW_T = Callable[[HttpRequest], HttpResponse] -_VIEW_DECORATOR_T = Callable[[_VIEW_T], _VIEW_T] +if TYPE_CHECKING: + from django.http import HttpRequest, HttpResponseBase + + # A generic Django view function + _VIEW_T = Callable[[HttpRequest], HttpResponseBase] + _VIEW_DECORATOR_T = Callable[[_VIEW_T], _VIEW_T] def csp_exempt(REPORT_ONLY: Optional[bool] = None) -> _VIEW_DECORATOR_T: @@ -18,7 +20,7 @@ def csp_exempt(REPORT_ONLY: Optional[bool] = None) -> _VIEW_DECORATOR_T: def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) - def _wrapped(*a: Any, **kw: Any) -> HttpResponse: + def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_exempt_ro", True) @@ -44,7 +46,7 @@ def csp_update(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = Fals def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) - def _wrapped(*a: Any, **kw: Any) -> HttpResponse: + def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_update_ro", config) @@ -63,7 +65,7 @@ def csp_replace(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = Fal def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) - def _wrapped(*a: Any, **kw: Any) -> HttpResponse: + def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_replace_ro", config) @@ -87,7 +89,7 @@ def csp(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kw def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) - def _wrapped(*a: Any, **kw: Any) -> HttpResponse: + def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_config_ro", processed_config) diff --git a/csp/extensions/__init__.py b/csp/extensions/__init__.py index 45c4de4..8ddc782 100644 --- a/csp/extensions/__init__.py +++ b/csp/extensions/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations -from typing import Callable, TYPE_CHECKING, Any + +from typing import TYPE_CHECKING, Any, Callable from jinja2 import nodes from jinja2.ext import Extension diff --git a/csp/middleware.py b/csp/middleware.py index 0a33ec7..ff58c61 100644 --- a/csp/middleware.py +++ b/csp/middleware.py @@ -1,4 +1,5 @@ from __future__ import annotations + import base64 import http.client as http_client import os @@ -13,7 +14,7 @@ from csp.utils import build_policy if TYPE_CHECKING: - from django.http import HttpRequest, HttpResponse + from django.http import HttpRequest, HttpResponseBase class CSPMiddleware(MiddlewareMixin): @@ -39,7 +40,7 @@ def process_request(self, request: HttpRequest) -> None: nonce = partial(self._make_nonce, request) setattr(request, "csp_nonce", SimpleLazyObject(nonce)) - def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: + def process_response(self, request: HttpRequest, response: HttpResponseBase) -> HttpResponseBase: # Check for debug view exempted_debug_codes = ( http_client.INTERNAL_SERVER_ERROR, @@ -72,14 +73,14 @@ def process_response(self, request: HttpRequest, response: HttpResponse) -> Http return response - def build_policy(self, request: HttpRequest, response: HttpResponse) -> str: + def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str: config = getattr(response, "_csp_config", None) update = getattr(response, "_csp_update", None) replace = getattr(response, "_csp_replace", None) nonce = getattr(request, "_csp_nonce", None) return build_policy(config=config, update=update, replace=replace, nonce=nonce) - def build_policy_ro(self, request: HttpRequest, response: HttpResponse) -> str: + def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str: config = getattr(response, "_csp_config_ro", None) update = getattr(response, "_csp_update_ro", None) replace = getattr(response, "_csp_replace_ro", None) diff --git a/csp/templatetags/csp.py b/csp/templatetags/csp.py index a28bfc1..bf95296 100644 --- a/csp/templatetags/csp.py +++ b/csp/templatetags/csp.py @@ -1,12 +1,14 @@ from __future__ import annotations + from typing import TYPE_CHECKING, Optional + from django import template from django.template.base import token_kwargs from csp.utils import build_script_tag if TYPE_CHECKING: - from django.template.base import NodeList, FilterExpression, Token, Parser + from django.template.base import FilterExpression, NodeList, Parser, Token from django.template.context import Context register = template.Library() diff --git a/csp/tests/test_decorators.py b/csp/tests/test_decorators.py index 6e20b79..e233d93 100644 --- a/csp/tests/test_decorators.py +++ b/csp/tests/test_decorators.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import TYPE_CHECKING import pytest @@ -12,14 +13,14 @@ from csp.tests.utils import response if TYPE_CHECKING: - from django.http import HttpRequest + from django.http import HttpRequest, HttpResponseBase mw = CSPMiddleware(response()) def test_csp_exempt() -> None: @csp_exempt() - def view(request: HttpRequest) -> HttpResponse: + def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view(RequestFactory().get("/")) @@ -29,7 +30,7 @@ def view(request: HttpRequest) -> HttpResponse: def test_csp_exempt_ro() -> None: @csp_exempt(REPORT_ONLY=True) - def view(request: HttpRequest) -> HttpResponse: + def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view(RequestFactory().get("/")) @@ -41,7 +42,7 @@ def view(request: HttpRequest) -> HttpResponse: def test_csp_update() -> None: request = RequestFactory().get("/") - def view_without_decorator(request: HttpRequest) -> HttpResponse: + def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) @@ -51,7 +52,7 @@ def view_without_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_update({"img-src": ["bar.com", NONCE]}) - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -74,7 +75,7 @@ def view_with_decorator(request: HttpRequest) -> HttpResponse: def test_csp_update_ro() -> None: request = RequestFactory().get("/") - def view_without_decorator(request: HttpRequest) -> HttpResponse: + def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) @@ -84,7 +85,7 @@ def view_without_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_update({"img-src": ["bar.com", NONCE]}, REPORT_ONLY=True) - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -107,7 +108,7 @@ def view_with_decorator(request: HttpRequest) -> HttpResponse: def test_csp_replace() -> None: request = RequestFactory().get("/") - def view_without_decorator(request: HttpRequest) -> HttpResponse: + def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) @@ -117,7 +118,7 @@ def view_without_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": ["bar.com"]}) - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -134,7 +135,7 @@ def view_with_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": None}) - def view_removing_directive(request: HttpRequest) -> HttpResponse: + def view_removing_directive(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_removing_directive(request) @@ -148,7 +149,7 @@ def view_removing_directive(request: HttpRequest) -> HttpResponse: def test_csp_replace_ro() -> None: request = RequestFactory().get("/") - def view_without_decorator(request: HttpRequest) -> HttpResponse: + def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) @@ -158,7 +159,7 @@ def view_without_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": ["bar.com"]}, REPORT_ONLY=True) - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -175,7 +176,7 @@ def view_with_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": None}, REPORT_ONLY=True) - def view_removing_directive(request: HttpRequest) -> HttpResponse: + def view_removing_directive(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_removing_directive(request) @@ -188,7 +189,7 @@ def view_removing_directive(request: HttpRequest) -> HttpResponse: def test_csp() -> None: request = RequestFactory().get("/") - def view_without_decorator(request: HttpRequest) -> HttpResponse: + def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) @@ -198,7 +199,7 @@ def view_without_decorator(request: HttpRequest) -> HttpResponse: assert policy_list == ["default-src 'self'"] @csp({"img-src": ["foo.com"], "font-src": ["bar.com"]}) - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -218,7 +219,7 @@ def view_with_decorator(request: HttpRequest) -> HttpResponse: def test_csp_ro() -> None: request = RequestFactory().get("/") - def view_without_decorator(request: HttpRequest) -> HttpResponse: + def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) @@ -229,7 +230,7 @@ def view_without_decorator(request: HttpRequest) -> HttpResponse: @csp({"img-src": ["foo.com"], "font-src": ["bar.com"]}, REPORT_ONLY=True) @csp({}) # CSP with no directives effectively removes the header. - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -251,7 +252,7 @@ def test_csp_string_values() -> None: request = RequestFactory().get("/") @csp({"img-src": "foo.com", "font-src": "bar.com"}) - def view_with_decorator(request: HttpRequest) -> HttpResponse: + def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) @@ -268,7 +269,7 @@ def test_csp_exempt_error() -> None: with pytest.raises(RuntimeError) as excinfo: # Ignore type error since we're checking for the exception raised for 3.x syntax @csp_exempt # type: ignore - def view(request: HttpRequest) -> HttpResponse: + def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp_exempt` decorator usage" in str(excinfo.value) @@ -278,7 +279,7 @@ def test_csp_update_error() -> None: with pytest.raises(RuntimeError) as excinfo: @csp_update(IMG_SRC="bar.com") - def view(request: HttpRequest) -> HttpResponse: + def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp_update` decorator arguments" in str(excinfo.value) @@ -288,7 +289,7 @@ def test_csp_replace_error() -> None: with pytest.raises(RuntimeError) as excinfo: @csp_replace(IMG_SRC="bar.com") - def view(request: HttpRequest) -> HttpResponse: + def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp_replace` decorator arguments" in str(excinfo.value) @@ -298,7 +299,7 @@ def test_csp_error() -> None: with pytest.raises(RuntimeError) as excinfo: @csp(IMG_SRC=["bar.com"]) - def view(request: HttpRequest) -> HttpResponse: + def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp` decorator arguments" in str(excinfo.value) diff --git a/csp/tests/utils.py b/csp/tests/utils.py index 36512ca..bcd6120 100644 --- a/csp/tests/utils.py +++ b/csp/tests/utils.py @@ -1,6 +1,7 @@ from __future__ import annotations + from abc import ABC, abstractmethod -from typing import Dict, Optional, TYPE_CHECKING, Callable, Any, Tuple +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple from django.http import HttpResponse from django.template import Context, Template, engines