Coverage for src/lexigram/graphql/security/query_whitelist.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 whitelist for production GraphQL endpoints.
3This module provides query whitelisting to restrict GraphQL
4operations to a predefined set for security.
5"""
7from __future__ import annotations
9from dataclasses import dataclass
11from lexigram import hashing # type: ignore[attr-defined]
14@dataclass
15class WhitelistEntry:
16 """A whitelisted query entry.
18 Attributes:
19 hash: SHA256 hash of the query.
20 query: The actual query string.
21 operation_name: Optional operation name.
22 description: Human-readable description.
23 """
25 hash: str
26 query: str
27 operation_name: str | None = None
28 description: str | None = None
31class QueryWhitelist:
32 """Query whitelist manager.
34 Example:
35 whitelist = QueryWhitelist()
36 whitelist.add("{ user { id name } }", "Get user")
38 # Validate incoming queries
39 if not whitelist.is_allowed(query):
40 raise PermissionError("Query not whitelisted")
41 """
43 def __init__(self, enabled: bool = False):
44 """Initialize the whitelist.
46 Args:
47 enabled: Whether whitelist enforcement is enabled.
48 """
49 self._enabled = enabled
50 self._entries: dict[str, WhitelistEntry] = {}
52 @property
53 def enabled(self) -> bool:
54 """Check if whitelist is enabled."""
55 return self._enabled
57 def add(
58 self,
59 query: str,
60 description: str | None = None,
61 operation_name: str | None = None,
62 ) -> WhitelistEntry:
63 """Add a query to the whitelist.
65 Args:
66 query: GraphQL query string.
67 description: Optional description.
68 operation_name: Optional operation name.
70 Returns:
71 The created whitelist entry.
72 """
73 hash_value = self._compute_hash(query)
75 entry = WhitelistEntry(
76 hash=hash_value,
77 query=query,
78 operation_name=operation_name,
79 description=description,
80 )
82 self._entries[hash_value] = entry
83 return entry
85 def remove(self, query: str) -> bool:
86 """Remove a query from the whitelist.
88 Args:
89 query: GraphQL query string.
91 Returns:
92 True if removed, False if not found.
93 """
94 hash_value = self._compute_hash(query)
96 if hash_value in self._entries:
97 del self._entries[hash_value]
98 return True
100 return False
102 def is_allowed(self, query: str) -> bool:
103 """Check if a query is allowed.
105 Args:
106 query: GraphQL query string.
108 Returns:
109 True if allowed, False otherwise.
110 """
111 if not self._enabled:
112 return True
114 hash_value = self._compute_hash(query)
115 return hash_value in self._entries
117 def get_entry(self, query: str) -> WhitelistEntry | None:
118 """Get the whitelist entry for a query.
120 Args:
121 query: GraphQL query string.
123 Returns:
124 Whitelist entry if found, None otherwise.
125 """
126 hash_value = self._compute_hash(query)
127 return self._entries.get(hash_value)
129 def get_all_hashes(self) -> set[str]:
130 """Get all whitelisted query hashes.
132 Returns:
133 Set of query hashes.
134 """
135 return set(self._entries.keys())
137 def clear(self) -> None:
138 """Clear all whitelist entries."""
139 self._entries.clear()
141 def _compute_hash(self, query: str) -> str:
142 """Compute SHA256 hash of a query.
144 Args:
145 query: GraphQL query string.
147 Returns:
148 Hex-encoded hash.
149 """
150 # Normalize query (remove extra whitespace)
151 normalized = " ".join(query.split())
152 return str(hashing.hash_hex(normalized))
155# Default whitelist instance
156default_whitelist = QueryWhitelist()
159__all__ = [
160 "QueryWhitelist",
161 "WhitelistEntry",
162 "default_whitelist",
163]