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