Skip to main content

lean_ctx/core/trust/
tenant_isolation.rs

1//! Small, composable tenant-boundary checks.
2
3use std::fmt;
4
5use super::identity::{IdentityContext, TenantId};
6
7/// Marker/access trait implemented by values that carry a tenant boundary.
8pub trait TenantBoundary {
9    /// Returns the tenant that owns the value.
10    fn tenant_id(&self) -> &TenantId;
11}
12
13impl TenantBoundary for TenantId {
14    fn tenant_id(&self) -> &TenantId {
15        self
16    }
17}
18
19impl TenantBoundary for IdentityContext {
20    fn tenant_id(&self) -> &TenantId {
21        &self.tenant
22    }
23}
24
25/// Error returned when two tenant-scoped values would be joined across a
26/// boundary.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct CrossTenantLeak {
29    /// Tenant selected by the left/owning value.
30    pub expected: TenantId,
31    /// Tenant carried by the right/referenced value.
32    pub actual: TenantId,
33}
34
35impl CrossTenantLeak {
36    /// Creates a cross-tenant error from the two observed tenants.
37    #[must_use]
38    pub fn new(expected: TenantId, actual: TenantId) -> Self {
39        Self { expected, actual }
40    }
41}
42
43impl fmt::Display for CrossTenantLeak {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(
46            formatter,
47            "tenant boundary violation: expected {}, found {}",
48            self.expected, self.actual
49        )
50    }
51}
52
53impl std::error::Error for CrossTenantLeak {}
54
55/// Stateless isolation helper for pair and collection checks.
56#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
57pub struct IsolationCheck;
58
59impl IsolationCheck {
60    /// Checks that two tenant-scoped values belong to the same tenant.
61    pub fn same_tenant<A: TenantBoundary + ?Sized, B: TenantBoundary + ?Sized>(
62        left: &A,
63        right: &B,
64    ) -> Result<(), CrossTenantLeak> {
65        assert_same_tenant(left, right)
66    }
67
68    /// Checks that every value in a collection belongs to its first tenant.
69    pub fn all_same_tenant<T: TenantBoundary>(values: &[T]) -> Result<(), CrossTenantLeak> {
70        let Some(first) = values.first() else {
71            return Ok(());
72        };
73        for value in values.iter().skip(1) {
74            assert_same_tenant(first, value)?;
75        }
76        Ok(())
77    }
78
79    /// Alias suitable for call sites that describe the operation as a check.
80    pub fn verify<A: TenantBoundary + ?Sized, B: TenantBoundary + ?Sized>(
81        left: &A,
82        right: &B,
83    ) -> Result<(), CrossTenantLeak> {
84        Self::same_tenant(left, right)
85    }
86}
87
88/// Verifies that two values share exactly the same tenant identifier.
89pub fn assert_same_tenant<A: TenantBoundary + ?Sized, B: TenantBoundary + ?Sized>(
90    left: &A,
91    right: &B,
92) -> Result<(), CrossTenantLeak> {
93    if left.tenant_id() == right.tenant_id() {
94        Ok(())
95    } else {
96        Err(CrossTenantLeak::new(
97            left.tenant_id().clone(),
98            right.tenant_id().clone(),
99        ))
100    }
101}