Skip to main content

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}