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>;
104}
105
106#[derive(Clone, Copy, Debug, Default)]
108pub struct OrdinalProjection;
109
110impl<B, O> OutcomeProjection<B, O> for OrdinalProjection
111where
112 B: GatekeepSqlxBackend,
113 O: SqlOutcome,
114{
115 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
116 Ok(SqlxFragment::bind(outcome.to_sql_ordinal()))
117 }
118}
119
120#[derive(Clone, Copy, Debug, Default)]
122pub struct NoGradeProjection;
123
124impl<B, O> OutcomeProjection<B, O> for NoGradeProjection
125where
126 B: GatekeepSqlxBackend,
127{
128 fn constant(&self, _outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
129 Err(LowerError::NonTotalGrade)
130 }
131}
132
133#[derive(Clone, Debug)]
135pub struct SqlxLowerer<B, P, M = OrdinalProjection> {
136 predicates: P,
137 projection: M,
138 backend: PhantomData<fn() -> B>,
139}
140
141#[cfg(feature = "postgres")]
143pub type PgLowerer<P, M = OrdinalProjection> = SqlxLowerer<PostgresBackend, P, M>;
144
145#[derive(Clone, Debug, PartialEq, Eq)]
146struct SqlxLowered<B> {
147 filter: SqlxFragment<B>,
148 grade: SqlxFragment<B>,
149}
150
151impl<B, P> SqlxLowerer<B, P, OrdinalProjection>
152where
153 B: GatekeepSqlxBackend,
154{
155 #[must_use]
157 pub const fn new(predicates: P) -> Self {
158 Self::with_projection(predicates, OrdinalProjection)
159 }
160}
161
162impl<B, P, M> SqlxLowerer<B, P, M>
163where
164 B: GatekeepSqlxBackend,
165{
166 #[must_use]
168 pub const fn with_projection(predicates: P, projection: M) -> Self {
169 Self {
170 predicates,
171 projection,
172 backend: PhantomData,
173 }
174 }
175
176 pub fn lower_filter<O>(
183 &self,
184 residual: &ResidualPolicy<O>,
185 cx: &Context,
186 ) -> Result<SqlxFragment<B>, LowerError>
187 where
188 P: SqlxFactPredicates<B>,
189 {
190 residual.try_fold_pruned(
191 &mut |branch| match branch {
192 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
193 !fallback.carries_obligation()
194 }
195 },
196 &mut |node| self.lower_filter_node(node, cx),
197 )
198 }
199
200 fn lower_filter_node<O>(
201 &self,
202 node: ResidualPolicyNode<'_, O, SqlxFragment<B>>,
203 cx: &Context,
204 ) -> Result<SqlxFragment<B>, LowerError>
205 where
206 P: SqlxFactPredicates<B>,
207 {
208 match node {
209 ResidualPolicyNode::Permit(_) | ResidualPolicyNode::PermitWithTrace { .. } => {
210 Ok(SqlxFragment::trusted("TRUE"))
211 }
212 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
213 Ok(SqlxFragment::trusted("FALSE"))
214 }
215 ResidualPolicyNode::Grant { condition, .. } => self.lower_condition(condition, cx),
216 ResidualPolicyNode::All { arms, .. } => Ok(fragment_set(arms, " AND ", "FALSE")),
217 ResidualPolicyNode::Any { arms, .. } => Ok(fragment_set(arms, " OR ", "FALSE")),
218 ResidualPolicyNode::OrElse {
219 fallback_policy,
220 primary,
221 fallback,
222 ..
223 } => {
224 if fallback_policy.carries_obligation() {
225 Ok(primary)
226 } else {
227 Ok(match fallback {
228 Some(fallback) => SqlxFragment::binary(" OR ", vec![primary, fallback]),
229 None => primary,
230 })
231 }
232 }
233 }
234 }
235
236 fn lower_condition(
237 &self,
238 condition: &Condition,
239 cx: &Context,
240 ) -> Result<SqlxFragment<B>, LowerError>
241 where
242 P: SqlxFactPredicates<B>,
243 {
244 match condition {
245 Condition::Always => Ok(SqlxFragment::trusted("TRUE")),
246 Condition::Never => Ok(SqlxFragment::trusted("FALSE")),
247 Condition::Has(fact) => self
248 .predicates
249 .predicate(fact, cx)
250 .map(is_true)
251 .ok_or_else(|| LowerError::Unlowerable(fact.clone())),
252 Condition::Not(inner) => Ok(SqlxFragment::unary(
253 "NOT ",
254 self.lower_condition(inner, cx)?,
255 )),
256 Condition::All(conditions) => {
257 lower_condition_set(conditions, " AND ", "FALSE", |item| {
258 self.lower_condition(item, cx)
259 })
260 }
261 Condition::Any(conditions) => {
262 lower_condition_set(conditions, " OR ", "FALSE", |item| {
263 self.lower_condition(item, cx)
264 })
265 }
266 }
267 }
268
269 fn lower_policy<O>(
270 &self,
271 residual: &ResidualPolicy<O>,
272 cx: &Context,
273 ) -> Result<SqlxLowered<B>, LowerError>
274 where
275 P: SqlxFactPredicates<B>,
276 M: OutcomeProjection<B, O>,
277 {
278 residual.try_fold_pruned(
279 &mut |branch| match branch {
280 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
281 !fallback.carries_obligation()
282 }
283 },
284 &mut |node| self.lower_node(node, cx),
285 )
286 }
287
288 fn lower_node<O>(
289 &self,
290 node: ResidualPolicyNode<'_, O, SqlxLowered<B>>,
291 cx: &Context,
292 ) -> Result<SqlxLowered<B>, LowerError>
293 where
294 P: SqlxFactPredicates<B>,
295 M: OutcomeProjection<B, O>,
296 {
297 match node {
298 ResidualPolicyNode::Permit(outcome)
299 | ResidualPolicyNode::PermitWithTrace { outcome, .. } => Ok(SqlxLowered {
300 filter: SqlxFragment::trusted("TRUE"),
301 grade: self.projection.constant(outcome)?,
302 }),
303 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
304 Ok(SqlxLowered {
305 filter: SqlxFragment::trusted("FALSE"),
306 grade: SqlxFragment::trusted("NULL"),
307 })
308 }
309 ResidualPolicyNode::Grant {
310 outcome, condition, ..
311 } => {
312 let filter = self.lower_condition(condition, cx)?;
313 let outcome = self.projection.constant(outcome)?;
314 Ok(SqlxLowered {
315 filter: filter.clone(),
316 grade: case_when(filter, outcome, SqlxFragment::trusted("NULL")),
317 })
318 }
319 ResidualPolicyNode::All { arms, .. } => {
320 let (filters, grades) = unzip_lowered(arms);
321 Ok(SqlxLowered {
322 filter: fragment_set(filters, " AND ", "FALSE"),
323 grade: grade_set::<B>(grades, B::MIN_FUNCTION),
324 })
325 }
326 ResidualPolicyNode::Any { arms, .. } => {
327 let (filters, grades) = unzip_lowered(arms);
328 Ok(SqlxLowered {
329 filter: fragment_set(filters, " OR ", "FALSE"),
330 grade: grade_set::<B>(grades, B::MAX_FUNCTION),
331 })
332 }
333 ResidualPolicyNode::OrElse {
334 fallback_policy,
335 primary,
336 fallback,
337 ..
338 } => {
339 if fallback_policy.carries_obligation() {
340 return Ok(primary);
341 }
342
343 Ok(match fallback {
344 Some(fallback) => SqlxLowered {
345 filter: SqlxFragment::binary(
346 " OR ",
347 vec![primary.filter.clone(), fallback.filter],
348 ),
349 grade: case_when(primary.filter, primary.grade, fallback.grade),
350 },
351 None => primary,
352 })
353 }
354 }
355 }
356}
357
358impl<O, B, P, M> QueryLowering<O> for SqlxLowerer<B, P, M>
359where
360 B: GatekeepSqlxBackend,
361 P: SqlxFactPredicates<B>,
362 M: OutcomeProjection<B, O>,
363{
364 type Filter = SqlxFragment<B>;
365 type Projection = SqlxFragment<B>;
366
367 fn lower(
368 &self,
369 residual: &ResidualPolicy<O>,
370 cx: &Context,
371 ) -> Result<Lowered<Self::Filter, Self::Projection>, LowerError> {
372 let lowered = self.lower_policy(residual, cx)?;
373 Ok(Lowered {
374 filter: lowered.filter,
375 grade: lowered.grade,
376 })
377 }
378}
379
380fn lower_condition_set<B>(
381 conditions: &[Condition],
382 separator: &str,
383 empty: &str,
384 lower: impl FnMut(&Condition) -> Result<SqlxFragment<B>, LowerError>,
385) -> Result<SqlxFragment<B>, LowerError> {
386 if conditions.is_empty() {
387 return Ok(SqlxFragment::trusted(empty));
388 }
389 let fragments = conditions
390 .iter()
391 .map(lower)
392 .collect::<Result<Vec<_>, _>>()?;
393 Ok(SqlxFragment::binary(separator, fragments))
394}
395
396fn fragment_set<B>(
397 fragments: Vec<SqlxFragment<B>>,
398 separator: &str,
399 empty: &str,
400) -> SqlxFragment<B> {
401 if fragments.is_empty() {
402 SqlxFragment::trusted(empty)
403 } else {
404 SqlxFragment::binary(separator, fragments)
405 }
406}
407
408fn grade_set<B>(grades: Vec<SqlxFragment<B>>, function: &str) -> SqlxFragment<B>
409where
410 B: GatekeepSqlxBackend,
411{
412 match grades.len() {
413 0 => SqlxFragment::trusted("NULL"),
414 1 => grades
415 .into_iter()
416 .next()
417 .unwrap_or_else(|| SqlxFragment::trusted("NULL")),
418 _ if B::GRADE_FUNCTION_PROPAGATES_NULL => {
419 let mut iter = grades.into_iter();
420 let mut combined = iter.next().unwrap_or_else(|| SqlxFragment::trusted("NULL"));
421 for grade in iter {
422 combined = null_safe_grade_pair(function, combined, grade);
423 }
424 combined
425 }
426 _ => SqlxFragment::function(function, grades),
427 }
428}
429
430fn null_safe_grade_pair<B>(
431 function: &str,
432 left: SqlxFragment<B>,
433 right: SqlxFragment<B>,
434) -> SqlxFragment<B> {
435 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
436 fragment.push_fragment(left.clone().wrapped());
437 fragment.push_sql(" IS NULL THEN ");
438 fragment.push_fragment(right.clone());
439 fragment.push_sql(" WHEN ");
440 fragment.push_fragment(right.clone().wrapped());
441 fragment.push_sql(" IS NULL THEN ");
442 fragment.push_fragment(left.clone());
443 fragment.push_sql(" ELSE ");
444 fragment.push_fragment(SqlxFragment::function(function, vec![left, right]));
445 fragment.push_sql(" END");
446 fragment
447}
448
449fn unzip_lowered<B>(lowered: Vec<SqlxLowered<B>>) -> (Vec<SqlxFragment<B>>, Vec<SqlxFragment<B>>) {
450 lowered
451 .into_iter()
452 .map(|lowered| (lowered.filter, lowered.grade))
453 .unzip()
454}
455
456fn case_when<B>(
457 condition: SqlxFragment<B>,
458 then_expr: SqlxFragment<B>,
459 else_expr: SqlxFragment<B>,
460) -> SqlxFragment<B> {
461 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
462 fragment.push_fragment(condition);
463 fragment.push_sql(" THEN ");
464 fragment.push_fragment(then_expr);
465 fragment.push_sql(" ELSE ");
466 fragment.push_fragment(else_expr);
467 fragment.push_sql(" END");
468 fragment
469}
470
471fn is_true<B>(predicate: SqlxFragment<B>) -> SqlxFragment<B> {
472 let mut fragment = SqlxFragment::trusted("(");
473 fragment.push_fragment(predicate);
474 fragment.push_sql(") IS TRUE");
475 fragment
476}