Skip to main content

cratestack_sqlx/
record_policy.rs

1//! Read policy for a record the caller never queried: a model event handed
2//! to an `@@subscribe` subscriber (`GET /rpc/subscribe/{op_id}`).
3//!
4//! A read through a delegate has its policy pushed into the `WHERE` clause.
5//! An event has no query: its record arrives from the outbox, so the
6//! policy is decided in-process against the record's values, by the same
7//! evaluator the create path uses (relation predicates still probe the
8//! database with `EXISTS`). Everything else follows the read path: an
9//! empty allow list denies, a `@deny` fires on anything not `False`.
10//!
11//! The evaluator reads an absent column as `False`, which would let a
12//! `@deny` over it pass. An event payload lacks every `@server_only`
13//! column (`#[serde(skip)]`), so [`missing_policy_columns`] is checked when
14//! a subscription opens, and a policy it cannot decide is refused there.
15
16mod open_check;
17
18use cratestack_core::{CratestackContext, CratestackError};
19
20pub use open_check::{may_read_some_row, refuse_unreadable_subscription};
21
22use crate::query::{PolicyDb, evaluate_create_policies};
23use crate::{PolicyExpr, ReadPolicy, ReadPredicate, SqlColumnValue, sqlx};
24
25/// Whether `ctx` may read the record whose column values are `values`,
26/// under `allow` / `deny` (a model's detail-and-read policies).
27pub async fn record_readable(
28    pool: &sqlx::PgPool,
29    allow: &[ReadPolicy],
30    deny: &[ReadPolicy],
31    values: &[SqlColumnValue],
32    ctx: &CratestackContext,
33) -> Result<bool, CratestackError> {
34    evaluate_create_policies(PolicyDb::Pool(pool), allow, deny, values, ctx).await
35}
36
37/// The columns of the policy's own model that `allow` / `deny` read and
38/// `present` does not hold. A relation predicate counts its local join
39/// column only: its nested expression runs in SQL against the related
40/// table, where every column is there.
41pub fn missing_policy_columns(
42    allow: &[ReadPolicy],
43    deny: &[ReadPolicy],
44    present: &[&str],
45) -> Vec<&'static str> {
46    let mut missing = Vec::new();
47    for policy in allow.iter().chain(deny) {
48        collect_columns(policy.expr, &mut missing);
49    }
50    missing.retain(|column| !present.contains(column));
51    missing.sort_unstable();
52    missing.dedup();
53    missing
54}
55
56fn collect_columns(expr: PolicyExpr, out: &mut Vec<&'static str>) {
57    match expr {
58        PolicyExpr::Predicate(predicate) => out.extend(predicate_column(predicate)),
59        PolicyExpr::And(exprs) | PolicyExpr::Or(exprs) => {
60            for expr in exprs {
61                collect_columns(*expr, out);
62            }
63        }
64    }
65}
66
67fn predicate_column(predicate: ReadPredicate) -> Option<&'static str> {
68    match predicate {
69        ReadPredicate::FieldIsTrue { column }
70        | ReadPredicate::FieldEqLiteral { column, .. }
71        | ReadPredicate::FieldNeLiteral { column, .. }
72        | ReadPredicate::FieldInLiterals { column, .. }
73        | ReadPredicate::FieldNotInLiterals { column, .. }
74        | ReadPredicate::FieldEqAuth { column, .. }
75        | ReadPredicate::FieldNeAuth { column, .. } => Some(column),
76        ReadPredicate::Relation { parent_column, .. } => Some(parent_column),
77        ReadPredicate::AuthNotNull
78        | ReadPredicate::AuthIsNull
79        | ReadPredicate::AuthIsSystem
80        | ReadPredicate::HasRole { .. }
81        | ReadPredicate::InTenant { .. }
82        | ReadPredicate::AuthFieldEqLiteral { .. }
83        | ReadPredicate::AuthFieldNeLiteral { .. } => None,
84    }
85}
86
87#[cfg(test)]
88mod tests;