Coverage for src/lexigram/graphql/security/alias.py: 100%
44 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"""Query alias limiting.
3This module provides alias limiting for GraphQL queries
4to prevent abuse through excessive field aliasing.
5"""
7from __future__ import annotations
9from typing import TYPE_CHECKING
11from strawberry.extensions import SchemaExtension
13from lexigram.graphql.exceptions import GraphQLError
14from lexigram.logging import get_logger
16if TYPE_CHECKING:
17 from collections.abc import Iterator
19 from graphql.language.ast import DocumentNode, SelectionSetNode
22logger = get_logger(__name__)
25class AliasLimitValidator:
26 """Validate query alias count against a limit.
28 Analyzes GraphQL queries to count aliases and validates
29 against a configured maximum.
31 Example:
32 ```python
33 validator = AliasLimitValidator(max_aliases=10)
35 count = validator.count_aliases(document)
36 validator.validate(document) # Raises if too many
37 ```
38 """
40 def __init__(self, max_aliases: int = 10) -> None:
41 """Initialize the validator.
43 Args:
44 max_aliases: Maximum allowed aliases.
45 """
46 self._max_aliases = max_aliases
48 @property
49 def max_aliases(self) -> int:
50 """Get maximum alias limit."""
51 return self._max_aliases
53 def count_aliases(self, document: DocumentNode) -> int:
54 """Count the number of aliases in a query.
56 Args:
57 document: Parsed GraphQL document.
59 Returns:
60 Number of aliases.
61 """
62 count = 0
64 for definition in document.definitions:
65 if hasattr(definition, "selection_set"):
66 count += self._count_in_selection_set(definition.selection_set)
68 return count
70 def _count_in_selection_set(
71 self,
72 selection_set: SelectionSetNode | None,
73 ) -> int:
74 """Count aliases in a selection set.
76 Args:
77 selection_set: Selection set to analyze.
79 Returns:
80 Number of aliases.
81 """
82 if selection_set is None:
83 return 0
85 count = 0
87 for selection in selection_set.selections:
88 # Check for alias
89 if hasattr(selection, "alias") and selection.alias:
90 count += 1
92 # Recurse into nested selections
93 if hasattr(selection, "selection_set"):
94 count += self._count_in_selection_set(selection.selection_set)
96 return count
98 def validate(self, document: DocumentNode) -> None:
99 """Validate alias count.
101 Args:
102 document: Parsed GraphQL document.
104 Raises:
105 SecurityError: If query has too many aliases.
106 """
107 count = self.count_aliases(document)
109 if count > self._max_aliases:
110 error = GraphQLError(
111 f"Query has {count} aliases, exceeding maximum of {self._max_aliases}",
112 )
113 error.safe = True
114 raise error
116 logger.debug("Query has %d aliases (limit: %d)", count, self._max_aliases)
119class AliasLimitExtension(SchemaExtension):
120 """Strawberry extension for query alias limiting.
122 Add this extension to your schema to automatically
123 validate alias count before execution.
125 Example:
126 ```python
127 from lexigram.graphql.security import AliasLimitExtension
129 schema = strawberry.Schema(
130 query=Query,
131 extensions=[AliasLimitExtension(max_aliases=10)],
132 )
133 ```
134 """
136 def __init__(self, max_aliases: int = 10) -> None:
137 """Initialize the extension.
139 Args:
140 max_aliases: Maximum allowed aliases.
141 """
142 self._validator = AliasLimitValidator(max_aliases=max_aliases)
144 def on_validate(self) -> Iterator[None]:
145 """Hook called during operation validation."""
146 execution_context = self.execution_context
148 # Validate alias count
149 if execution_context.graphql_document:
150 self._validator.validate(execution_context.graphql_document)
152 yield
155__all__ = ["AliasLimitExtension", "AliasLimitValidator"]