Skip to main content

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}