cratestack_sqlx/record_policy/
open_check.rs1use cratestack_core::{CratestackContext, CratestackError};
18
19use crate::query::{Truth, auth_value_to_sql, evaluate_input_truth};
20use crate::{PolicyExpr, ReadPolicy, ReadPredicate};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23struct Possible(u8);
24
25impl Possible {
26 const TRUE: u8 = 1;
27 const FALSE: u8 = 2;
28 const UNKNOWN: u8 = 4;
29 const ANY: Self = Self(Self::TRUE | Self::FALSE | Self::UNKNOWN);
30
31 fn of(truth: Truth) -> Self {
32 Self(match truth {
33 Truth::True => Self::TRUE,
34 Truth::False => Self::FALSE,
35 Truth::Unknown => Self::UNKNOWN,
36 })
37 }
38
39 fn values(self) -> impl Iterator<Item = Truth> {
40 [
41 (Self::TRUE, Truth::True),
42 (Self::FALSE, Truth::False),
43 (Self::UNKNOWN, Truth::Unknown),
44 ]
45 .into_iter()
46 .filter(move |(bit, _)| self.0 & bit != 0)
47 .map(|(_, truth)| truth)
48 }
49
50 fn combine(self, other: Self, op: fn(Truth, Truth) -> Truth) -> Self {
51 let mut out = 0;
52 for a in self.values() {
53 for b in other.values() {
54 out |= Self::of(op(a, b)).0;
55 }
56 }
57 Self(out)
58 }
59
60 fn can_be(self, truth: Truth) -> bool {
61 self.0 & Self::of(truth).0 != 0
62 }
63}
64
65fn possible(expr: PolicyExpr, ctx: &CratestackContext) -> Possible {
66 match expr {
67 PolicyExpr::Predicate(predicate) => predicate_possible(predicate, ctx),
68 PolicyExpr::And(exprs) => exprs.iter().fold(Possible::of(Truth::True), |acc, expr| {
69 acc.combine(possible(*expr, ctx), Truth::and)
70 }),
71 PolicyExpr::Or(exprs) => exprs.iter().fold(Possible::of(Truth::False), |acc, expr| {
72 acc.combine(possible(*expr, ctx), Truth::or)
73 }),
74 }
75}
76
77fn predicate_possible(predicate: ReadPredicate, ctx: &CratestackContext) -> Possible {
78 match predicate {
79 ReadPredicate::FieldEqAuth { auth_field, .. }
80 | ReadPredicate::FieldNeAuth { auth_field, .. } => {
81 if auth_value_to_sql(ctx, auth_field).is_some() {
82 Possible::ANY
83 } else {
84 Possible::of(Truth::False)
85 }
86 }
87 ReadPredicate::Relation { .. } => Possible(Possible::TRUE | Possible::FALSE),
89 ReadPredicate::FieldIsTrue { .. }
90 | ReadPredicate::FieldEqLiteral { .. }
91 | ReadPredicate::FieldNeLiteral { .. }
92 | ReadPredicate::FieldInLiterals { .. }
93 | ReadPredicate::FieldNotInLiterals { .. } => Possible::ANY,
94 ReadPredicate::AuthNotNull
95 | ReadPredicate::AuthIsNull
96 | ReadPredicate::AuthIsSystem
97 | ReadPredicate::HasRole { .. }
98 | ReadPredicate::InTenant { .. }
99 | ReadPredicate::AuthFieldEqLiteral { .. }
100 | ReadPredicate::AuthFieldNeLiteral { .. } => {
101 Possible::of(evaluate_input_truth(predicate, &[], ctx))
102 }
103 }
104}
105
106pub fn may_read_some_row(
110 allow: &[ReadPolicy],
111 deny: &[ReadPolicy],
112 ctx: &CratestackContext,
113) -> bool {
114 let allowed = allow
115 .iter()
116 .any(|policy| possible(policy.expr, ctx).can_be(Truth::True));
117 let always_denied = deny
118 .iter()
119 .any(|policy| !possible(policy.expr, ctx).can_be(Truth::False));
120 allowed && !always_denied
121}
122
123pub fn refuse_unreadable_subscription(
125 model: &str,
126 allow: &[ReadPolicy],
127 deny: &[ReadPolicy],
128 ctx: &CratestackContext,
129) -> Result<(), CratestackError> {
130 if may_read_some_row(allow, deny, ctx) {
131 Ok(())
132 } else if !ctx.is_authenticated() {
133 Err(CratestackError::Unauthorized(format!(
134 "subscribing to `{model}` requires authentication"
135 )))
136 } else {
137 Err(CratestackError::Forbidden(format!(
138 "no `{model}` record is readable by this caller"
139 )))
140 }
141}
142
143#[cfg(test)]
144mod tests;