use std::fmt;
use super::identity::{IdentityContext, TenantId};
pub trait TenantBoundary {
fn tenant_id(&self) -> &TenantId;
}
impl TenantBoundary for TenantId {
fn tenant_id(&self) -> &TenantId {
self
}
}
impl TenantBoundary for IdentityContext {
fn tenant_id(&self) -> &TenantId {
&self.tenant
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CrossTenantLeak {
pub expected: TenantId,
pub actual: TenantId,
}
impl CrossTenantLeak {
#[must_use]
pub fn new(expected: TenantId, actual: TenantId) -> Self {
Self { expected, actual }
}
}
impl fmt::Display for CrossTenantLeak {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"tenant boundary violation: expected {}, found {}",
self.expected, self.actual
)
}
}
impl std::error::Error for CrossTenantLeak {}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct IsolationCheck;
impl IsolationCheck {
pub fn same_tenant<A: TenantBoundary + ?Sized, B: TenantBoundary + ?Sized>(
left: &A,
right: &B,
) -> Result<(), CrossTenantLeak> {
assert_same_tenant(left, right)
}
pub fn all_same_tenant<T: TenantBoundary>(values: &[T]) -> Result<(), CrossTenantLeak> {
let Some(first) = values.first() else {
return Ok(());
};
for value in values.iter().skip(1) {
assert_same_tenant(first, value)?;
}
Ok(())
}
pub fn verify<A: TenantBoundary + ?Sized, B: TenantBoundary + ?Sized>(
left: &A,
right: &B,
) -> Result<(), CrossTenantLeak> {
Self::same_tenant(left, right)
}
}
pub fn assert_same_tenant<A: TenantBoundary + ?Sized, B: TenantBoundary + ?Sized>(
left: &A,
right: &B,
) -> Result<(), CrossTenantLeak> {
if left.tenant_id() == right.tenant_id() {
Ok(())
} else {
Err(CrossTenantLeak::new(
left.tenant_id().clone(),
right.tenant_id().clone(),
))
}
}