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, LegacyDecisionAuditDecodeError, decode_decision_audit,
37 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 pub fn lower_filter<O>(
190 &self,
191 residual: &ResidualPolicy<O>,
192 cx: &Context,
193 ) -> Result<SqlxFragment<B>, LowerError>
194 where
195 P: SqlxFactPredicates<B>,
196 {
197 let policy = residual.try_fold_pruned(
198 &mut |branch| match branch {
199 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
200 !fallback.carries_obligation()
201 }
202 },
203 &mut |node| self.lower_filter_node(node, cx),
204 )?;
205 Ok(self.enforce_tenant_filter(policy, cx))
206 }
207
208 fn lower_filter_node<O>(
209 &self,
210 node: ResidualPolicyNode<'_, O, SqlxFragment<B>>,
211 cx: &Context,
212 ) -> Result<SqlxFragment<B>, LowerError>
213 where
214 P: SqlxFactPredicates<B>,
215 {
216 match node {
217 ResidualPolicyNode::Permit(_) | ResidualPolicyNode::PermitWithTrace { .. } => {
218 Ok(SqlxFragment::trusted("TRUE"))
219 }
220 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
221 Ok(SqlxFragment::trusted("FALSE"))
222 }
223 ResidualPolicyNode::Grant { condition, .. } => self.lower_condition(condition, cx),
224 ResidualPolicyNode::All { arms, .. } => Ok(fragment_set(arms, " AND ", "FALSE")),
225 ResidualPolicyNode::Any { arms, .. } => Ok(fragment_set(arms, " OR ", "FALSE")),
226 ResidualPolicyNode::OrElse {
227 fallback_policy,
228 primary,
229 fallback,
230 ..
231 } => {
232 if fallback_policy.carries_obligation() {
233 Ok(primary)
234 } else {
235 Ok(match fallback {
236 Some(fallback) => SqlxFragment::binary(" OR ", vec![primary, fallback]),
237 None => primary,
238 })
239 }
240 }
241 }
242 }
243
244 fn lower_condition(
245 &self,
246 condition: &Condition,
247 cx: &Context,
248 ) -> Result<SqlxFragment<B>, LowerError>
249 where
250 P: SqlxFactPredicates<B>,
251 {
252 match condition {
253 Condition::Always => Ok(SqlxFragment::trusted("TRUE")),
254 Condition::Never => Ok(SqlxFragment::trusted("FALSE")),
255 Condition::Has(fact) => self
256 .predicates
257 .predicate(fact, cx)
258 .map(is_true)
259 .ok_or_else(|| LowerError::Unlowerable(fact.clone())),
260 Condition::Not(inner) => Ok(SqlxFragment::unary(
261 "NOT ",
262 self.lower_condition(inner, cx)?,
263 )),
264 Condition::All(conditions) => {
265 lower_condition_set(conditions, " AND ", "FALSE", |item| {
266 self.lower_condition(item, cx)
267 })
268 }
269 Condition::Any(conditions) => {
270 lower_condition_set(conditions, " OR ", "FALSE", |item| {
271 self.lower_condition(item, cx)
272 })
273 }
274 }
275 }
276
277 #[must_use]
284 pub fn enforce_tenant_filter(&self, policy: SqlxFragment<B>, cx: &Context) -> SqlxFragment<B> {
285 let mut tenant = SqlxFragment::trusted(self.tenant_column.qualified());
286 tenant.push_sql(" = ");
287 tenant.push_fragment(SqlxFragment::bind(cx.tenant().as_str()));
288 SqlxFragment::binary(" AND ", [tenant, policy])
289 }
290
291 #[must_use]
296 pub fn enforce_tenant_projection(
297 &self,
298 grade: SqlxFragment<B>,
299 cx: &Context,
300 ) -> SqlxFragment<B> {
301 let tenant = self.enforce_tenant_filter(SqlxFragment::trusted("TRUE"), cx);
302 case_when(tenant, grade, SqlxFragment::trusted("NULL"))
303 }
304
305 fn lower_policy<O>(
306 &self,
307 residual: &ResidualPolicy<O>,
308 cx: &Context,
309 ) -> Result<SqlxLowered<B>, LowerError>
310 where
311 P: SqlxFactPredicates<B>,
312 M: OutcomeProjection<B, O>,
313 {
314 residual.try_fold_pruned(
315 &mut |branch| match branch {
316 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
317 !fallback.carries_obligation()
318 }
319 },
320 &mut |node| self.lower_node(node, cx),
321 )
322 }
323
324 fn lower_node<O>(
325 &self,
326 node: ResidualPolicyNode<'_, O, SqlxLowered<B>>,
327 cx: &Context,
328 ) -> Result<SqlxLowered<B>, LowerError>
329 where
330 P: SqlxFactPredicates<B>,
331 M: OutcomeProjection<B, O>,
332 {
333 match node {
334 ResidualPolicyNode::Permit(outcome)
335 | ResidualPolicyNode::PermitWithTrace { outcome, .. } => Ok(SqlxLowered {
336 filter: SqlxFragment::trusted("TRUE"),
337 grade: self.projection.constant(outcome)?,
338 }),
339 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
340 Ok(SqlxLowered {
341 filter: SqlxFragment::trusted("FALSE"),
342 grade: SqlxFragment::trusted("NULL"),
343 })
344 }
345 ResidualPolicyNode::Grant {
346 outcome, condition, ..
347 } => {
348 let filter = self.lower_condition(condition, cx)?;
349 let outcome = self.projection.constant(outcome)?;
350 Ok(SqlxLowered {
351 filter: filter.clone(),
352 grade: case_when(filter, outcome, SqlxFragment::trusted("NULL")),
353 })
354 }
355 ResidualPolicyNode::All { arms, .. } => {
356 let (filters, grades) = unzip_lowered(arms);
357 Ok(SqlxLowered {
358 filter: fragment_set(filters, " AND ", "FALSE"),
359 grade: grade_set::<B>(grades, B::MIN_FUNCTION),
360 })
361 }
362 ResidualPolicyNode::Any { arms, .. } => {
363 let (filters, grades) = unzip_lowered(arms);
364 Ok(SqlxLowered {
365 filter: fragment_set(filters, " OR ", "FALSE"),
366 grade: grade_set::<B>(grades, B::MAX_FUNCTION),
367 })
368 }
369 ResidualPolicyNode::OrElse {
370 fallback_policy,
371 primary,
372 fallback,
373 ..
374 } => {
375 if fallback_policy.carries_obligation() {
376 return Ok(primary);
377 }
378
379 Ok(match fallback {
380 Some(fallback) => SqlxLowered {
381 filter: SqlxFragment::binary(
382 " OR ",
383 vec![primary.filter.clone(), fallback.filter],
384 ),
385 grade: case_when(primary.filter, primary.grade, fallback.grade),
386 },
387 None => primary,
388 })
389 }
390 }
391 }
392}
393
394impl<O, B, P, M> QueryLowering<O> for SqlxLowerer<B, P, M>
395where
396 B: GatekeepSqlxBackend,
397 P: SqlxFactPredicates<B>,
398 M: OutcomeProjection<B, O>,
399{
400 type Filter = SqlxFragment<B>;
401 type Projection = SqlxFragment<B>;
402
403 fn lower(
404 &self,
405 residual: &ResidualPolicy<O>,
406 cx: &Context,
407 ) -> Result<Lowered<Self::Filter, Self::Projection>, LowerError> {
408 let mut lowered = self.lower_policy(residual, cx)?;
409 lowered.filter = self.enforce_tenant_filter(lowered.filter, cx);
410 lowered.grade = self.enforce_tenant_projection(lowered.grade, cx);
411 Ok(Lowered {
412 filter: lowered.filter,
413 grade: lowered.grade,
414 })
415 }
416}
417
418fn lower_condition_set<B>(
419 conditions: &[Condition],
420 separator: &str,
421 empty: &str,
422 lower: impl FnMut(&Condition) -> Result<SqlxFragment<B>, LowerError>,
423) -> Result<SqlxFragment<B>, LowerError> {
424 if conditions.is_empty() {
425 return Ok(SqlxFragment::trusted(empty));
426 }
427
428 let fragments = conditions
429 .iter()
430 .map(lower)
431 .collect::<Result<Vec<_>, _>>()?;
432 Ok(SqlxFragment::binary(separator, fragments))
433}
434
435fn fragment_set<B>(
436 fragments: Vec<SqlxFragment<B>>,
437 separator: &str,
438 empty: &str,
439) -> SqlxFragment<B> {
440 if fragments.is_empty() {
441 SqlxFragment::trusted(empty)
442 } else {
443 SqlxFragment::binary(separator, fragments)
444 }
445}
446
447fn grade_set<B>(grades: Vec<SqlxFragment<B>>, function: &str) -> SqlxFragment<B>
448where
449 B: GatekeepSqlxBackend,
450{
451 match grades.len() {
452 0 => SqlxFragment::trusted("NULL"),
453 1 => grades
454 .into_iter()
455 .next()
456 .unwrap_or_else(|| SqlxFragment::trusted("NULL")),
457 _ if B::GRADE_FUNCTION_PROPAGATES_NULL => {
458 let mut iter = grades.into_iter();
459 let mut combined = iter.next().unwrap_or_else(|| SqlxFragment::trusted("NULL"));
460 for grade in iter {
461 combined = null_safe_grade_pair(function, combined, grade);
462 }
463 combined
464 }
465 _ => SqlxFragment::function(function, grades),
466 }
467}
468
469fn null_safe_grade_pair<B>(
470 function: &str,
471 left: SqlxFragment<B>,
472 right: SqlxFragment<B>,
473) -> SqlxFragment<B> {
474 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
475 fragment.push_fragment(left.clone().wrapped());
476 fragment.push_sql(" IS NULL THEN ");
477 fragment.push_fragment(right.clone());
478 fragment.push_sql(" WHEN ");
479 fragment.push_fragment(right.clone().wrapped());
480 fragment.push_sql(" IS NULL THEN ");
481 fragment.push_fragment(left.clone());
482 fragment.push_sql(" ELSE ");
483 fragment.push_fragment(SqlxFragment::function(function, vec![left, right]));
484 fragment.push_sql(" END");
485 fragment
486}
487
488fn unzip_lowered<B>(lowered: Vec<SqlxLowered<B>>) -> (Vec<SqlxFragment<B>>, Vec<SqlxFragment<B>>) {
489 lowered
490 .into_iter()
491 .map(|lowered| (lowered.filter, lowered.grade))
492 .unzip()
493}
494
495fn case_when<B>(
496 condition: SqlxFragment<B>,
497 then_expr: SqlxFragment<B>,
498 else_expr: SqlxFragment<B>,
499) -> SqlxFragment<B> {
500 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
501 fragment.push_fragment(condition);
502 fragment.push_sql(" THEN ");
503 fragment.push_fragment(then_expr);
504 fragment.push_sql(" ELSE ");
505 fragment.push_fragment(else_expr);
506 fragment.push_sql(" END");
507 fragment
508}
509
510fn is_true<B>(predicate: SqlxFragment<B>) -> SqlxFragment<B> {
511 let mut fragment = SqlxFragment::trusted("(");
512 fragment.push_fragment(predicate);
513 fragment.push_sql(") IS TRUE");
514 fragment
515}