1use qail_core::access::{AccessContext, AccessError, AccessPolicy};
4use qail_core::ast::Qail;
5
6use super::{
7 AstPipelineMode, AutoCountPlan, PgDriver, PgError, PgPool, PgResult, PgRow, PooledConnection,
8 PreparedAstQuery, QueryResult, ResultFormat,
9};
10
11fn access_denied_error(err: AccessError) -> PgError {
12 PgError::Query(format!("Access denied by policy: {}", err))
13}
14
15fn check_access(policy: &AccessPolicy, ctx: &AccessContext, cmd: &Qail) -> PgResult<()> {
16 policy.check_command(ctx, cmd).map_err(access_denied_error)
17}
18
19fn check_all_access(policy: &AccessPolicy, ctx: &AccessContext, cmds: &[Qail]) -> PgResult<()> {
20 for cmd in cmds {
21 check_access(policy, ctx, cmd)?;
22 }
23 Ok(())
24}
25
26fn copy_export_table_command(table: &str, columns: &[String]) -> Qail {
27 Qail::export(table).columns(columns.iter().map(String::as_str))
28}
29
30impl PgDriver {
31 pub fn check_access(
33 &self,
34 cmd: &Qail,
35 access_ctx: &AccessContext,
36 access_policy: &AccessPolicy,
37 ) -> PgResult<()> {
38 check_access(access_policy, access_ctx, cmd)
39 }
40
41 pub async fn fetch_all_checked(
43 &mut self,
44 cmd: &Qail,
45 access_ctx: &AccessContext,
46 access_policy: &AccessPolicy,
47 ) -> PgResult<Vec<PgRow>> {
48 check_access(access_policy, access_ctx, cmd)?;
49 self.fetch_all(cmd).await
50 }
51
52 pub async fn fetch_all_with_format_checked(
54 &mut self,
55 cmd: &Qail,
56 result_format: ResultFormat,
57 access_ctx: &AccessContext,
58 access_policy: &AccessPolicy,
59 ) -> PgResult<Vec<PgRow>> {
60 check_access(access_policy, access_ctx, cmd)?;
61 self.fetch_all_with_format(cmd, result_format).await
62 }
63
64 pub async fn fetch_all_uncached_checked(
66 &mut self,
67 cmd: &Qail,
68 access_ctx: &AccessContext,
69 access_policy: &AccessPolicy,
70 ) -> PgResult<Vec<PgRow>> {
71 check_access(access_policy, access_ctx, cmd)?;
72 self.fetch_all_uncached(cmd).await
73 }
74
75 pub async fn fetch_all_uncached_with_format_checked(
77 &mut self,
78 cmd: &Qail,
79 result_format: ResultFormat,
80 access_ctx: &AccessContext,
81 access_policy: &AccessPolicy,
82 ) -> PgResult<Vec<PgRow>> {
83 check_access(access_policy, access_ctx, cmd)?;
84 self.fetch_all_uncached_with_format(cmd, result_format)
85 .await
86 }
87
88 pub async fn fetch_all_fast_checked(
90 &mut self,
91 cmd: &Qail,
92 access_ctx: &AccessContext,
93 access_policy: &AccessPolicy,
94 ) -> PgResult<Vec<PgRow>> {
95 check_access(access_policy, access_ctx, cmd)?;
96 self.fetch_all_fast(cmd).await
97 }
98
99 pub async fn fetch_all_fast_with_format_checked(
101 &mut self,
102 cmd: &Qail,
103 result_format: ResultFormat,
104 access_ctx: &AccessContext,
105 access_policy: &AccessPolicy,
106 ) -> PgResult<Vec<PgRow>> {
107 check_access(access_policy, access_ctx, cmd)?;
108 self.fetch_all_fast_with_format(cmd, result_format).await
109 }
110
111 pub async fn fetch_one_checked(
113 &mut self,
114 cmd: &Qail,
115 access_ctx: &AccessContext,
116 access_policy: &AccessPolicy,
117 ) -> PgResult<PgRow> {
118 check_access(access_policy, access_ctx, cmd)?;
119 self.fetch_one(cmd).await
120 }
121
122 pub async fn prepare_ast_query_checked(
128 &mut self,
129 cmd: &Qail,
130 access_ctx: &AccessContext,
131 access_policy: &AccessPolicy,
132 ) -> PgResult<PreparedAstQuery> {
133 check_access(access_policy, access_ctx, cmd)?;
134 self.prepare_ast_query(cmd).await
135 }
136
137 pub async fn fetch_typed_checked<T: super::row::QailRow>(
139 &mut self,
140 cmd: &Qail,
141 access_ctx: &AccessContext,
142 access_policy: &AccessPolicy,
143 ) -> PgResult<Vec<T>> {
144 check_access(access_policy, access_ctx, cmd)?;
145 self.fetch_typed(cmd).await
146 }
147
148 pub async fn fetch_typed_with_format_checked<T: super::row::QailRow>(
150 &mut self,
151 cmd: &Qail,
152 result_format: ResultFormat,
153 access_ctx: &AccessContext,
154 access_policy: &AccessPolicy,
155 ) -> PgResult<Vec<T>> {
156 check_access(access_policy, access_ctx, cmd)?;
157 self.fetch_typed_with_format(cmd, result_format).await
158 }
159
160 pub async fn fetch_one_typed_checked<T: super::row::QailRow>(
162 &mut self,
163 cmd: &Qail,
164 access_ctx: &AccessContext,
165 access_policy: &AccessPolicy,
166 ) -> PgResult<Option<T>> {
167 check_access(access_policy, access_ctx, cmd)?;
168 self.fetch_one_typed(cmd).await
169 }
170
171 pub async fn fetch_one_typed_with_format_checked<T: super::row::QailRow>(
173 &mut self,
174 cmd: &Qail,
175 result_format: ResultFormat,
176 access_ctx: &AccessContext,
177 access_policy: &AccessPolicy,
178 ) -> PgResult<Option<T>> {
179 check_access(access_policy, access_ctx, cmd)?;
180 self.fetch_one_typed_with_format(cmd, result_format).await
181 }
182
183 pub async fn execute_checked(
185 &mut self,
186 cmd: &Qail,
187 access_ctx: &AccessContext,
188 access_policy: &AccessPolicy,
189 ) -> PgResult<u64> {
190 check_access(access_policy, access_ctx, cmd)?;
191 self.execute(cmd).await
192 }
193
194 pub async fn copy_bulk_checked(
196 &mut self,
197 cmd: &Qail,
198 rows: &[Vec<qail_core::ast::Value>],
199 access_ctx: &AccessContext,
200 access_policy: &AccessPolicy,
201 ) -> PgResult<u64> {
202 check_access(access_policy, access_ctx, cmd)?;
203 self.copy_bulk(cmd, rows).await
204 }
205
206 pub async fn copy_bulk_bytes_checked(
208 &mut self,
209 cmd: &Qail,
210 data: &[u8],
211 access_ctx: &AccessContext,
212 access_policy: &AccessPolicy,
213 ) -> PgResult<u64> {
214 check_access(access_policy, access_ctx, cmd)?;
215 self.copy_bulk_bytes(cmd, data).await
216 }
217
218 pub async fn copy_export_table_checked(
220 &mut self,
221 table: &str,
222 columns: &[String],
223 access_ctx: &AccessContext,
224 access_policy: &AccessPolicy,
225 ) -> PgResult<Vec<u8>> {
226 check_access(
227 access_policy,
228 access_ctx,
229 ©_export_table_command(table, columns),
230 )?;
231 self.copy_export_table(table, columns).await
232 }
233
234 pub async fn copy_export_table_stream_checked<F, Fut>(
236 &mut self,
237 table: &str,
238 columns: &[String],
239 on_chunk: F,
240 access_ctx: &AccessContext,
241 access_policy: &AccessPolicy,
242 ) -> PgResult<()>
243 where
244 F: FnMut(Vec<u8>) -> Fut,
245 Fut: std::future::Future<Output = PgResult<()>>,
246 {
247 check_access(
248 access_policy,
249 access_ctx,
250 ©_export_table_command(table, columns),
251 )?;
252 self.copy_export_table_stream(table, columns, on_chunk)
253 .await
254 }
255
256 pub async fn copy_export_cmd_stream_checked<F, Fut>(
258 &mut self,
259 cmd: &Qail,
260 on_chunk: F,
261 access_ctx: &AccessContext,
262 access_policy: &AccessPolicy,
263 ) -> PgResult<()>
264 where
265 F: FnMut(Vec<u8>) -> Fut,
266 Fut: std::future::Future<Output = PgResult<()>>,
267 {
268 check_access(access_policy, access_ctx, cmd)?;
269 self.copy_export_cmd_stream(cmd, on_chunk).await
270 }
271
272 pub async fn copy_export_cmd_stream_rows_checked<F>(
274 &mut self,
275 cmd: &Qail,
276 on_row: F,
277 access_ctx: &AccessContext,
278 access_policy: &AccessPolicy,
279 ) -> PgResult<()>
280 where
281 F: FnMut(Vec<String>) -> PgResult<()>,
282 {
283 check_access(access_policy, access_ctx, cmd)?;
284 self.copy_export_cmd_stream_rows(cmd, on_row).await
285 }
286
287 pub async fn stream_cmd_checked(
289 &mut self,
290 cmd: &Qail,
291 batch_size: usize,
292 access_ctx: &AccessContext,
293 access_policy: &AccessPolicy,
294 ) -> PgResult<Vec<Vec<PgRow>>> {
295 check_access(access_policy, access_ctx, cmd)?;
296 self.stream_cmd(cmd, batch_size).await
297 }
298
299 pub async fn query_ast_checked(
301 &mut self,
302 cmd: &Qail,
303 access_ctx: &AccessContext,
304 access_policy: &AccessPolicy,
305 ) -> PgResult<QueryResult> {
306 check_access(access_policy, access_ctx, cmd)?;
307 self.query_ast(cmd).await
308 }
309
310 pub async fn query_ast_with_format_checked(
312 &mut self,
313 cmd: &Qail,
314 result_format: ResultFormat,
315 access_ctx: &AccessContext,
316 access_policy: &AccessPolicy,
317 ) -> PgResult<QueryResult> {
318 check_access(access_policy, access_ctx, cmd)?;
319 self.query_ast_with_format(cmd, result_format).await
320 }
321
322 pub async fn execute_batch_checked(
327 &mut self,
328 cmds: &[Qail],
329 access_ctx: &AccessContext,
330 access_policy: &AccessPolicy,
331 ) -> PgResult<Vec<u64>> {
332 check_all_access(access_policy, access_ctx, cmds)?;
333 self.execute_batch(cmds).await
334 }
335
336 pub async fn execute_count_auto_with_plan_checked(
338 &mut self,
339 cmds: &[Qail],
340 access_ctx: &AccessContext,
341 access_policy: &AccessPolicy,
342 ) -> PgResult<(usize, AutoCountPlan)> {
343 check_all_access(access_policy, access_ctx, cmds)?;
344 self.execute_count_auto_with_plan(cmds).await
345 }
346
347 pub async fn execute_count_auto_checked(
349 &mut self,
350 cmds: &[Qail],
351 access_ctx: &AccessContext,
352 access_policy: &AccessPolicy,
353 ) -> PgResult<usize> {
354 check_all_access(access_policy, access_ctx, cmds)?;
355 self.execute_count_auto(cmds).await
356 }
357
358 pub async fn pipeline_execute_count_with_mode_checked(
360 &mut self,
361 cmds: &[Qail],
362 mode: AstPipelineMode,
363 access_ctx: &AccessContext,
364 access_policy: &AccessPolicy,
365 ) -> PgResult<usize> {
366 check_all_access(access_policy, access_ctx, cmds)?;
367 self.pipeline_execute_count_with_mode(cmds, mode).await
368 }
369
370 pub async fn pipeline_execute_count_checked(
372 &mut self,
373 cmds: &[Qail],
374 access_ctx: &AccessContext,
375 access_policy: &AccessPolicy,
376 ) -> PgResult<usize> {
377 check_all_access(access_policy, access_ctx, cmds)?;
378 self.pipeline_execute_count(cmds).await
379 }
380
381 pub async fn pipeline_execute_rows_checked(
383 &mut self,
384 cmds: &[Qail],
385 access_ctx: &AccessContext,
386 access_policy: &AccessPolicy,
387 ) -> PgResult<Vec<Vec<PgRow>>> {
388 check_all_access(access_policy, access_ctx, cmds)?;
389 self.pipeline_execute_rows(cmds).await
390 }
391}
392
393impl PooledConnection {
394 pub fn check_access(
396 &self,
397 cmd: &Qail,
398 access_ctx: &AccessContext,
399 access_policy: &AccessPolicy,
400 ) -> PgResult<()> {
401 check_access(access_policy, access_ctx, cmd)
402 }
403
404 pub async fn execute_checked(
406 &mut self,
407 cmd: &Qail,
408 access_ctx: &AccessContext,
409 access_policy: &AccessPolicy,
410 ) -> PgResult<u64> {
411 check_access(access_policy, access_ctx, cmd)?;
412 self.execute(cmd).await
413 }
414
415 pub async fn fetch_all_cached_checked(
417 &mut self,
418 cmd: &Qail,
419 access_ctx: &AccessContext,
420 access_policy: &AccessPolicy,
421 ) -> PgResult<Vec<PgRow>> {
422 check_access(access_policy, access_ctx, cmd)?;
423 self.fetch_all_cached(cmd).await
424 }
425
426 pub async fn fetch_all_cached_with_format_checked(
428 &mut self,
429 cmd: &Qail,
430 result_format: ResultFormat,
431 access_ctx: &AccessContext,
432 access_policy: &AccessPolicy,
433 ) -> PgResult<Vec<PgRow>> {
434 check_access(access_policy, access_ctx, cmd)?;
435 self.fetch_all_cached_with_format(cmd, result_format).await
436 }
437
438 pub async fn fetch_all_uncached_checked(
440 &mut self,
441 cmd: &Qail,
442 access_ctx: &AccessContext,
443 access_policy: &AccessPolicy,
444 ) -> PgResult<Vec<PgRow>> {
445 check_access(access_policy, access_ctx, cmd)?;
446 self.fetch_all_uncached(cmd).await
447 }
448
449 pub async fn fetch_all_uncached_with_format_checked(
451 &mut self,
452 cmd: &Qail,
453 result_format: ResultFormat,
454 access_ctx: &AccessContext,
455 access_policy: &AccessPolicy,
456 ) -> PgResult<Vec<PgRow>> {
457 check_access(access_policy, access_ctx, cmd)?;
458 self.fetch_all_uncached_with_format(cmd, result_format)
459 .await
460 }
461
462 pub async fn fetch_all_fast_checked(
464 &mut self,
465 cmd: &Qail,
466 access_ctx: &AccessContext,
467 access_policy: &AccessPolicy,
468 ) -> PgResult<Vec<PgRow>> {
469 check_access(access_policy, access_ctx, cmd)?;
470 self.fetch_all_fast(cmd).await
471 }
472
473 pub async fn fetch_all_fast_with_format_checked(
475 &mut self,
476 cmd: &Qail,
477 result_format: ResultFormat,
478 access_ctx: &AccessContext,
479 access_policy: &AccessPolicy,
480 ) -> PgResult<Vec<PgRow>> {
481 check_access(access_policy, access_ctx, cmd)?;
482 self.fetch_all_fast_with_format(cmd, result_format).await
483 }
484
485 pub async fn fetch_all_with_rls_checked(
487 &mut self,
488 cmd: &Qail,
489 rls_sql: &str,
490 access_ctx: &AccessContext,
491 access_policy: &AccessPolicy,
492 ) -> PgResult<Vec<PgRow>> {
493 check_access(access_policy, access_ctx, cmd)?;
494 self.fetch_all_with_rls(cmd, rls_sql).await
495 }
496
497 pub async fn fetch_all_with_rls_with_format_checked(
499 &mut self,
500 cmd: &Qail,
501 rls_sql: &str,
502 result_format: ResultFormat,
503 access_ctx: &AccessContext,
504 access_policy: &AccessPolicy,
505 ) -> PgResult<Vec<PgRow>> {
506 check_access(access_policy, access_ctx, cmd)?;
507 self.fetch_all_with_rls_with_format(cmd, rls_sql, result_format)
508 .await
509 }
510
511 pub async fn copy_export_checked(
513 &mut self,
514 cmd: &Qail,
515 access_ctx: &AccessContext,
516 access_policy: &AccessPolicy,
517 ) -> PgResult<Vec<Vec<String>>> {
518 check_access(access_policy, access_ctx, cmd)?;
519 self.copy_export(cmd).await
520 }
521
522 pub async fn copy_export_stream_raw_checked<F, Fut>(
524 &mut self,
525 cmd: &Qail,
526 on_chunk: F,
527 access_ctx: &AccessContext,
528 access_policy: &AccessPolicy,
529 ) -> PgResult<()>
530 where
531 F: FnMut(Vec<u8>) -> Fut,
532 Fut: std::future::Future<Output = PgResult<()>>,
533 {
534 check_access(access_policy, access_ctx, cmd)?;
535 self.copy_export_stream_raw(cmd, on_chunk).await
536 }
537
538 pub async fn copy_export_stream_rows_checked<F>(
540 &mut self,
541 cmd: &Qail,
542 on_row: F,
543 access_ctx: &AccessContext,
544 access_policy: &AccessPolicy,
545 ) -> PgResult<()>
546 where
547 F: FnMut(Vec<String>) -> PgResult<()>,
548 {
549 check_access(access_policy, access_ctx, cmd)?;
550 self.copy_export_stream_rows(cmd, on_row).await
551 }
552
553 pub async fn copy_export_table_checked(
555 &mut self,
556 table: &str,
557 columns: &[String],
558 access_ctx: &AccessContext,
559 access_policy: &AccessPolicy,
560 ) -> PgResult<Vec<u8>> {
561 check_access(
562 access_policy,
563 access_ctx,
564 ©_export_table_command(table, columns),
565 )?;
566 self.copy_export_table(table, columns).await
567 }
568
569 pub async fn copy_export_table_stream_checked<F, Fut>(
571 &mut self,
572 table: &str,
573 columns: &[String],
574 on_chunk: F,
575 access_ctx: &AccessContext,
576 access_policy: &AccessPolicy,
577 ) -> PgResult<()>
578 where
579 F: FnMut(Vec<u8>) -> Fut,
580 Fut: std::future::Future<Output = PgResult<()>>,
581 {
582 check_access(
583 access_policy,
584 access_ctx,
585 ©_export_table_command(table, columns),
586 )?;
587 self.copy_export_table_stream(table, columns, on_chunk)
588 .await
589 }
590
591 pub async fn fetch_typed_checked<T: super::row::QailRow>(
593 &mut self,
594 cmd: &Qail,
595 access_ctx: &AccessContext,
596 access_policy: &AccessPolicy,
597 ) -> PgResult<Vec<T>> {
598 check_access(access_policy, access_ctx, cmd)?;
599 self.fetch_typed(cmd).await
600 }
601
602 pub async fn fetch_typed_with_format_checked<T: super::row::QailRow>(
604 &mut self,
605 cmd: &Qail,
606 result_format: ResultFormat,
607 access_ctx: &AccessContext,
608 access_policy: &AccessPolicy,
609 ) -> PgResult<Vec<T>> {
610 check_access(access_policy, access_ctx, cmd)?;
611 self.fetch_typed_with_format(cmd, result_format).await
612 }
613
614 pub async fn fetch_one_typed_checked<T: super::row::QailRow>(
616 &mut self,
617 cmd: &Qail,
618 access_ctx: &AccessContext,
619 access_policy: &AccessPolicy,
620 ) -> PgResult<Option<T>> {
621 check_access(access_policy, access_ctx, cmd)?;
622 self.fetch_one_typed(cmd).await
623 }
624
625 pub async fn fetch_one_typed_with_format_checked<T: super::row::QailRow>(
627 &mut self,
628 cmd: &Qail,
629 result_format: ResultFormat,
630 access_ctx: &AccessContext,
631 access_policy: &AccessPolicy,
632 ) -> PgResult<Option<T>> {
633 check_access(access_policy, access_ctx, cmd)?;
634 self.fetch_one_typed_with_format(cmd, result_format).await
635 }
636
637 pub async fn pipeline_execute_rows_ast_checked(
639 &mut self,
640 cmds: &[Qail],
641 access_ctx: &AccessContext,
642 access_policy: &AccessPolicy,
643 ) -> PgResult<Vec<Vec<Vec<Option<Vec<u8>>>>>> {
644 check_all_access(access_policy, access_ctx, cmds)?;
645 self.pipeline_execute_rows_ast(cmds).await
646 }
647}
648
649impl PgPool {
650 pub async fn execute_count_auto_with_plan_checked(
652 &self,
653 cmds: &[Qail],
654 access_ctx: &AccessContext,
655 access_policy: &AccessPolicy,
656 ) -> PgResult<(usize, AutoCountPlan)> {
657 check_all_access(access_policy, access_ctx, cmds)?;
658 self.execute_count_auto_with_plan(cmds).await
659 }
660
661 pub async fn execute_count_auto_checked(
663 &self,
664 cmds: &[Qail],
665 access_ctx: &AccessContext,
666 access_policy: &AccessPolicy,
667 ) -> PgResult<usize> {
668 check_all_access(access_policy, access_ctx, cmds)?;
669 self.execute_count_auto(cmds).await
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use qail_core::access::{
676 AccessContext, AccessOperation, AccessPolicy, ColumnRule, TableAccessPolicy,
677 };
678 use qail_core::ast::{Expr, Qail};
679
680 use super::{check_access, check_all_access, copy_export_table_command};
681 use crate::driver::PgError;
682
683 #[test]
684 fn checked_pg_error_uses_existing_query_variant() {
685 let err = check_access(
686 &AccessPolicy::new(),
687 &AccessContext::anonymous(),
688 &Qail::get("orders"),
689 )
690 .expect_err("missing policy should fail closed");
691
692 match err {
693 PgError::Query(message) => {
694 assert!(message.contains("Access denied by policy"));
695 assert!(message.contains("orders"));
696 }
697 other => panic!("unexpected error variant: {other:?}"),
698 }
699 }
700
701 #[test]
702 fn checked_batch_rejects_denied_later_command_before_execution() {
703 let policy = AccessPolicy::new().with_table(
704 "orders",
705 TableAccessPolicy::new()
706 .allow_operations([AccessOperation::Read])
707 .read_columns(ColumnRule::only(["id"])),
708 );
709 let cmds = vec![
710 Qail::get("orders").columns(["id"]),
711 Qail::get("orders").columns(["id", "private_note"]),
712 ];
713
714 let err = check_all_access(&policy, &AccessContext::anonymous(), &cmds)
715 .expect_err("second command should deny before any wrapper executes");
716
717 assert!(matches!(err, PgError::Query(_)));
718 }
719
720 #[test]
721 fn checked_policy_recurses_into_subqueries() {
722 let policy = AccessPolicy::new().with_table(
723 "orders",
724 TableAccessPolicy::new().allow_operations([AccessOperation::Read]),
725 );
726 let cmd = Qail::get("orders").columns_expr([Expr::Subquery {
727 query: Box::new(Qail::get("users").columns(["id"])),
728 alias: None,
729 }]);
730
731 let err = check_access(&policy, &AccessContext::anonymous(), &cmd)
732 .expect_err("subquery table should require its own policy");
733
734 match err {
735 PgError::Query(message) => assert!(message.contains("users")),
736 other => panic!("unexpected error variant: {other:?}"),
737 }
738 }
739
740 #[test]
741 fn checked_copy_export_table_command_uses_read_column_policy() {
742 let policy = AccessPolicy::new().with_table(
743 "orders",
744 TableAccessPolicy::new()
745 .allow_operations([AccessOperation::Read])
746 .read_columns(ColumnRule::only(["id"])),
747 );
748 let columns = vec!["id".to_string(), "private_note".to_string()];
749 let cmd = copy_export_table_command("orders", &columns);
750
751 let err = check_access(&policy, &AccessContext::anonymous(), &cmd)
752 .expect_err("denied COPY export column should fail before execution");
753
754 match err {
755 PgError::Query(message) => assert!(message.contains("private_note")),
756 other => panic!("unexpected error variant: {other:?}"),
757 }
758 }
759}