fraiseql_server/encryption/
query_builder.rs1use std::collections::HashSet;
45
46use crate::secrets_manager::SecretsError;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum QueryType {
51 Insert,
53 Select,
55 Update,
57 Delete,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ClauseType {
64 Where,
66 OrderBy,
68 Join,
70 GroupBy,
72}
73
74pub struct QueryBuilderIntegration {
79 encrypted_fields: HashSet<String>,
81}
82
83impl QueryBuilderIntegration {
84 pub fn new(encrypted_fields: Vec<String>) -> Self {
86 Self {
87 encrypted_fields: encrypted_fields.into_iter().collect(),
88 }
89 }
90
91 pub fn validate_where_clause(&self, fields: &[&str]) -> Result<(), SecretsError> {
100 for field in fields {
101 if self.encrypted_fields.contains(&field.to_string()) {
102 return Err(SecretsError::ValidationError(format!(
103 "Cannot use encrypted field '{}' in WHERE clause. \
104 Encrypted fields are not queryable due to non-deterministic encryption. \
105 Consider using: (1) deterministic hash of plaintext, \
106 (2) separate plaintext index, or (3) application-level filtering.",
107 field
108 )));
109 }
110 }
111
112 Ok(())
113 }
114
115 pub fn validate_order_by_clause(&self, fields: &[&str]) -> Result<(), SecretsError> {
120 for field in fields {
121 if self.encrypted_fields.contains(&field.to_string()) {
122 return Err(SecretsError::ValidationError(format!(
123 "Cannot use encrypted field '{}' in ORDER BY clause. \
124 Encrypted ciphertext does not preserve plaintext sort order. \
125 Consider ordering by unencrypted fields instead.",
126 field
127 )));
128 }
129 }
130
131 Ok(())
132 }
133
134 pub fn validate_join_condition(&self, fields: &[&str]) -> Result<(), SecretsError> {
139 for field in fields {
140 if self.encrypted_fields.contains(&field.to_string()) {
141 return Err(SecretsError::ValidationError(format!(
142 "Cannot use encrypted field '{}' in JOIN condition. \
143 Encrypted fields are not comparable. \
144 JOIN on unencrypted fields instead, or denormalize data.",
145 field
146 )));
147 }
148 }
149
150 Ok(())
151 }
152
153 pub fn validate_group_by_clause(&self, fields: &[&str]) -> Result<(), SecretsError> {
155 for field in fields {
156 if self.encrypted_fields.contains(&field.to_string()) {
157 return Err(SecretsError::ValidationError(format!(
158 "Cannot use encrypted field '{}' in GROUP BY clause. \
159 Encrypted ciphertext values are not stable for grouping.",
160 field
161 )));
162 }
163 }
164
165 Ok(())
166 }
167
168 pub fn validate_is_null_on_encrypted(&self, _field: &str) -> Result<(), SecretsError> {
173 Ok(())
176 }
177
178 pub fn validate_clause(
180 &self,
181 clause_type: ClauseType,
182 fields: &[&str],
183 ) -> Result<(), SecretsError> {
184 match clause_type {
185 ClauseType::Where => self.validate_where_clause(fields),
186 ClauseType::OrderBy => self.validate_order_by_clause(fields),
187 ClauseType::Join => self.validate_join_condition(fields),
188 ClauseType::GroupBy => self.validate_group_by_clause(fields),
189 }
190 }
191
192 pub fn encrypted_fields(&self) -> Vec<String> {
194 self.encrypted_fields.iter().cloned().collect()
195 }
196
197 pub fn is_encrypted(&self, field: &str) -> bool {
199 self.encrypted_fields.contains(&field.to_string())
200 }
201
202 pub fn get_encrypted_fields_in_list(&self, fields: &[&str]) -> Vec<String> {
204 fields.iter().filter(|f| self.is_encrypted(f)).map(|f| f.to_string()).collect()
205 }
206
207 pub fn validate_query(
211 &self,
212 query_type: QueryType,
213 where_fields: &[&str],
214 order_by_fields: &[&str],
215 join_fields: &[&str],
216 ) -> Result<(), SecretsError> {
217 match query_type {
219 QueryType::Insert | QueryType::Update => {
220 Ok(())
223 },
224 QueryType::Select => {
225 self.validate_where_clause(where_fields)?;
227 self.validate_order_by_clause(order_by_fields)?;
228 self.validate_join_condition(join_fields)?;
229 Ok(())
230 },
231 QueryType::Delete => {
232 Ok(())
234 },
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_validate_where_clause_unencrypted_field() {
245 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
246 let result = qbi.validate_where_clause(&["name"]);
247 assert!(result.is_ok());
248 }
249
250 #[test]
251 fn test_validate_where_clause_encrypted_field_rejects() {
252 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
253 let result = qbi.validate_where_clause(&["email"]);
254 assert!(result.is_err());
255 assert!(matches!(result, Err(SecretsError::ValidationError(_))));
256 }
257
258 #[test]
259 fn test_validate_order_by_unencrypted_field() {
260 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
261 let result = qbi.validate_order_by_clause(&["name"]);
262 assert!(result.is_ok());
263 }
264
265 #[test]
266 fn test_validate_order_by_encrypted_field_rejects() {
267 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
268 let result = qbi.validate_order_by_clause(&["email"]);
269 assert!(result.is_err());
270 }
271
272 #[test]
273 fn test_validate_join_unencrypted_field() {
274 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
275 let result = qbi.validate_join_condition(&["user_id"]);
276 assert!(result.is_ok());
277 }
278
279 #[test]
280 fn test_validate_join_encrypted_field_rejects() {
281 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
282 let result = qbi.validate_join_condition(&["email"]);
283 assert!(result.is_err());
284 }
285
286 #[test]
287 fn test_validate_group_by_unencrypted_field() {
288 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
289 let result = qbi.validate_group_by_clause(&["status"]);
290 assert!(result.is_ok());
291 }
292
293 #[test]
294 fn test_validate_group_by_encrypted_field_rejects() {
295 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
296 let result = qbi.validate_group_by_clause(&["email"]);
297 assert!(result.is_err());
298 }
299
300 #[test]
301 fn test_validate_is_null_on_encrypted_field() {
302 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
303 let result = qbi.validate_is_null_on_encrypted("email");
304 assert!(result.is_ok());
305 }
306
307 #[test]
308 fn test_validate_mixed_encrypted_unencrypted_fields() {
309 let qbi = QueryBuilderIntegration::new(vec!["email".to_string(), "phone".to_string()]);
310 let result = qbi.validate_where_clause(&["name", "email"]);
312 assert!(result.is_err());
313 }
314
315 #[test]
316 fn test_validate_clause_with_type() {
317 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
318 let result = qbi.validate_clause(ClauseType::Where, &["email"]);
319 assert!(result.is_err());
320
321 let result = qbi.validate_clause(ClauseType::OrderBy, &["email"]);
322 assert!(result.is_err());
323
324 let result = qbi.validate_clause(ClauseType::Join, &["email"]);
325 assert!(result.is_err());
326 }
327
328 #[test]
329 fn test_encrypted_fields_list() {
330 let qbi = QueryBuilderIntegration::new(vec!["email".to_string(), "phone".to_string()]);
331 let fields = qbi.encrypted_fields();
332 assert_eq!(fields.len(), 2);
333 assert!(fields.contains(&"email".to_string()));
334 assert!(fields.contains(&"phone".to_string()));
335 }
336
337 #[test]
338 fn test_is_encrypted() {
339 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
340 assert!(qbi.is_encrypted("email"));
341 assert!(!qbi.is_encrypted("name"));
342 }
343
344 #[test]
345 fn test_get_encrypted_fields_in_list() {
346 let qbi = QueryBuilderIntegration::new(vec!["email".to_string(), "phone".to_string()]);
347 let result = qbi.get_encrypted_fields_in_list(&["name", "email", "phone"]);
348 assert_eq!(result.len(), 2);
349 assert!(result.contains(&"email".to_string()));
350 assert!(result.contains(&"phone".to_string()));
351 }
352
353 #[test]
354 fn test_validate_query_insert_allows_encrypted() {
355 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
356 let result = qbi.validate_query(QueryType::Insert, &[], &[], &[]);
357 assert!(result.is_ok());
358 }
359
360 #[test]
361 fn test_validate_query_select_rejects_encrypted_where() {
362 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
363 let result = qbi.validate_query(QueryType::Select, &["email"], &[], &[]);
364 assert!(result.is_err());
365 }
366
367 #[test]
368 fn test_validate_query_delete_allows_encrypted() {
369 let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
370 let result = qbi.validate_query(QueryType::Delete, &[], &[], &[]);
371 assert!(result.is_ok());
372 }
373}