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, MAX_TENANT_IDENTIFIER_BYTES, SqlxDriver, SqlxDriverError, SqlxFragment,
50 SqlxValue, TenantColumn, TenantColumnError, TenantIdentifierPart,
51 infer_enabled_driver_from_url, validate_database_url_for_backend,
52};
53#[cfg(feature = "postgres")]
54pub use fragment::{PgFragment, PgValue, PostgresBackend};
55
56pub trait SqlxFactPredicates<B>
58where
59 B: GatekeepSqlxBackend,
60{
61 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<B>>;
64}
65
66#[cfg(feature = "postgres")]
68pub trait PgFactPredicates {
69 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<PgFragment>;
72}
73
74#[cfg(feature = "postgres")]
75impl<T> SqlxFactPredicates<PostgresBackend> for T
76where
77 T: PgFactPredicates,
78{
79 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<PostgresBackend>> {
80 PgFactPredicates::predicate(self, fact, cx)
81 }
82}
83
84pub trait SqlOutcome {
86 fn to_sql_ordinal(&self) -> i64;
88}
89
90impl SqlOutcome for () {
91 fn to_sql_ordinal(&self) -> i64 {
92 0
93 }
94}
95
96pub trait OutcomeProjection<B, O>
98where
99 B: GatekeepSqlxBackend,
100{
101 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError>;
108}
109
110#[derive(Clone, Copy, Debug, Default)]
112pub struct OrdinalProjection;
113
114impl<B, O> OutcomeProjection<B, O> for OrdinalProjection
115where
116 B: GatekeepSqlxBackend,
117 O: SqlOutcome,
118{
119 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
120 Ok(SqlxFragment::bind(outcome.to_sql_ordinal()))
121 }
122}
123
124#[derive(Clone, Copy, Debug, Default)]
126pub struct NoGradeProjection;
127
128impl<B, O> OutcomeProjection<B, O> for NoGradeProjection
129where
130 B: GatekeepSqlxBackend,
131{
132 fn constant(&self, _outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
133 Err(LowerError::NonTotalGrade)
134 }
135}
136
137#[derive(Clone, Debug)]
139pub struct SqlxLowerer<B, P, M = OrdinalProjection> {
140 predicates: P,
141 projection: M,
142 tenant_column: TenantColumn,
143 backend: PhantomData<fn() -> B>,
144}
145
146#[cfg(feature = "postgres")]
148pub type PgLowerer<P, M = OrdinalProjection> = SqlxLowerer<PostgresBackend, P, M>;
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151struct SqlxLowered<B> {
152 filter: SqlxFragment<B>,
153 grade: SqlxFragment<B>,
154}
155
156impl<B, P> SqlxLowerer<B, P, OrdinalProjection>
157where
158 B: GatekeepSqlxBackend,
159{
160 #[must_use]
162 pub fn new(predicates: P, tenant_column: TenantColumn) -> Self {
163 Self::with_projection(predicates, OrdinalProjection, tenant_column)
164 }
165}
166
167impl<B, P, M> SqlxLowerer<B, P, M>
168where
169 B: GatekeepSqlxBackend,
170{
171 #[must_use]
173 pub fn with_projection(predicates: P, projection: M, tenant_column: TenantColumn) -> Self {
174 Self {
175 predicates,
176 projection,
177 tenant_column,
178 backend: PhantomData,
179 }
180 }
181
182 pub fn lower_filter<O>(
189 &self,
190 residual: &ResidualPolicy<O>,
191 cx: &Context,
192 ) -> Result<SqlxFragment<B>, LowerError>
193 where
194 P: SqlxFactPredicates<B>,
195 {
196 let policy = residual.try_fold_pruned(
197 &mut |branch| match branch {
198 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
199 !fallback.carries_obligation()
200 }
201 },
202 &mut |node| self.lower_filter_node(node, cx),
203 )?;
204 Ok(self.enforce_tenant_filter(policy, cx))
205 }
206
207 fn lower_filter_node<O>(
208 &self,
209 node: ResidualPolicyNode<'_, O, SqlxFragment<B>>,
210 cx: &Context,
211 ) -> Result<SqlxFragment<B>, LowerError>
212 where
213 P: SqlxFactPredicates<B>,
214 {
215 match node {
216 ResidualPolicyNode::Permit(_) | ResidualPolicyNode::PermitWithTrace { .. } => {
217 Ok(SqlxFragment::trusted("TRUE"))
218 }
219 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
220 Ok(SqlxFragment::trusted("FALSE"))
221 }
222 ResidualPolicyNode::Grant { condition, .. } => self.lower_condition(condition, cx),
223 ResidualPolicyNode::All { arms, .. } => Ok(fragment_set(arms, " AND ", "FALSE")),
224 ResidualPolicyNode::Any { arms, .. } => Ok(fragment_set(arms, " OR ", "FALSE")),
225 ResidualPolicyNode::OrElse {
226 fallback_policy,
227 primary,
228 fallback,
229 ..
230 } => {
231 if fallback_policy.carries_obligation() {
232 Ok(primary)
233 } else {
234 Ok(match fallback {
235 Some(fallback) => SqlxFragment::binary(" OR ", vec![primary, fallback]),
236 None => primary,
237 })
238 }
239 }
240 }
241 }
242
243 fn lower_condition(
244 &self,
245 condition: &Condition,
246 cx: &Context,
247 ) -> Result<SqlxFragment<B>, LowerError>
248 where
249 P: SqlxFactPredicates<B>,
250 {
251 match condition {
252 Condition::Always => Ok(SqlxFragment::trusted("TRUE")),
253 Condition::Never => Ok(SqlxFragment::trusted("FALSE")),
254 Condition::Has(fact) => self
255 .predicates
256 .predicate(fact, cx)
257 .map(is_true)
258 .ok_or_else(|| LowerError::Unlowerable(fact.clone())),
259 Condition::Not(inner) => Ok(SqlxFragment::unary(
260 "NOT ",
261 self.lower_condition(inner, cx)?,
262 )),
263 Condition::All(conditions) => {
264 lower_condition_set(conditions, " AND ", "FALSE", |item| {
265 self.lower_condition(item, cx)
266 })
267 }
268 Condition::Any(conditions) => {
269 lower_condition_set(conditions, " OR ", "FALSE", |item| {
270 self.lower_condition(item, cx)
271 })
272 }
273 }
274 }
275
276 #[must_use]
283 pub fn enforce_tenant_filter(&self, policy: SqlxFragment<B>, cx: &Context) -> SqlxFragment<B> {
284 let mut tenant = SqlxFragment::trusted(self.tenant_column.qualified());
285 tenant.push_sql(" = ");
286 tenant.push_fragment(SqlxFragment::bind(cx.tenant().as_str()));
287 SqlxFragment::binary(" AND ", [tenant, policy])
288 }
289
290 #[must_use]
295 pub fn enforce_tenant_projection(
296 &self,
297 grade: SqlxFragment<B>,
298 cx: &Context,
299 ) -> SqlxFragment<B> {
300 let tenant = self.enforce_tenant_filter(SqlxFragment::trusted("TRUE"), cx);
301 case_when(tenant, grade, SqlxFragment::trusted("NULL"))
302 }
303
304 fn lower_policy<O>(
305 &self,
306 residual: &ResidualPolicy<O>,
307 cx: &Context,
308 ) -> Result<SqlxLowered<B>, LowerError>
309 where
310 P: SqlxFactPredicates<B>,
311 M: OutcomeProjection<B, O>,
312 {
313 residual.try_fold_pruned(
314 &mut |branch| match branch {
315 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
316 !fallback.carries_obligation()
317 }
318 },
319 &mut |node| self.lower_node(node, cx),
320 )
321 }
322
323 fn lower_node<O>(
324 &self,
325 node: ResidualPolicyNode<'_, O, SqlxLowered<B>>,
326 cx: &Context,
327 ) -> Result<SqlxLowered<B>, LowerError>
328 where
329 P: SqlxFactPredicates<B>,
330 M: OutcomeProjection<B, O>,
331 {
332 match node {
333 ResidualPolicyNode::Permit(outcome)
334 | ResidualPolicyNode::PermitWithTrace { outcome, .. } => Ok(SqlxLowered {
335 filter: SqlxFragment::trusted("TRUE"),
336 grade: self.projection.constant(outcome)?,
337 }),
338 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
339 Ok(SqlxLowered {
340 filter: SqlxFragment::trusted("FALSE"),
341 grade: SqlxFragment::trusted("NULL"),
342 })
343 }
344 ResidualPolicyNode::Grant {
345 outcome, condition, ..
346 } => {
347 let filter = self.lower_condition(condition, cx)?;
348 let outcome = self.projection.constant(outcome)?;
349 Ok(SqlxLowered {
350 filter: filter.clone(),
351 grade: case_when(filter, outcome, SqlxFragment::trusted("NULL")),
352 })
353 }
354 ResidualPolicyNode::All { arms, .. } => {
355 let (filters, grades) = unzip_lowered(arms);
356 Ok(SqlxLowered {
357 filter: fragment_set(filters, " AND ", "FALSE"),
358 grade: grade_set::<B>(grades, B::MIN_FUNCTION),
359 })
360 }
361 ResidualPolicyNode::Any { arms, .. } => {
362 let (filters, grades) = unzip_lowered(arms);
363 Ok(SqlxLowered {
364 filter: fragment_set(filters, " OR ", "FALSE"),
365 grade: grade_set::<B>(grades, B::MAX_FUNCTION),
366 })
367 }
368 ResidualPolicyNode::OrElse {
369 fallback_policy,
370 primary,
371 fallback,
372 ..
373 } => {
374 if fallback_policy.carries_obligation() {
375 return Ok(primary);
376 }
377
378 Ok(match fallback {
379 Some(fallback) => SqlxLowered {
380 filter: SqlxFragment::binary(
381 " OR ",
382 vec![primary.filter.clone(), fallback.filter],
383 ),
384 grade: case_when(primary.filter, primary.grade, fallback.grade),
385 },
386 None => primary,
387 })
388 }
389 }
390 }
391}
392
393impl<O, B, P, M> QueryLowering<O> for SqlxLowerer<B, P, M>
394where
395 B: GatekeepSqlxBackend,
396 P: SqlxFactPredicates<B>,
397 M: OutcomeProjection<B, O>,
398{
399 type Filter = SqlxFragment<B>;
400 type Projection = SqlxFragment<B>;
401
402 fn lower(
403 &self,
404 residual: &ResidualPolicy<O>,
405 cx: &Context,
406 ) -> Result<Lowered<Self::Filter, Self::Projection>, LowerError> {
407 let mut lowered = self.lower_policy(residual, cx)?;
408 lowered.filter = self.enforce_tenant_filter(lowered.filter, cx);
409 lowered.grade = self.enforce_tenant_projection(lowered.grade, cx);
410 Ok(Lowered {
411 filter: lowered.filter,
412 grade: lowered.grade,
413 })
414 }
415}
416
417fn lower_condition_set<B>(
418 conditions: &[Condition],
419 separator: &str,
420 empty: &str,
421 lower: impl FnMut(&Condition) -> Result<SqlxFragment<B>, LowerError>,
422) -> Result<SqlxFragment<B>, LowerError> {
423 if conditions.is_empty() {
424 return Ok(SqlxFragment::trusted(empty));
425 }
426
427 let fragments = conditions
428 .iter()
429 .map(lower)
430 .collect::<Result<Vec<_>, _>>()?;
431 Ok(SqlxFragment::binary(separator, fragments))
432}
433
434fn fragment_set<B>(
435 fragments: Vec<SqlxFragment<B>>,
436 separator: &str,
437 empty: &str,
438) -> SqlxFragment<B> {
439 if fragments.is_empty() {
440 SqlxFragment::trusted(empty)
441 } else {
442 SqlxFragment::binary(separator, fragments)
443 }
444}
445
446fn grade_set<B>(grades: Vec<SqlxFragment<B>>, function: &str) -> SqlxFragment<B>
447where
448 B: GatekeepSqlxBackend,
449{
450 match grades.len() {
451 0 => SqlxFragment::trusted("NULL"),
452 1 => grades
453 .into_iter()
454 .next()
455 .unwrap_or_else(|| SqlxFragment::trusted("NULL")),
456 _ if B::GRADE_FUNCTION_PROPAGATES_NULL => {
457 let mut iter = grades.into_iter();
458 let mut combined = iter.next().unwrap_or_else(|| SqlxFragment::trusted("NULL"));
459 for grade in iter {
460 combined = null_safe_grade_pair(function, combined, grade);
461 }
462 combined
463 }
464 _ => SqlxFragment::function(function, grades),
465 }
466}
467
468fn null_safe_grade_pair<B>(
469 function: &str,
470 left: SqlxFragment<B>,
471 right: SqlxFragment<B>,
472) -> SqlxFragment<B> {
473 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
474 fragment.push_fragment(left.clone().wrapped());
475 fragment.push_sql(" IS NULL THEN ");
476 fragment.push_fragment(right.clone());
477 fragment.push_sql(" WHEN ");
478 fragment.push_fragment(right.clone().wrapped());
479 fragment.push_sql(" IS NULL THEN ");
480 fragment.push_fragment(left.clone());
481 fragment.push_sql(" ELSE ");
482 fragment.push_fragment(SqlxFragment::function(function, vec![left, right]));
483 fragment.push_sql(" END");
484 fragment
485}
486
487fn unzip_lowered<B>(lowered: Vec<SqlxLowered<B>>) -> (Vec<SqlxFragment<B>>, Vec<SqlxFragment<B>>) {
488 lowered
489 .into_iter()
490 .map(|lowered| (lowered.filter, lowered.grade))
491 .unzip()
492}
493
494fn case_when<B>(
495 condition: SqlxFragment<B>,
496 then_expr: SqlxFragment<B>,
497 else_expr: SqlxFragment<B>,
498) -> SqlxFragment<B> {
499 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
500 fragment.push_fragment(condition);
501 fragment.push_sql(" THEN ");
502 fragment.push_fragment(then_expr);
503 fragment.push_sql(" ELSE ");
504 fragment.push_fragment(else_expr);
505 fragment.push_sql(" END");
506 fragment
507}
508
509fn is_true<B>(predicate: SqlxFragment<B>) -> SqlxFragment<B> {
510 let mut fragment = SqlxFragment::trusted("(");
511 fragment.push_fragment(predicate);
512 fragment.push_sql(") IS TRUE");
513 fragment
514}