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