sqry_core/query/security/config.rs
1//! Security configuration for CD queries
2//!
3//! Provides configuration for query execution limits including:
4//! - Timeout (30s NON-NEGOTIABLE ceiling per spec AC-8)
5//! - Result cap (10k default)
6//! - Memory limit (512MB default)
7//! - Cost estimation limits
8
9use std::time::Duration;
10
11/// Security controls for CD queries
12///
13/// **TIMEOUT ALIGNMENT** (per Codex iter3 review):
14/// Default timeout is 30s to match spec AC-8 (NON-NEGOTIABLE).
15/// This is the hard ceiling - CLI can allow users to specify LOWER values only.
16///
17/// **PRIVATE FIELDS** (per Codex iter6 review):
18/// All fields are private to prevent bypassing security limits.
19/// Use constructors (`default()`, `interactive()`, `with_timeout()`) and
20/// getters (`timeout()`, `result_cap()`, etc.) to access values.
21/// The 30s ceiling cannot be bypassed by direct field assignment.
22#[derive(Debug, Clone)]
23pub struct QuerySecurityConfig {
24 /// Maximum query execution time (PRIVATE - use getter)
25 timeout: Duration,
26
27 /// Maximum results to return (PRIVATE - use getter)
28 result_cap: usize,
29
30 /// Maximum memory for query processing in bytes (PRIVATE - use getter)
31 memory_limit: usize,
32
33 /// Enable query logging (PRIVATE - use getter)
34 audit_enabled: bool,
35
36 /// Pre-execution cost limit (PRIVATE - use getter)
37 cost_limit: usize,
38}
39
40impl QuerySecurityConfig {
41 /// The hard ceiling timeout (30s) - NON-NEGOTIABLE per spec AC-8
42 pub const HARD_CEILING_TIMEOUT: Duration = Duration::from_secs(30);
43
44 /// Default result cap (10,000)
45 pub const DEFAULT_RESULT_CAP: usize = 10_000;
46
47 /// Default memory limit (512 MB)
48 pub const DEFAULT_MEMORY_LIMIT: usize = 512 * 1024 * 1024;
49
50 /// Default cost limit (1M estimated operations)
51 pub const DEFAULT_COST_LIMIT: usize = 1_000_000;
52
53 /// Create a config for interactive use with shorter timeout
54 ///
55 /// Users can request shorter timeouts for faster feedback.
56 /// Uses 10s timeout instead of 30s for responsiveness.
57 #[must_use]
58 pub fn interactive() -> Self {
59 Self {
60 timeout: Duration::from_secs(10), // Shorter for responsiveness
61 ..Default::default()
62 }
63 }
64
65 /// Create a config with a custom timeout (capped at 30s)
66 ///
67 /// **IMPORTANT**: The timeout is capped at 30s regardless of input.
68 /// This enforces the NON-NEGOTIABLE security requirement from spec AC-8.
69 ///
70 /// **Builder pattern**: Can be chained from other constructors:
71 /// `QuerySecurityConfig::default().with_timeout(Duration::from_secs(5))`
72 #[must_use]
73 pub fn with_timeout(self, timeout: Duration) -> Self {
74 Self {
75 // Cap at 30s - the NON-NEGOTIABLE hard ceiling
76 timeout: timeout.min(Self::HARD_CEILING_TIMEOUT),
77 ..self
78 }
79 }
80
81 /// Create a config with a custom result cap
82 ///
83 /// **Builder pattern**: Can be chained from other constructors:
84 /// `QuerySecurityConfig::default().with_result_cap(100)`
85 #[must_use]
86 pub fn with_result_cap(self, cap: usize) -> Self {
87 Self {
88 result_cap: cap,
89 ..self
90 }
91 }
92
93 /// Create a config with a custom memory limit (bytes)
94 ///
95 /// **Builder pattern**: Can be chained from other constructors:
96 /// `QuerySecurityConfig::default().with_memory_limit(1024 * 1024)` // 1MB
97 #[must_use]
98 pub fn with_memory_limit(self, limit: usize) -> Self {
99 Self {
100 memory_limit: limit,
101 ..self
102 }
103 }
104
105 /// Create a config with a custom cost limit
106 ///
107 /// **Builder pattern**: Can be chained from other constructors:
108 /// `QuerySecurityConfig::default().with_cost_limit(500_000)`
109 #[must_use]
110 pub fn with_cost_limit(self, limit: usize) -> Self {
111 Self {
112 cost_limit: limit,
113 ..self
114 }
115 }
116
117 /// Create a config with audit logging disabled
118 ///
119 /// **Builder pattern**: Can be chained from other constructors:
120 /// `QuerySecurityConfig::default().without_audit()`
121 #[must_use]
122 pub fn without_audit(self) -> Self {
123 Self {
124 audit_enabled: false,
125 ..self
126 }
127 }
128
129 // ======== GETTERS (per Codex iter6 review) ========
130 // Fields are private; use these getters to access values.
131
132 /// Get the timeout duration
133 #[must_use]
134 pub fn timeout(&self) -> Duration {
135 self.timeout
136 }
137
138 /// Get the result cap
139 #[must_use]
140 pub fn result_cap(&self) -> usize {
141 self.result_cap
142 }
143
144 /// Get the memory limit (bytes)
145 #[must_use]
146 pub fn memory_limit(&self) -> usize {
147 self.memory_limit
148 }
149
150 /// Check if audit logging is enabled
151 #[must_use]
152 pub fn audit_enabled(&self) -> bool {
153 self.audit_enabled
154 }
155
156 /// Get the cost limit
157 #[must_use]
158 pub fn cost_limit(&self) -> usize {
159 self.cost_limit
160 }
161}
162
163impl Default for QuerySecurityConfig {
164 fn default() -> Self {
165 Self {
166 // **30s DEFAULT** (per Codex iter3 review, aligns with spec AC-8)
167 // This is the NON-NEGOTIABLE hard ceiling
168 timeout: Self::HARD_CEILING_TIMEOUT,
169 result_cap: Self::DEFAULT_RESULT_CAP,
170 memory_limit: Self::DEFAULT_MEMORY_LIMIT,
171 audit_enabled: true,
172 cost_limit: Self::DEFAULT_COST_LIMIT,
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_default_config() {
183 let config = QuerySecurityConfig::default();
184 assert_eq!(config.timeout(), Duration::from_secs(30));
185 assert_eq!(config.result_cap(), 10_000);
186 assert_eq!(config.memory_limit(), 512 * 1024 * 1024);
187 assert!(config.audit_enabled());
188 assert_eq!(config.cost_limit(), 1_000_000);
189 }
190
191 #[test]
192 fn test_interactive_config() {
193 let config = QuerySecurityConfig::interactive();
194 assert_eq!(config.timeout(), Duration::from_secs(10));
195 // Other values should be default
196 assert_eq!(config.result_cap(), 10_000);
197 }
198
199 #[test]
200 fn test_timeout_capped_at_30s() {
201 // Requesting 60s should be capped to 30s
202 let config = QuerySecurityConfig::default().with_timeout(Duration::from_secs(60));
203 assert_eq!(config.timeout(), Duration::from_secs(30));
204 }
205
206 #[test]
207 fn test_timeout_under_ceiling() {
208 // Requesting 5s should be allowed
209 let config = QuerySecurityConfig::default().with_timeout(Duration::from_secs(5));
210 assert_eq!(config.timeout(), Duration::from_secs(5));
211 }
212
213 #[test]
214 fn test_builder_chain() {
215 let config = QuerySecurityConfig::default()
216 .with_timeout(Duration::from_secs(15))
217 .with_result_cap(500)
218 .with_memory_limit(64 * 1024 * 1024)
219 .without_audit();
220
221 assert_eq!(config.timeout(), Duration::from_secs(15));
222 assert_eq!(config.result_cap(), 500);
223 assert_eq!(config.memory_limit(), 64 * 1024 * 1024);
224 assert!(!config.audit_enabled());
225 }
226
227 #[test]
228 fn test_hard_ceiling_constant() {
229 assert_eq!(
230 QuerySecurityConfig::HARD_CEILING_TIMEOUT,
231 Duration::from_secs(30)
232 );
233 }
234}