1#![forbid(unsafe_code)]
18
19#[cfg(not(any(feature = "postgres", feature = "sqlite", feature = "mysql")))]
20compile_error!(
21 "gatekeep-sqlx requires at least one SQLx backend feature: postgres, sqlite, or mysql"
22);
23
24use std::marker::PhantomData;
25
26use gatekeep::{
27 Condition, Context, FactId, LowerError, Lowered, QueryLowering, ResidualPolicy,
28 ResidualPolicyBranch, ResidualPolicyNode,
29};
30
31mod audit;
32mod fragment;
33
34#[cfg(feature = "mysql")]
35pub use audit::MySqlDecisionAuditRepository;
36#[cfg(feature = "postgres")]
37pub use audit::PgDecisionAuditRepository;
38#[cfg(feature = "sqlite")]
39pub use audit::SqliteDecisionAuditRepository;
40pub use audit::{DecisionAuditRecord, SqlxAuditError, SqlxAuditStore, SqlxDecisionAuditRepository};
41#[cfg(feature = "mysql")]
42pub use fragment::MySqlBackend;
43#[cfg(feature = "sqlite")]
44pub use fragment::SqliteBackend;
45pub use fragment::{
46 GatekeepSqlxBackend, SqlxDriver, SqlxDriverError, SqlxFragment, SqlxValue,
47 infer_enabled_driver_from_url, validate_database_url_for_backend,
48};
49#[cfg(feature = "postgres")]
50pub use fragment::{PgFragment, PgValue, PostgresBackend};
51
52pub trait SqlxFactPredicates<B>
54where
55 B: GatekeepSqlxBackend,
56{
57 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<B>>;
60}
61
62#[cfg(feature = "postgres")]
64pub trait PgFactPredicates {
65 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<PgFragment>;
68}
69
70#[cfg(feature = "postgres")]
71impl<T> SqlxFactPredicates<PostgresBackend> for T
72where
73 T: PgFactPredicates,
74{
75 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<PostgresBackend>> {
76 PgFactPredicates::predicate(self, fact, cx)
77 }
78}
79
80pub trait SqlOutcome {
82 fn to_sql_ordinal(&self) -> i64;
84}
85
86impl SqlOutcome for () {
87 fn to_sql_ordinal(&self) -> i64 {
88 0
89 }
90}
91
92pub trait OutcomeProjection<B, O>
94where
95 B: GatekeepSqlxBackend,
96{
97 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError>;
99}
100
101#[derive(Clone, Copy, Debug, Default)]
103pub struct OrdinalProjection;
104
105impl<B, O> OutcomeProjection<B, O> for OrdinalProjection
106where
107 B: GatekeepSqlxBackend,
108 O: SqlOutcome,
109{
110 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
111 Ok(SqlxFragment::bind(outcome.to_sql_ordinal()))
112 }
113}
114
115#[derive(Clone, Copy, Debug, Default)]
117pub struct NoGradeProjection;
118
119impl<B, O> OutcomeProjection<B, O> for NoGradeProjection
120where
121 B: GatekeepSqlxBackend,
122{
123 fn constant(&self, _outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
124 Err(LowerError::NonTotalGrade)
125 }
126}
127
128#[derive(Clone, Debug)]
130pub struct SqlxLowerer<B, P, M = OrdinalProjection> {
131 predicates: P,
132 projection: M,
133 backend: PhantomData<fn() -> B>,
134}
135
136#[cfg(feature = "postgres")]
138pub type PgLowerer<P, M = OrdinalProjection> = SqlxLowerer<PostgresBackend, P, M>;
139
140#[derive(Clone, Debug, PartialEq, Eq)]
141struct SqlxLowered<B> {
142 filter: SqlxFragment<B>,
143 grade: SqlxFragment<B>,
144}
145
146impl<B, P> SqlxLowerer<B, P, OrdinalProjection>
147where
148 B: GatekeepSqlxBackend,
149{
150 #[must_use]
152 pub const fn new(predicates: P) -> Self {
153 Self::with_projection(predicates, OrdinalProjection)
154 }
155}
156
157impl<B, P, M> SqlxLowerer<B, P, M>
158where
159 B: GatekeepSqlxBackend,
160{
161 #[must_use]
163 pub const fn with_projection(predicates: P, projection: M) -> Self {
164 Self {
165 predicates,
166 projection,
167 backend: PhantomData,
168 }
169 }
170
171 pub fn lower_filter<O>(
173 &self,
174 residual: &ResidualPolicy<O>,
175 cx: &Context,
176 ) -> Result<SqlxFragment<B>, LowerError>
177 where
178 P: SqlxFactPredicates<B>,
179 {
180 residual.try_fold_pruned(
181 &mut |branch| match branch {
182 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
183 !fallback.carries_obligation()
184 }
185 },
186 &mut |node| self.lower_filter_node(node, cx),
187 )
188 }
189
190 fn lower_filter_node<O>(
191 &self,
192 node: ResidualPolicyNode<'_, O, SqlxFragment<B>>,
193 cx: &Context,
194 ) -> Result<SqlxFragment<B>, LowerError>
195 where
196 P: SqlxFactPredicates<B>,
197 {
198 match node {
199 ResidualPolicyNode::Permit(_) | ResidualPolicyNode::PermitWithTrace { .. } => {
200 Ok(SqlxFragment::trusted("TRUE"))
201 }
202 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
203 Ok(SqlxFragment::trusted("FALSE"))
204 }
205 ResidualPolicyNode::Grant { condition, .. } => self.lower_condition(condition, cx),
206 ResidualPolicyNode::All { arms, .. } => Ok(fragment_set(arms, " AND ", "FALSE")),
207 ResidualPolicyNode::Any { arms, .. } => Ok(fragment_set(arms, " OR ", "FALSE")),
208 ResidualPolicyNode::OrElse {
209 fallback_policy,
210 primary,
211 fallback,
212 ..
213 } => {
214 if fallback_policy.carries_obligation() {
215 Ok(primary)
216 } else {
217 Ok(match fallback {
218 Some(fallback) => SqlxFragment::binary(" OR ", vec![primary, fallback]),
219 None => primary,
220 })
221 }
222 }
223 }
224 }
225
226 fn lower_condition(
227 &self,
228 condition: &Condition,
229 cx: &Context,
230 ) -> Result<SqlxFragment<B>, LowerError>
231 where
232 P: SqlxFactPredicates<B>,
233 {
234 match condition {
235 Condition::Always => Ok(SqlxFragment::trusted("TRUE")),
236 Condition::Never => Ok(SqlxFragment::trusted("FALSE")),
237 Condition::Has(fact) => self
238 .predicates
239 .predicate(fact, cx)
240 .map(is_true)
241 .ok_or_else(|| LowerError::Unlowerable(fact.clone())),
242 Condition::Not(inner) => Ok(SqlxFragment::unary(
243 "NOT ",
244 self.lower_condition(inner, cx)?,
245 )),
246 Condition::All(conditions) => {
247 lower_condition_set(conditions, " AND ", "FALSE", |item| {
248 self.lower_condition(item, cx)
249 })
250 }
251 Condition::Any(conditions) => {
252 lower_condition_set(conditions, " OR ", "FALSE", |item| {
253 self.lower_condition(item, cx)
254 })
255 }
256 }
257 }
258
259 fn lower_policy<O>(
260 &self,
261 residual: &ResidualPolicy<O>,
262 cx: &Context,
263 ) -> Result<SqlxLowered<B>, LowerError>
264 where
265 P: SqlxFactPredicates<B>,
266 M: OutcomeProjection<B, O>,
267 {
268 residual.try_fold_pruned(
269 &mut |branch| match branch {
270 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
271 !fallback.carries_obligation()
272 }
273 },
274 &mut |node| self.lower_node(node, cx),
275 )
276 }
277
278 fn lower_node<O>(
279 &self,
280 node: ResidualPolicyNode<'_, O, SqlxLowered<B>>,
281 cx: &Context,
282 ) -> Result<SqlxLowered<B>, LowerError>
283 where
284 P: SqlxFactPredicates<B>,
285 M: OutcomeProjection<B, O>,
286 {
287 match node {
288 ResidualPolicyNode::Permit(outcome)
289 | ResidualPolicyNode::PermitWithTrace { outcome, .. } => Ok(SqlxLowered {
290 filter: SqlxFragment::trusted("TRUE"),
291 grade: self.projection.constant(outcome)?,
292 }),
293 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
294 Ok(SqlxLowered {
295 filter: SqlxFragment::trusted("FALSE"),
296 grade: SqlxFragment::trusted("NULL"),
297 })
298 }
299 ResidualPolicyNode::Grant {
300 outcome, condition, ..
301 } => {
302 let filter = self.lower_condition(condition, cx)?;
303 let outcome = self.projection.constant(outcome)?;
304 Ok(SqlxLowered {
305 filter: filter.clone(),
306 grade: case_when(filter, outcome, SqlxFragment::trusted("NULL")),
307 })
308 }
309 ResidualPolicyNode::All { arms, .. } => {
310 let (filters, grades) = unzip_lowered(arms);
311 Ok(SqlxLowered {
312 filter: fragment_set(filters, " AND ", "FALSE"),
313 grade: grade_set::<B>(grades, B::MIN_FUNCTION),
314 })
315 }
316 ResidualPolicyNode::Any { arms, .. } => {
317 let (filters, grades) = unzip_lowered(arms);
318 Ok(SqlxLowered {
319 filter: fragment_set(filters, " OR ", "FALSE"),
320 grade: grade_set::<B>(grades, B::MAX_FUNCTION),
321 })
322 }
323 ResidualPolicyNode::OrElse {
324 fallback_policy,
325 primary,
326 fallback,
327 ..
328 } => {
329 if fallback_policy.carries_obligation() {
330 return Ok(primary);
331 }
332
333 Ok(match fallback {
334 Some(fallback) => SqlxLowered {
335 filter: SqlxFragment::binary(
336 " OR ",
337 vec![primary.filter.clone(), fallback.filter],
338 ),
339 grade: case_when(primary.filter, primary.grade, fallback.grade),
340 },
341 None => primary,
342 })
343 }
344 }
345 }
346}
347
348impl<O, B, P, M> QueryLowering<O> for SqlxLowerer<B, P, M>
349where
350 B: GatekeepSqlxBackend,
351 P: SqlxFactPredicates<B>,
352 M: OutcomeProjection<B, O>,
353{
354 type Filter = SqlxFragment<B>;
355 type Projection = SqlxFragment<B>;
356
357 fn lower(
358 &self,
359 residual: &ResidualPolicy<O>,
360 cx: &Context,
361 ) -> Result<Lowered<Self::Filter, Self::Projection>, LowerError> {
362 let lowered = self.lower_policy(residual, cx)?;
363 Ok(Lowered {
364 filter: lowered.filter,
365 grade: lowered.grade,
366 })
367 }
368}
369
370fn lower_condition_set<B>(
371 conditions: &[Condition],
372 separator: &str,
373 empty: &str,
374 lower: impl FnMut(&Condition) -> Result<SqlxFragment<B>, LowerError>,
375) -> Result<SqlxFragment<B>, LowerError> {
376 if conditions.is_empty() {
377 return Ok(SqlxFragment::trusted(empty));
378 }
379 let fragments = conditions
380 .iter()
381 .map(lower)
382 .collect::<Result<Vec<_>, _>>()?;
383 Ok(SqlxFragment::binary(separator, fragments))
384}
385
386fn fragment_set<B>(
387 fragments: Vec<SqlxFragment<B>>,
388 separator: &str,
389 empty: &str,
390) -> SqlxFragment<B> {
391 if fragments.is_empty() {
392 SqlxFragment::trusted(empty)
393 } else {
394 SqlxFragment::binary(separator, fragments)
395 }
396}
397
398fn grade_set<B>(grades: Vec<SqlxFragment<B>>, function: &str) -> SqlxFragment<B>
399where
400 B: GatekeepSqlxBackend,
401{
402 match grades.len() {
403 0 => SqlxFragment::trusted("NULL"),
404 1 => grades
405 .into_iter()
406 .next()
407 .unwrap_or_else(|| SqlxFragment::trusted("NULL")),
408 _ if B::GRADE_FUNCTION_PROPAGATES_NULL => {
409 let mut iter = grades.into_iter();
410 let mut combined = iter.next().unwrap_or_else(|| SqlxFragment::trusted("NULL"));
411 for grade in iter {
412 combined = null_safe_grade_pair(function, combined, grade);
413 }
414 combined
415 }
416 _ => SqlxFragment::function(function, grades),
417 }
418}
419
420fn null_safe_grade_pair<B>(
421 function: &str,
422 left: SqlxFragment<B>,
423 right: SqlxFragment<B>,
424) -> SqlxFragment<B> {
425 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
426 fragment.push_fragment(left.clone().wrapped());
427 fragment.push_sql(" IS NULL THEN ");
428 fragment.push_fragment(right.clone());
429 fragment.push_sql(" WHEN ");
430 fragment.push_fragment(right.clone().wrapped());
431 fragment.push_sql(" IS NULL THEN ");
432 fragment.push_fragment(left.clone());
433 fragment.push_sql(" ELSE ");
434 fragment.push_fragment(SqlxFragment::function(function, vec![left, right]));
435 fragment.push_sql(" END");
436 fragment
437}
438
439fn unzip_lowered<B>(lowered: Vec<SqlxLowered<B>>) -> (Vec<SqlxFragment<B>>, Vec<SqlxFragment<B>>) {
440 lowered
441 .into_iter()
442 .map(|lowered| (lowered.filter, lowered.grade))
443 .unzip()
444}
445
446fn case_when<B>(
447 condition: SqlxFragment<B>,
448 then_expr: SqlxFragment<B>,
449 else_expr: SqlxFragment<B>,
450) -> SqlxFragment<B> {
451 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
452 fragment.push_fragment(condition);
453 fragment.push_sql(" THEN ");
454 fragment.push_fragment(then_expr);
455 fragment.push_sql(" ELSE ");
456 fragment.push_fragment(else_expr);
457 fragment.push_sql(" END");
458 fragment
459}
460
461fn is_true<B>(predicate: SqlxFragment<B>) -> SqlxFragment<B> {
462 let mut fragment = SqlxFragment::trusted("(");
463 fragment.push_fragment(predicate);
464 fragment.push_sql(") IS TRUE");
465 fragment
466}