use std::sync::Arc;
use super::id::TenantId;
use super::permissions::{Operation, TenantPermissions};
use crate::error::{TenantError, ValidationError};
#[derive(Debug, Clone)]
pub struct TenantContext {
tenant_id: TenantId,
permissions: Arc<TenantPermissions>,
correlation_id: Option<String>,
user_id: Option<String>,
}
impl TenantContext {
pub fn new(tenant_id: TenantId, permissions: TenantPermissions) -> Self {
Self {
tenant_id,
permissions: Arc::new(permissions),
correlation_id: None,
user_id: None,
}
}
pub fn system() -> Self {
Self::new(TenantId::system(), TenantPermissions::full_access())
}
pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
self.correlation_id = Some(correlation_id.into());
self
}
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn tenant_id(&self) -> &TenantId {
&self.tenant_id
}
pub fn permissions(&self) -> &TenantPermissions {
&self.permissions
}
pub fn correlation_id(&self) -> Option<&str> {
self.correlation_id.as_deref()
}
pub fn user_id(&self) -> Option<&str> {
self.user_id.as_deref()
}
pub fn is_system(&self) -> bool {
self.tenant_id.is_system()
}
pub fn check_permission(
&self,
operation: Operation,
resource_type: &str,
) -> Result<(), TenantError> {
if self.permissions.can_perform(operation, resource_type) {
Ok(())
} else {
Err(TenantError::OperationNotPermitted {
tenant_id: self.tenant_id.clone(),
operation: operation.to_string(),
})
}
}
pub fn check_access(&self, resource_tenant: &TenantId) -> Result<(), TenantError> {
if &self.tenant_id == resource_tenant {
return Ok(());
}
if resource_tenant.is_system() && self.permissions.can_access_system_tenant() {
return Ok(());
}
if self.permissions.can_access_child_tenants()
&& resource_tenant.is_descendant_of(&self.tenant_id)
{
return Ok(());
}
Err(TenantError::AccessDenied {
tenant_id: self.tenant_id.clone(),
resource_type: "unknown".to_string(),
resource_id: "unknown".to_string(),
})
}
pub fn validate_reference(
&self,
reference: &str,
target_tenant: &TenantId,
) -> Result<(), TenantError> {
if &self.tenant_id == target_tenant {
return Ok(());
}
if target_tenant.is_system() && self.permissions.can_access_system_tenant() {
return Ok(());
}
Err(TenantError::CrossTenantReference {
source_tenant: self.tenant_id.clone(),
target_tenant: target_tenant.clone(),
reference: reference.to_string(),
})
}
}
pub struct TenantContextBuilder {
tenant_id: Option<TenantId>,
permissions: Option<TenantPermissions>,
correlation_id: Option<String>,
user_id: Option<String>,
}
impl TenantContextBuilder {
pub fn new() -> Self {
Self {
tenant_id: None,
permissions: None,
correlation_id: None,
user_id: None,
}
}
pub fn tenant_id(mut self, tenant_id: TenantId) -> Self {
self.tenant_id = Some(tenant_id);
self
}
pub fn tenant_id_str(mut self, tenant_id: &str) -> Self {
self.tenant_id = Some(TenantId::new(tenant_id));
self
}
pub fn permissions(mut self, permissions: TenantPermissions) -> Self {
self.permissions = Some(permissions);
self
}
pub fn correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
self.correlation_id = Some(correlation_id.into());
self
}
pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn build(self) -> Result<TenantContext, ValidationError> {
let tenant_id = self
.tenant_id
.ok_or_else(|| ValidationError::MissingRequiredField {
field: "tenant_id".to_string(),
})?;
let permissions = self
.permissions
.unwrap_or_else(TenantPermissions::full_access);
let mut ctx = TenantContext::new(tenant_id, permissions);
ctx.correlation_id = self.correlation_id;
ctx.user_id = self.user_id;
Ok(ctx)
}
}
impl Default for TenantContextBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenant_context_creation() {
let ctx = TenantContext::new(TenantId::new("my-tenant"), TenantPermissions::full_access());
assert_eq!(ctx.tenant_id().as_str(), "my-tenant");
assert!(!ctx.is_system());
}
#[test]
fn test_system_context() {
let ctx = TenantContext::system();
assert!(ctx.is_system());
}
#[test]
fn test_with_correlation_id() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access())
.with_correlation_id("req-123");
assert_eq!(ctx.correlation_id(), Some("req-123"));
}
#[test]
fn test_with_user_id() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access())
.with_user_id("user-456");
assert_eq!(ctx.user_id(), Some("user-456"));
}
#[test]
fn test_check_permission_allowed() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access());
assert!(ctx.check_permission(Operation::Create, "Patient").is_ok());
}
#[test]
fn test_check_permission_denied() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::read_only());
let result = ctx.check_permission(Operation::Create, "Patient");
assert!(result.is_err());
}
#[test]
fn test_check_access_same_tenant() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access());
assert!(ctx.check_access(&TenantId::new("t1")).is_ok());
}
#[test]
fn test_check_access_different_tenant() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access());
assert!(ctx.check_access(&TenantId::new("t2")).is_err());
}
#[test]
fn test_check_access_system_tenant() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access());
assert!(ctx.check_access(&TenantId::system()).is_ok());
}
#[test]
fn test_check_access_child_tenant() {
let perms = TenantPermissions::builder()
.can_access_child_tenants(true)
.build();
let ctx = TenantContext::new(TenantId::new("parent"), perms);
assert!(ctx.check_access(&TenantId::new("parent/child")).is_ok());
}
#[test]
fn test_validate_reference_same_tenant() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access());
assert!(
ctx.validate_reference("Patient/123", &TenantId::new("t1"))
.is_ok()
);
}
#[test]
fn test_validate_reference_cross_tenant() {
let ctx = TenantContext::new(TenantId::new("t1"), TenantPermissions::full_access());
let result = ctx.validate_reference("Patient/123", &TenantId::new("t2"));
assert!(result.is_err());
}
#[test]
fn test_builder() {
let ctx = TenantContextBuilder::new()
.tenant_id_str("my-tenant")
.permissions(TenantPermissions::read_only())
.correlation_id("corr-123")
.user_id("user-456")
.build()
.unwrap();
assert_eq!(ctx.tenant_id().as_str(), "my-tenant");
assert_eq!(ctx.correlation_id(), Some("corr-123"));
assert_eq!(ctx.user_id(), Some("user-456"));
}
#[test]
fn test_builder_missing_tenant_id() {
let result = TenantContextBuilder::new()
.permissions(TenantPermissions::full_access())
.build();
assert!(result.is_err());
}
}