lean_ctx/core/trust/
tenant_isolation.rs1use std::fmt;
4
5use super::identity::{IdentityContext, TenantId};
6
7pub trait TenantBoundary {
9 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#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct CrossTenantLeak {
29 pub expected: TenantId,
31 pub actual: TenantId,
33}
34
35impl CrossTenantLeak {
36 #[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
57pub struct IsolationCheck;
58
59impl IsolationCheck {
60 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 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 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
88pub 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}