From 0f42b9e5c94bd44ee93e0df7a6c7333dbd56f62d Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 12:05:04 +0200 Subject: [PATCH 1/2] fix(middleware): pass global behavior rules and require GUARD_SECURITY_CONFIG like fastapi-guard Pass global behavior return rules to the engine response pass. Raise ImproperlyConfigured when GUARD_SECURITY_CONFIG is unset instead of silently running with SecurityConfig() defaults. Add real-routing parity tests for passthrough, bypass, blocked and global rules. --- AGENTS.md | 2 +- CLAUDE.md | 2 +- djangoapi_guard/middleware.py | 12 ++- tests/engine_parity_urls.py | 29 +++++++ tests/test_engine_parity.py | 153 ++++++++++++++++++++++++++++++++++ tests/test_middleware.py | 7 +- 6 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 tests/engine_parity_urls.py create mode 100644 tests/test_engine_parity.py diff --git a/AGENTS.md b/AGENTS.md index 7beeb24..e7796ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ GUARD_SECURITY_CONFIG = SecurityConfig( ) ``` -`GUARD_SECURITY_CONFIG` must be a `SecurityConfig` instance; when the setting is absent the middleware falls back to `SecurityConfig()` defaults. +`GUARD_SECURITY_CONFIG` must be a `SecurityConfig` instance; when the setting is absent the middleware raises `ImproperlyConfigured` (there is no silent default, matching fastapi-guard requiring `config=`). ## Development Commands diff --git a/CLAUDE.md b/CLAUDE.md index 7beeb24..e7796ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,7 +101,7 @@ GUARD_SECURITY_CONFIG = SecurityConfig( ) ``` -`GUARD_SECURITY_CONFIG` must be a `SecurityConfig` instance; when the setting is absent the middleware falls back to `SecurityConfig()` defaults. +`GUARD_SECURITY_CONFIG` must be a `SecurityConfig` instance; when the setting is absent the middleware raises `ImproperlyConfigured` (there is no silent default, matching fastapi-guard requiring `config=`). ## Development Commands diff --git a/djangoapi_guard/middleware.py b/djangoapi_guard/middleware.py index 7ba209d..b97968e 100644 --- a/djangoapi_guard/middleware.py +++ b/djangoapi_guard/middleware.py @@ -4,6 +4,7 @@ from typing import Any, cast from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest, HttpResponse from guard_core.exceptions import GuardRedisError from guard_core.models import SecurityConfig @@ -41,7 +42,10 @@ def __init__( config = getattr(settings, "GUARD_SECURITY_CONFIG", None) if config is None: - config = SecurityConfig() + raise ImproperlyConfigured( + "DjangoAPIGuard requires settings.GUARD_SECURITY_CONFIG to be " + "a SecurityConfig instance" + ) self.config: SecurityConfig = config self.logger: logging.Logger = setup_custom_logging( @@ -380,6 +384,9 @@ def _finalize_response( response_time, route_config, process_behavioral_rules=self.behavioral_processor.process_return_rules, + process_global_behavioral_rules=( + self.behavioral_processor.process_global_return_rules + ), ) final = unwrap_response(result) if self._cors_handler: @@ -535,6 +542,9 @@ def _process_response( response_time, route_config, process_behavioral_rules=self.behavioral_processor.process_return_rules, + process_global_behavioral_rules=( + self.behavioral_processor.process_global_return_rules + ), ) return unwrap_response(result) diff --git a/tests/engine_parity_urls.py b/tests/engine_parity_urls.py new file mode 100644 index 0000000..336b4a3 --- /dev/null +++ b/tests/engine_parity_urls.py @@ -0,0 +1,29 @@ +from django.http import HttpRequest, JsonResponse +from django.urls import path +from guard_core.models import SecurityConfig + +from djangoapi_guard import SecurityDecorator + +security = SecurityDecorator( + SecurityConfig(enable_redis=False, behavior_scan_response_body=True) +) + + +def health(request: HttpRequest) -> JsonResponse: + return JsonResponse({"message": "ok"}) + + +def open_view(request: HttpRequest) -> JsonResponse: + return JsonResponse({"message": "Echo response"}) + + +@security.bypass(["all"]) +def bypassed(request: HttpRequest) -> JsonResponse: + return JsonResponse({"message": "real view"}) + + +urlpatterns = [ + path("health/", health), + path("open/", open_view), + path("bypassed/", bypassed), +] diff --git a/tests/test_engine_parity.py b/tests/test_engine_parity.py new file mode 100644 index 0000000..15233ce --- /dev/null +++ b/tests/test_engine_parity.py @@ -0,0 +1,153 @@ +"""Engine parity: the adapter routes through the engine exactly like fastapi-guard. + +Real Django URL resolution and views behind the middleware, real engine bypass +handler and response factory. The metrics collector is spied on (it still runs) +to prove which responses reach the engine response pass. +""" + +from collections.abc import Iterator +from typing import Any + +import pytest +from django.core.exceptions import ImproperlyConfigured +from django.core.handlers.base import BaseHandler +from django.http import HttpResponse +from django.test import RequestFactory, override_settings +from guard_core.models import SecurityConfig +from guard_core.protocols.response_protocol import GuardResponse + +from djangoapi_guard.middleware import DjangoAPIGuard +from tests import engine_parity_urls + + +@pytest.fixture(autouse=True) +def _urlconf() -> Iterator[None]: + with override_settings(ROOT_URLCONF="tests.engine_parity_urls", MIDDLEWARE=[]): + yield + + +def _build(**overrides: Any) -> tuple[DjangoAPIGuard, list[str]]: + base: dict[str, Any] = { + "enable_redis": False, + "enable_agent": False, + "enable_penetration_detection": False, + "whitelist": None, + "blacklist": [], + "exclude_paths": ["/health"], + "security_headers": {"enabled": True}, + } + base.update(overrides) + handler = BaseHandler() + handler.load_middleware() + with override_settings(GUARD_SECURITY_CONFIG=SecurityConfig(**base)): + guard = DjangoAPIGuard(handler.get_response) + guard.set_decorator_handler(engine_parity_urls.security) + metrics: list[str] = [] + assert guard.metrics_collector is not None + real = guard.metrics_collector.collect_request_metrics + + def spy(request: Any, response_time: float, status_code: int) -> None: + metrics.append(request.url_path) + real(request, response_time, status_code) + + guard.metrics_collector.collect_request_metrics = spy # type: ignore[method-assign] + return guard, metrics + + +def _get( + guard: DjangoAPIGuard, path: str, remote_addr: str | None = "127.0.0.1" +) -> HttpResponse: + request = RequestFactory().get(path) + if remote_addr is None: + request.META.pop("REMOTE_ADDR", None) + else: + request.META["REMOTE_ADDR"] = remote_addr + return guard(request) + + +def test_missing_security_config_is_rejected() -> None: + with override_settings(): + from django.conf import settings + + if hasattr(settings, "GUARD_SECURITY_CONFIG"): + del settings.GUARD_SECURITY_CONFIG + with pytest.raises(ImproperlyConfigured, match="GUARD_SECURITY_CONFIG"): + DjangoAPIGuard(lambda request: HttpResponse("ok")) + + +def test_excluded_path_reaches_view_with_scoped_checks_and_response_pass() -> None: + guard, metrics = _build(blacklist=["10.9.9.9"]) + + ok = _get(guard, "/health/") + assert ok.status_code == 200 + assert ok["X-Content-Type-Options"] == "nosniff" + assert metrics == ["/health/"] + + blocked = _get(guard, "/health/", "10.9.9.9") + assert blocked.status_code == 403 + assert metrics == ["/health/"] + + +def test_no_client_address_gets_fail_secure_403_by_default() -> None: + guard, metrics = _build() + + resp = _get(guard, "/open/", None) + + assert resp.status_code == 403 + assert b"Client address could not be determined" in resp.content + assert metrics == [] + + +def test_no_client_address_fail_secure_false_runs_the_pipeline() -> None: + guard, metrics = _build(fail_secure=False) + + resp = _get(guard, "/open/", None) + + assert resp.status_code == 200 + assert metrics == ["/open/"] + + +def test_route_bypass_reaches_real_view_and_skips_response_pass() -> None: + def modifier(response: GuardResponse) -> GuardResponse: + response.headers["X-Modified"] = "1" + return response + + guard, metrics = _build(custom_response_modifier=modifier) + + resp = _get(guard, "/bypassed/") + + assert resp.status_code == 200 + assert b"real view" in resp.content + assert resp["X-Modified"] == "1" + assert metrics == [] + assert "X-Content-Type-Options" not in resp + + +def test_blocked_request_emits_no_request_metrics() -> None: + guard, metrics = _build(blacklist=["10.9.9.9"]) + + assert _get(guard, "/open/", "10.9.9.9").status_code == 403 + assert metrics == [] + assert _get(guard, "/open/").status_code == 200 + assert metrics == ["/open/"] + + +def test_global_behavior_return_rule_fires() -> None: + guard, _metrics = _build( + behavior_scan_response_body=True, + global_behavior_rules=[ + { + "rule_type": "return_pattern", + "threshold": 2, + "window": 60, + "pattern": "json:message==Echo response", + "action": "ban", + "ban_duration": 30, + } + ], + ) + + codes = [_get(guard, "/open/", "203.0.113.9").status_code for _ in range(5)] + + assert codes[0] == 200 + assert 403 in codes diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 0fc4ff6..e267ee6 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1462,12 +1462,13 @@ def get_response(request: HttpRequest) -> HttpResponse: return DjangoAPIGuard(get_response) - def test_config_default_from_settings(self) -> None: + def test_missing_config_is_rejected(self) -> None: import django.conf + from django.core.exceptions import ImproperlyConfigured django.conf.settings.GUARD_SECURITY_CONFIG = None - middleware = DjangoAPIGuard(lambda _: HttpResponse("OK")) - assert middleware.config is not None + with pytest.raises(ImproperlyConfigured, match="GUARD_SECURITY_CONFIG"): + DjangoAPIGuard(lambda _: HttpResponse("OK")) def test_guard_response_factory_property(self) -> None: from djangoapi_guard.adapters import DjangoResponseFactory From 0f39dd19a020a0c3fb634458d68aef64d7669da5 Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 12:44:03 +0200 Subject: [PATCH 2/2] docs(readme): build GUARD_SECURITY_CONFIG as a SecurityConfig and fix the advanced example The README examples used a plain dict and a nonexistent djangoapi_guard.models import, which cannot work now that a missing or non-SecurityConfig setting is rejected. Convert them to SecurityConfig, state that the setting is required, and fix the advanced example so it starts (status: return pattern, guard request and response API in its hooks). --- README.md | 188 +++++++++--------- .../advanced_app/routes/behavioral.py | 4 +- examples/advanced/advanced_app/security.py | 23 ++- 3 files changed, 111 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 094a84d..a31d1fe 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,11 @@ To use `djapi-guard`, you need to add the middleware to your Django project and ### settings.py +`settings.GUARD_SECURITY_CONFIG` is required and must be a `SecurityConfig` instance: if it is missing, the middleware raises `ImproperlyConfigured` at startup instead of silently running with defaults. + ```python +from djangoapi_guard import SecurityConfig + MIDDLEWARE = [ # Add DjangoAPI Guard middleware early in the stack "djangoapi_guard.middleware.DjangoAPIGuard", @@ -250,28 +254,28 @@ MIDDLEWARE = [ # ... other middleware ] -# Define your security configuration -GUARD_SECURITY_CONFIG = { - "ipinfo_token": "your_ipinfo_token_here", # Optional: IPInfo token required for IP geolocation - "ipinfo_db_path": "custom/ipinfo.db", # Optional custom database path - "whitelist": ["192.168.1.1", "2001:db8::1"], - "blacklist": ["10.0.0.1", "2001:db8::2"], - "blocked_countries": ["AR", "IT"], - "blocked_user_agents": ["curl", "wget"], - "auto_ban_threshold": 5, - "auto_ban_duration": 86400, - "custom_log_file": "security.log", - "rate_limit": 100, - "enforce_https": True, - "enable_cors": True, - "cors_allow_origins": ["*"], - "cors_allow_methods": ["GET", "POST"], - "cors_allow_headers": ["*"], - "cors_allow_credentials": True, - "cors_expose_headers": ["X-Custom-Header"], - "cors_max_age": 600, - "block_cloud_providers": {"AWS", "GCP", "Azure"}, -} +# Define your security configuration (required: a SecurityConfig instance) +GUARD_SECURITY_CONFIG = SecurityConfig( + ipinfo_token="your_ipinfo_token_here", # Optional: IPInfo token required for IP geolocation + ipinfo_db_path="custom/ipinfo.db", # Optional custom database path + whitelist=["192.168.1.1", "2001:db8::1"], + blacklist=["10.0.0.1", "2001:db8::2"], + blocked_countries=["AR", "IT"], + blocked_user_agents=["curl", "wget"], + auto_ban_threshold=5, + auto_ban_duration=86400, + custom_log_file="security.log", + rate_limit=100, + enforce_https=True, + enable_cors=True, + cors_allow_origins=["*"], + cors_allow_methods=["GET", "POST"], + cors_allow_headers=["*"], + cors_allow_credentials=True, + cors_expose_headers=["X-Custom-Header"], + cors_max_age=600, + block_cloud_providers={"AWS", "GCP", "Azure"}, +) ``` ### views.py @@ -288,10 +292,10 @@ def read_root(request): You can control access based on IP addresses using the `whitelist` and `blacklist` options in the `SecurityConfig`. ```python -GUARD_SECURITY_CONFIG = { - "whitelist": ["192.168.1.1", "2001:db8::1"], - "blacklist": ["10.0.0.1", "2001:db8::2"], -} +GUARD_SECURITY_CONFIG = SecurityConfig( + whitelist=["192.168.1.1", "2001:db8::1"], + blacklist=["10.0.0.1", "2001:db8::2"], +) ``` ## User Agent Filtering @@ -299,9 +303,9 @@ GUARD_SECURITY_CONFIG = { Block requests from specific user agents by adding patterns to the `blocked_user_agents` list. ```python -GUARD_SECURITY_CONFIG = { - "blocked_user_agents": ["curl", "wget"], -} +GUARD_SECURITY_CONFIG = SecurityConfig( + blocked_user_agents=["curl", "wget"], +) ``` ## Rate Limiting @@ -309,9 +313,9 @@ GUARD_SECURITY_CONFIG = { Limit the number of requests from a single IP using the `rate_limit` option. ```python -GUARD_SECURITY_CONFIG = { - "rate_limit": 100, # Maximum 100 requests per minute -} +GUARD_SECURITY_CONFIG = SecurityConfig( + rate_limit=100, # Maximum 100 requests per minute +) ``` ## Automatic IP Banning @@ -319,10 +323,10 @@ GUARD_SECURITY_CONFIG = { Automatically ban IPs after a certain number of suspicious requests using the `auto_ban_threshold` and `auto_ban_duration` options. ```python -GUARD_SECURITY_CONFIG = { - "auto_ban_threshold": 5, # Ban IP after 5 suspicious requests - "auto_ban_duration": 86400, # Ban duration in seconds (1 day) -} +GUARD_SECURITY_CONFIG = SecurityConfig( + auto_ban_threshold=5, # Ban IP after 5 suspicious requests + auto_ban_duration=86400, # Ban duration in seconds (1 day) +) ``` ## Penetration Attempt Detection @@ -330,17 +334,17 @@ GUARD_SECURITY_CONFIG = { Enable penetration attempt detection using the `enable_penetration_detection` option. ```python -GUARD_SECURITY_CONFIG = { - "enable_penetration_detection": True, # True by default -} +GUARD_SECURITY_CONFIG = SecurityConfig( + enable_penetration_detection=True, # True by default +) ``` Optional: Enable `passive mode` to log suspicious activity without blocking requests. ```python -GUARD_SECURITY_CONFIG = { - "passive_mode": True, # False by default -} +GUARD_SECURITY_CONFIG = SecurityConfig( + passive_mode=True, # False by default +) ``` ## Custom Penetration Detection @@ -366,10 +370,10 @@ def submit_data(request): Log security events with console output (always enabled) and optional file logging: ```python -GUARD_SECURITY_CONFIG = { - "custom_log_file": "security.log", # Optional: adds file logging +GUARD_SECURITY_CONFIG = SecurityConfig( + custom_log_file="security.log", # Optional: adds file logging # "custom_log_file": None, # Default: console output only -} +) ``` **Note:** Console output is always enabled for visibility. File logging is only activated when `custom_log_file` is provided. @@ -379,8 +383,8 @@ GUARD_SECURITY_CONFIG = { Configure comprehensive security headers following OWASP best practices: ```python -GUARD_SECURITY_CONFIG = { - "security_headers": { +GUARD_SECURITY_CONFIG = SecurityConfig( + security_headers={ "enabled": True, "hsts": { "max_age": 31536000, # 1 year @@ -406,7 +410,7 @@ GUARD_SECURITY_CONFIG = { "X-Custom-Header": "CustomValue", }, }, -} +) ``` Key security headers supported: @@ -427,15 +431,15 @@ Key security headers supported: Configure CORS settings for your Django application using the `enable_cors` and related options. ```python -GUARD_SECURITY_CONFIG = { - "enable_cors": True, - "cors_allow_origins": ["*"], - "cors_allow_methods": ["GET", "POST"], - "cors_allow_headers": ["*"], - "cors_allow_credentials": True, - "cors_expose_headers": ["X-Custom-Header"], - "cors_max_age": 600, -} +GUARD_SECURITY_CONFIG = SecurityConfig( + enable_cors=True, + cors_allow_origins=["*"], + cors_allow_methods=["GET", "POST"], + cors_allow_headers=["*"], + cors_allow_credentials=True, + cors_expose_headers=["X-Custom-Header"], + cors_max_age=600, +) ``` ## Cloud Provider IP Blocking @@ -443,9 +447,9 @@ GUARD_SECURITY_CONFIG = { Block requests from cloud provider IPs (AWS, GCP, Azure) using the `block_cloud_providers` option. ```python -GUARD_SECURITY_CONFIG = { - "block_cloud_providers": {"AWS", "GCP", "Azure"}, -} +GUARD_SECURITY_CONFIG = SecurityConfig( + block_cloud_providers={"AWS", "GCP", "Azure"}, +) ``` ## IP Geolocation and Country Blocking @@ -478,10 +482,8 @@ from django.http import JsonResponse # Create decorator instance from django.conf import settings -from djangoapi_guard.models import SecurityConfig as SecurityConfigModel -config = SecurityConfigModel(**settings.GUARD_SECURITY_CONFIG) -guard_deco = SecurityDecorator(config) +guard_deco = SecurityDecorator(settings.GUARD_SECURITY_CONFIG) # Apply decorators to specific views def public_endpoint(request): @@ -582,11 +584,11 @@ ___ Configure trusted proxies to securely handle X-Forwarded-For headers: ```python -GUARD_SECURITY_CONFIG = { - "trusted_proxies": ["10.0.0.1", "192.168.1.0/24"], # List of trusted proxy IPs or CIDR ranges - "trusted_proxy_depth": 1, # How many proxies to expect in chain - "trust_x_forwarded_proto": True, # Whether to trust X-Forwarded-Proto for HTTPS detection (default: True) -} +GUARD_SECURITY_CONFIG = SecurityConfig( + trusted_proxies=["10.0.0.1", "192.168.1.0/24"], # List of trusted proxy IPs or CIDR ranges + trusted_proxy_depth=1, # How many proxies to expect in chain + trust_x_forwarded_proto=True, # Whether to trust X-Forwarded-Proto for HTTPS detection (default: True) +) ``` When `trusted_proxies` is configured, DjangoAPI Guard will: @@ -610,11 +612,11 @@ To use the geolocation feature with this handler: ```python from djangoapi_guard.protocols.geo_ip_protocol import GeoIPHandler -GUARD_SECURITY_CONFIG = { - "geo_ip_handler": GeoIPHandler, - "blocked_countries": ["AR", "IT"], # Block specific countries using ISO 3166-1 alpha-2 codes - "whitelist_countries": ["US", "CA"], # Optional: Only allow specific countries -} +GUARD_SECURITY_CONFIG = SecurityConfig( + geo_ip_handler=GeoIPHandler, + blocked_countries=["AR", "IT"], # Block specific countries using ISO 3166-1 alpha-2 codes + whitelist_countries=["US", "CA"], # Optional: Only allow specific countries +) ``` The database is automatically downloaded and cached locally when the middleware starts, if required, and it's updated daily to ensure accuracy. @@ -647,11 +649,11 @@ class GeoIPHandler: ... -GUARD_SECURITY_CONFIG = { - "geo_ip_handler": GeoIPHandler, - "blocked_countries": ["AR", "IT"], # Block specific countries using ISO 3166-1 alpha-2 codes - "whitelist_countries": ["US", "CA"], # Optional: Only allow specific countries -} +GUARD_SECURITY_CONFIG = SecurityConfig( + geo_ip_handler=GeoIPHandler, + blocked_countries=["AR", "IT"], # Block specific countries using ISO 3166-1 alpha-2 codes + whitelist_countries=["US", "CA"], # Optional: Only allow specific countries +) ``` ### Custom Request Check @@ -667,9 +669,9 @@ def custom_check(req) -> Optional[HttpResponse]: return HttpResponse("Missing custom header", status=400) return None -GUARD_SECURITY_CONFIG = { - "custom_request_check": custom_check, -} +GUARD_SECURITY_CONFIG = SecurityConfig( + custom_request_check=custom_check, +) ``` ### Custom Response Modifier @@ -695,9 +697,9 @@ def custom_modifier(response: HttpResponse) -> HttpResponse: return response -GUARD_SECURITY_CONFIG = { - "custom_response_modifier": custom_modifier, -} +GUARD_SECURITY_CONFIG = SecurityConfig( + custom_response_modifier=custom_modifier, +) ``` The example above shows how to: @@ -712,11 +714,11 @@ ___ Enable distributed state management across multiple instances: ```python -GUARD_SECURITY_CONFIG = { - "enable_redis": True, - "redis_url": "redis://prod-redis:6379/1", - "redis_prefix": "myapp:security:", -} +GUARD_SECURITY_CONFIG = SecurityConfig( + enable_redis=True, + redis_url="redis://prod-redis:6379/1", + redis_prefix="myapp:security:", +) ``` The Redis integration provides: @@ -733,10 +735,10 @@ ___ Block all incoming traffic except from explicitly whitelisted IPs. Useful during active attacks or maintenance windows: ```python -GUARD_SECURITY_CONFIG = { - "emergency_mode": True, - "emergency_whitelist": ["10.0.0.1", "192.168.1.0/24"], -} +GUARD_SECURITY_CONFIG = SecurityConfig( + emergency_mode=True, + emergency_whitelist=["10.0.0.1", "192.168.1.0/24"], +) ``` ___ diff --git a/examples/advanced/advanced_app/routes/behavioral.py b/examples/advanced/advanced_app/routes/behavioral.py index 32895b8..0136b1f 100644 --- a/examples/advanced/advanced_app/routes/behavioral.py +++ b/examples/advanced/advanced_app/routes/behavioral.py @@ -20,7 +20,7 @@ def monitor_usage_patterns(request: HttpRequest) -> JsonResponse: @require_GET -@guard.return_monitor(pattern="404", max_occurrences=3, window=60, action="ban") +@guard.return_monitor(pattern="status:404", max_occurrences=3, window=60, action="ban") def monitor_return_patterns(request: HttpRequest, status_code: int) -> JsonResponse: if status_code == 404: return JsonResponse({"detail": "Not found"}, status=404) @@ -52,7 +52,7 @@ def detect_suspicious_frequency(request: HttpRequest) -> JsonResponse: ), BehaviorRule( rule_type="return_pattern", - pattern="404", + pattern="status:404", threshold=5, window=60, action="ban", diff --git a/examples/advanced/advanced_app/security.py b/examples/advanced/advanced_app/security.py index 7e3f749..4ff98e3 100644 --- a/examples/advanced/advanced_app/security.py +++ b/examples/advanced/advanced_app/security.py @@ -1,24 +1,29 @@ import logging import os -from django.http import HttpRequest, HttpResponse, JsonResponse +from django.http import JsonResponse +from guard_core.protocols.response_protocol import GuardResponse +from guard_core.sync.protocols.request_protocol import SyncGuardRequest as GuardRequest from djangoapi_guard import SecurityConfig, SecurityDecorator +from djangoapi_guard.adapters import DjangoGuardResponse logger = logging.getLogger(__name__) -def custom_request_check(request: HttpRequest) -> HttpResponse | None: - if request.GET.get("debug") == "true": - return JsonResponse({"detail": "Debug mode not allowed"}, status=403) +def custom_request_check(request: GuardRequest) -> GuardResponse | None: + if request.query_params.get("debug") == "true": + return DjangoGuardResponse( + JsonResponse({"detail": "Debug mode not allowed"}, status=403) + ) return None -def custom_response_modifier(response: HttpResponse) -> HttpResponse: - response["X-Content-Type-Options"] = "nosniff" - response["X-Frame-Options"] = "DENY" - response["X-XSS-Protection"] = "1; mode=block" - response["Referrer-Policy"] = "strict-origin-when-cross-origin" +def custom_response_modifier(response: GuardResponse) -> GuardResponse: + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return response