Coverage for src/lexigram/graphql/security/extensions.py: 50%
24 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""GraphQL security schema extensions.
3Strawberry ``SchemaExtension`` implementations for security concerns.
4This approach integrates cleanly with Strawberry's execution pipeline so
5security checks run inside the schema lifecycle rather than in the executor.
6"""
8from __future__ import annotations
10from typing import TYPE_CHECKING
12from strawberry.extensions import SchemaExtension
14from lexigram.logging import get_logger
16if TYPE_CHECKING:
17 from collections.abc import AsyncGenerator
19 from lexigram.graphql.security.rate_limit import RateLimiter
21logger = get_logger(__name__)
24class RateLimitExtension(SchemaExtension):
25 """Enforce rate limiting on every GraphQL operation.
27 This extension delegates the actual limit check to the injected
28 :class:`~lexigram.graphql.security.rate_limit.RateLimiter` (resolved
29 from the DI container by :class:`~lexigram.graphql.providers.GraphQLProvider`)
30 and raises :class:`~lexigram.graphql.exceptions.RateLimitError` when the
31 limit is exceeded.
33 Register it on the schema builder instead of wiring it into the executor::
35 builder.add_extension(
36 RateLimitExtension(rate_limiter=rate_limiter, max_requests=60)
37 )
39 Args:
40 rate_limiter: Rate limiter implementation; when ``None`` the extension
41 is a no-op (useful for local/test environments).
42 max_requests: Request allowance per window.
43 window_seconds: Window length in seconds.
44 """
46 def __init__(
47 self,
48 *,
49 rate_limiter: RateLimiter | None = None,
50 max_requests: int = 60,
51 window_seconds: int = 60,
52 ) -> None:
53 self._rate_limiter = rate_limiter
54 self._max_requests = max_requests
55 self._window_seconds = window_seconds
57 async def on_operation(self) -> AsyncGenerator[None, None]:
58 """Check the rate limit before the GraphQL operation executes."""
59 if self._rate_limiter is not None:
60 context = self.execution_context.context
62 from lexigram.graphql.exceptions import RateLimitError
64 try:
65 allowed = await self._rate_limiter.is_allowed(
66 context,
67 max_requests=self._max_requests,
68 window_seconds=self._window_seconds,
69 )
70 if not allowed:
71 raise RateLimitError("Rate limit exceeded")
72 except RateLimitError:
73 raise
74 except (OSError, RuntimeError, LookupError) as e:
75 # Infrastructure errors (Redis down etc.) must not block requests.
76 logger.warning("rate_limit_check_error", error=str(e))
78 yield
81__all__ = ["RateLimitExtension"]