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 ATTEMPT_AUDIT_EVENT_TYPE, AttemptAuditEventError, DECISION_AUDIT_CONTENT_TYPE,
35 DECISION_AUDIT_EVENT_TYPE, DEFAULT_AUDIT_STREAM, DecisionAuditConfig, DecisionAuditConfigError,
36 DecisionAuditDecodeError, DecisionAuditEventError, LegacyDecisionAuditDecodeError,
37 decode_authorization_attempt, decode_decision_audit, decode_legacy_decision_audit,
38};
39#[cfg(feature = "mysql")]
40pub use audit::{MySqlDovecoteAudit, MySqlDovecoteAuditError};
41#[cfg(feature = "postgres")]
42pub use audit::{PgDovecoteAudit, PgDovecoteAuditError};
43#[cfg(feature = "sqlite")]
44pub use audit::{SqliteDovecoteAudit, SqliteDovecoteAuditError};
45#[cfg(feature = "mysql")]
46pub use fragment::MySqlBackend;
47#[cfg(feature = "sqlite")]
48pub use fragment::SqliteBackend;
49pub use fragment::{
50 GatekeepSqlxBackend, MAX_TENANT_IDENTIFIER_BYTES, SqlxDriver, SqlxDriverError, SqlxFragment,
51 SqlxValue, TenantColumn, TenantColumnError, TenantIdentifierPart,
52 infer_enabled_driver_from_url, validate_database_url_for_backend,
53};
54#[cfg(feature = "postgres")]
55pub use fragment::{PgFragment, PgValue, PostgresBackend};
56
57pub trait SqlxFactPredicates<B>
59where
60 B: GatekeepSqlxBackend,
61{
62 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<B>>;
65}
66
67#[cfg(feature = "postgres")]
69pub trait PgFactPredicates {
70 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<PgFragment>;
73}
74
75#[cfg(feature = "postgres")]
76impl<T> SqlxFactPredicates<PostgresBackend> for T
77where
78 T: PgFactPredicates,
79{
80 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<PostgresBackend>> {
81 PgFactPredicates::predicate(self, fact, cx)
82 }
83}
84
85pub trait SqlOutcome {
87 fn to_sql_ordinal(&self) -> i64;
89}
90
91impl SqlOutcome for () {
92 fn to_sql_ordinal(&self) -> i64 {
93 0
94 }
95}
96
97pub trait OutcomeProjection<B, O>
99where
100 B: GatekeepSqlxBackend,
101{
102 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError>;
109}
110
111#[derive(Clone, Copy, Debug, Default)]
113pub struct OrdinalProjection;
114
115impl<B, O> OutcomeProjection<B, O> for OrdinalProjection
116where
117 B: GatekeepSqlxBackend,
118 O: SqlOutcome,
119{
120 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
121 Ok(SqlxFragment::bind(outcome.to_sql_ordinal()))
122 }
123}
124
125#[derive(Clone, Copy, Debug, Default)]
127pub struct NoGradeProjection;
128
129impl<B, O> OutcomeProjection<B, O> for NoGradeProjection
130where
131 B: GatekeepSqlxBackend,
132{
133 fn constant(&self, _outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
134 Err(LowerError::NonTotalGrade)
135 }
136}
137
138#[derive(Clone, Debug)]
140pub struct SqlxLowerer<B, P, M = OrdinalProjection> {
141 predicates: P,
142 projection: M,
143 tenant_column: TenantColumn,
144 backend: PhantomData<fn() -> B>,
145}
146
147#[cfg(feature = "postgres")]
149pub type PgLowerer<P, M = OrdinalProjection> = SqlxLowerer<PostgresBackend, P, M>;
150
151#[derive(Clone, Debug, PartialEq, Eq)]
152struct SqlxLowered<B> {
153 filter: SqlxFragment<B>,
154 grade: SqlxFragment<B>,
155}
156
157impl<B, P> SqlxLowerer<B, P, OrdinalProjection>
158where
159 B: GatekeepSqlxBackend,
160{
161 #[must_use]
163 pub fn new(predicates: P, tenant_column: TenantColumn) -> Self {
164 Self::with_projection(predicates, OrdinalProjection, tenant_column)
165 }
166}
167
168impl<B, P, M> SqlxLowerer<B, P, M>
169where
170 B: GatekeepSqlxBackend,
171{
172 #[must_use]
174 pub fn with_projection(predicates: P, projection: M, tenant_column: TenantColumn) -> Self {
175 Self {
176 predicates,
177 projection,
178 tenant_column,
179 backend: PhantomData,
180 }
181 }
182
183 #[must_use]
186 pub fn unmapped_facts<O>(&self, residual: &ResidualPolicy<O>, cx: &Context) -> Vec<FactId>
187 where
188 P: SqlxFactPredicates<B>,
189 {
190 gatekeep::required_residual_facts(residual)
191 .into_iter()
192 .filter(|fact| self.predicates.predicate(fact, cx).is_none())
193 .collect()
194 }
195
196 pub fn lower_result<O>(
205 &self,
206 result: &gatekeep::Residual<O>,
207 cx: &Context,
208 ) -> Result<Lowered<SqlxFragment<B>, SqlxFragment<B>>, LowerError>
209 where
210 P: SqlxFactPredicates<B>,
211 M: OutcomeProjection<B, O>,
212 {
213 match result {
214 gatekeep::Residual::Pending { residual, .. } => self.lower(residual, cx),
215 gatekeep::Residual::Resolved(decision) => {
216 let (filter, grade) = match &decision.effect {
217 gatekeep::Effect::Permit(outcome) => (
218 SqlxFragment::trusted("TRUE"),
219 self.projection.constant(outcome)?,
220 ),
221 gatekeep::Effect::Deny => (
222 SqlxFragment::trusted("FALSE"),
223 SqlxFragment::trusted("NULL"),
224 ),
225 };
226 Ok(Lowered {
227 filter: self.enforce_tenant_filter(filter, cx),
228 grade: self.enforce_tenant_projection(grade, cx),
229 })
230 }
231 }
232 }
233
234 pub fn lower_filter<O>(
241 &self,
242 residual: &ResidualPolicy<O>,
243 cx: &Context,
244 ) -> Result<SqlxFragment<B>, LowerError>
245 where
246 P: SqlxFactPredicates<B>,
247 {
248 let policy = residual.try_fold_pruned(
249 &mut |branch| match branch {
250 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
251 !fallback.carries_obligation()
252 }
253 },
254 &mut |node| self.lower_filter_node(node, cx),
255 )?;
256 Ok(self.enforce_tenant_filter(policy, cx))
257 }
258
259 fn lower_filter_node<O>(
260 &self,
261 node: ResidualPolicyNode<'_, O, SqlxFragment<B>>,
262 cx: &Context,
263 ) -> Result<SqlxFragment<B>, LowerError>
264 where
265 P: SqlxFactPredicates<B>,
266 {
267 match node {
268 ResidualPolicyNode::Permit(_) | ResidualPolicyNode::PermitWithTrace { .. } => {
269 Ok(SqlxFragment::trusted("TRUE"))
270 }
271 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
272 Ok(SqlxFragment::trusted("FALSE"))
273 }
274 ResidualPolicyNode::Grant { condition, .. } => self.lower_condition(condition, cx),
275 ResidualPolicyNode::All { arms, .. } => Ok(fragment_set(arms, " AND ", "FALSE")),
276 ResidualPolicyNode::Any { arms, .. } => Ok(fragment_set(arms, " OR ", "FALSE")),
277 ResidualPolicyNode::OrElse {
278 fallback_policy,
279 primary,
280 fallback,
281 ..
282 } => {
283 if fallback_policy.carries_obligation() {
284 Ok(primary)
285 } else {
286 Ok(match fallback {
287 Some(fallback) => SqlxFragment::binary(" OR ", vec![primary, fallback]),
288 None => primary,
289 })
290 }
291 }
292 }
293 }
294
295 fn lower_condition(
296 &self,
297 condition: &Condition,
298 cx: &Context,
299 ) -> Result<SqlxFragment<B>, LowerError>
300 where
301 P: SqlxFactPredicates<B>,
302 {
303 match condition {
304 Condition::Always => Ok(SqlxFragment::trusted("TRUE")),
305 Condition::Never => Ok(SqlxFragment::trusted("FALSE")),
306 Condition::Has(fact) => self
307 .predicates
308 .predicate(fact, cx)
309 .map(is_true)
310 .ok_or_else(|| LowerError::Unlowerable(fact.clone())),
311 Condition::Not(inner) => Ok(SqlxFragment::unary(
312 "NOT ",
313 self.lower_condition(inner, cx)?,
314 )),
315 Condition::All(conditions) => {
316 lower_condition_set(conditions, " AND ", "FALSE", |item| {
317 self.lower_condition(item, cx)
318 })
319 }
320 Condition::Any(conditions) => {
321 lower_condition_set(conditions, " OR ", "FALSE", |item| {
322 self.lower_condition(item, cx)
323 })
324 }
325 }
326 }
327
328 #[must_use]
335 pub fn enforce_tenant_filter(&self, policy: SqlxFragment<B>, cx: &Context) -> SqlxFragment<B> {
336 let mut tenant = SqlxFragment::trusted(self.tenant_column.qualified());
337 tenant.push_sql(" = ");
338 tenant.push_fragment(SqlxFragment::bind(cx.tenant().as_str()));
339 SqlxFragment::binary(" AND ", [tenant, policy])
340 }
341
342 #[must_use]
347 pub fn enforce_tenant_projection(
348 &self,
349 grade: SqlxFragment<B>,
350 cx: &Context,
351 ) -> SqlxFragment<B> {
352 let tenant = self.enforce_tenant_filter(SqlxFragment::trusted("TRUE"), cx);
353 case_when(tenant, grade, SqlxFragment::trusted("NULL"))
354 }
355
356 fn lower_policy<O>(
357 &self,
358 residual: &ResidualPolicy<O>,
359 cx: &Context,
360 ) -> Result<SqlxLowered<B>, LowerError>
361 where
362 P: SqlxFactPredicates<B>,
363 M: OutcomeProjection<B, O>,
364 {
365 residual.try_fold_pruned(
366 &mut |branch| match branch {
367 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
368 !fallback.carries_obligation()
369 }
370 },
371 &mut |node| self.lower_node(node, cx),
372 )
373 }
374
375 fn lower_node<O>(
376 &self,
377 node: ResidualPolicyNode<'_, O, SqlxLowered<B>>,
378 cx: &Context,
379 ) -> Result<SqlxLowered<B>, LowerError>
380 where
381 P: SqlxFactPredicates<B>,
382 M: OutcomeProjection<B, O>,
383 {
384 match node {
385 ResidualPolicyNode::Permit(outcome)
386 | ResidualPolicyNode::PermitWithTrace { outcome, .. } => Ok(SqlxLowered {
387 filter: SqlxFragment::trusted("TRUE"),
388 grade: self.projection.constant(outcome)?,
389 }),
390 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
391 Ok(SqlxLowered {
392 filter: SqlxFragment::trusted("FALSE"),
393 grade: SqlxFragment::trusted("NULL"),
394 })
395 }
396 ResidualPolicyNode::Grant {
397 outcome, condition, ..
398 } => {
399 let filter = self.lower_condition(condition, cx)?;
400 let outcome = self.projection.constant(outcome)?;
401 Ok(SqlxLowered {
402 filter: filter.clone(),
403 grade: case_when(filter, outcome, SqlxFragment::trusted("NULL")),
404 })
405 }
406 ResidualPolicyNode::All { arms, .. } => {
407 let (filters, grades) = unzip_lowered(arms);
408 Ok(SqlxLowered {
409 filter: fragment_set(filters, " AND ", "FALSE"),
410 grade: grade_set::<B>(grades, B::MIN_FUNCTION),
411 })
412 }
413 ResidualPolicyNode::Any { arms, .. } => {
414 let (filters, grades) = unzip_lowered(arms);
415 Ok(SqlxLowered {
416 filter: fragment_set(filters, " OR ", "FALSE"),
417 grade: grade_set::<B>(grades, B::MAX_FUNCTION),
418 })
419 }
420 ResidualPolicyNode::OrElse {
421 fallback_policy,
422 primary,
423 fallback,
424 ..
425 } => {
426 if fallback_policy.carries_obligation() {
427 return Ok(primary);
428 }
429
430 Ok(match fallback {
431 Some(fallback) => SqlxLowered {
432 filter: SqlxFragment::binary(
433 " OR ",
434 vec![primary.filter.clone(), fallback.filter],
435 ),
436 grade: case_when(primary.filter, primary.grade, fallback.grade),
437 },
438 None => primary,
439 })
440 }
441 }
442 }
443}
444
445impl<O, B, P, M> QueryLowering<O> for SqlxLowerer<B, P, M>
446where
447 B: GatekeepSqlxBackend,
448 P: SqlxFactPredicates<B>,
449 M: OutcomeProjection<B, O>,
450{
451 type Filter = SqlxFragment<B>;
452 type Projection = SqlxFragment<B>;
453
454 fn lower(
455 &self,
456 residual: &ResidualPolicy<O>,
457 cx: &Context,
458 ) -> Result<Lowered<Self::Filter, Self::Projection>, LowerError> {
459 let mut lowered = self.lower_policy(residual, cx)?;
460 lowered.filter = self.enforce_tenant_filter(lowered.filter, cx);
461 lowered.grade = self.enforce_tenant_projection(lowered.grade, cx);
462 Ok(Lowered {
463 filter: lowered.filter,
464 grade: lowered.grade,
465 })
466 }
467}
468
469fn lower_condition_set<B>(
470 conditions: &[Condition],
471 separator: &str,
472 empty: &str,
473 lower: impl FnMut(&Condition) -> Result<SqlxFragment<B>, LowerError>,
474) -> Result<SqlxFragment<B>, LowerError> {
475 if conditions.is_empty() {
476 return Ok(SqlxFragment::trusted(empty));
477 }
478
479 let fragments = conditions
480 .iter()
481 .map(lower)
482 .collect::<Result<Vec<_>, _>>()?;
483 Ok(SqlxFragment::binary(separator, fragments))
484}
485
486fn fragment_set<B>(
487 fragments: Vec<SqlxFragment<B>>,
488 separator: &str,
489 empty: &str,
490) -> SqlxFragment<B> {
491 if fragments.is_empty() {
492 SqlxFragment::trusted(empty)
493 } else {
494 SqlxFragment::binary(separator, fragments)
495 }
496}
497
498fn grade_set<B>(grades: Vec<SqlxFragment<B>>, function: &str) -> SqlxFragment<B>
499where
500 B: GatekeepSqlxBackend,
501{
502 match grades.len() {
503 0 => SqlxFragment::trusted("NULL"),
504 1 => grades
505 .into_iter()
506 .next()
507 .unwrap_or_else(|| SqlxFragment::trusted("NULL")),
508 _ if B::GRADE_FUNCTION_PROPAGATES_NULL => {
509 let mut iter = grades.into_iter();
510 let mut combined = iter.next().unwrap_or_else(|| SqlxFragment::trusted("NULL"));
511 for grade in iter {
512 combined = null_safe_grade_pair(function, combined, grade);
513 }
514 combined
515 }
516 _ => SqlxFragment::function(function, grades),
517 }
518}
519
520fn null_safe_grade_pair<B>(
521 function: &str,
522 left: SqlxFragment<B>,
523 right: SqlxFragment<B>,
524) -> SqlxFragment<B> {
525 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
526 fragment.push_fragment(left.clone().wrapped());
527 fragment.push_sql(" IS NULL THEN ");
528 fragment.push_fragment(right.clone());
529 fragment.push_sql(" WHEN ");
530 fragment.push_fragment(right.clone().wrapped());
531 fragment.push_sql(" IS NULL THEN ");
532 fragment.push_fragment(left.clone());
533 fragment.push_sql(" ELSE ");
534 fragment.push_fragment(SqlxFragment::function(function, vec![left, right]));
535 fragment.push_sql(" END");
536 fragment
537}
538
539fn unzip_lowered<B>(lowered: Vec<SqlxLowered<B>>) -> (Vec<SqlxFragment<B>>, Vec<SqlxFragment<B>>) {
540 lowered
541 .into_iter()
542 .map(|lowered| (lowered.filter, lowered.grade))
543 .unzip()
544}
545
546fn case_when<B>(
547 condition: SqlxFragment<B>,
548 then_expr: SqlxFragment<B>,
549 else_expr: SqlxFragment<B>,
550) -> SqlxFragment<B> {
551 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
552 fragment.push_fragment(condition);
553 fragment.push_sql(" THEN ");
554 fragment.push_fragment(then_expr);
555 fragment.push_sql(" ELSE ");
556 fragment.push_fragment(else_expr);
557 fragment.push_sql(" END");
558 fragment
559}
560
561fn is_true<B>(predicate: SqlxFragment<B>) -> SqlxFragment<B> {
562 let mut fragment = SqlxFragment::trusted("(");
563 fragment.push_fragment(predicate);
564 fragment.push_sql(") IS TRUE");
565 fragment
566}