Skip to main content

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}