modkit_db/secure/provider.rs
1use sea_orm::{ColumnTrait, Condition, EntityTrait, sea_query::Expr};
2
3use crate::secure::{AccessScope, ScopableEntity};
4
5/// Provides tenant filtering logic for scoped queries.
6///
7/// This trait abstracts the tenant filtering mechanism, allowing for future
8/// enhancements like hierarchical tenant structures ("effective tenants")
9/// without changing calling code.
10///
11/// # Current Implementation
12///
13/// `SimpleTenantFilter` uses direct `tenant_id IN (...)` filtering.
14///
15/// # Future Enhancement
16///
17/// A `HierarchicalTenantFilter` could query "effective tenant IDs" from
18/// a tenant hierarchy service and expand the filter accordingly.
19pub trait TenantFilterProvider {
20 /// Build a condition for tenant filtering based on the scope.
21 ///
22 /// Returns:
23 /// - `None` if no tenant filtering needed (empty `tenant_ids`)
24 /// - `Some(deny_all)` if entity has no tenant column but tenants requested
25 /// - `Some(filter)` with appropriate tenant IN clause
26 fn tenant_condition<E>(scope: &AccessScope) -> Option<Condition>
27 where
28 E: ScopableEntity + EntityTrait,
29 E::Column: ColumnTrait + Copy;
30}
31
32/// Simple tenant filter using direct IN clause.
33///
34/// This is the v1 implementation that filters by:
35/// `tenant_id IN (scope.tenant_ids)`
36///
37/// # Future
38///
39/// Can be replaced with a hierarchical provider that expands
40/// `tenant_ids` to include child tenants.
41pub struct SimpleTenantFilter;
42
43impl TenantFilterProvider for SimpleTenantFilter {
44 fn tenant_condition<E>(scope: &AccessScope) -> Option<Condition>
45 where
46 E: ScopableEntity + EntityTrait,
47 E::Column: ColumnTrait + Copy,
48 {
49 // No tenant IDs in scope → no tenant filter
50 if scope.tenant_ids().is_empty() {
51 return None;
52 }
53
54 // Entity has no tenant column but tenant IDs requested → deny all
55 let Some(tcol) = E::tenant_col() else {
56 return Some(Condition::all().add(Expr::value(false)));
57 };
58
59 // Build tenant IN filter
60 Some(Condition::all().add(Expr::col(tcol).is_in(scope.tenant_ids().to_vec())))
61 }
62}
63
64#[cfg(test)]
65#[cfg_attr(coverage_nightly, coverage(off))]
66mod tests {
67 use super::*;
68
69 // Note: Full integration tests with SeaORM entities should be written in actual
70 // application code where real entities are defined. These are basic unit tests
71 // for the provider trait pattern.
72
73 #[test]
74 fn test_provider_trait_compiles() {
75 // This test verifies the provider trait compiles correctly
76 // The actual tenant filtering is tested in integration tests with real entities
77 let scope = AccessScope::default();
78 assert!(scope.is_empty());
79 }
80}