use std::env;
use std::fs;
use std::path::{Component, Path, PathBuf};
use crate::error::CorpFinanceError;
use crate::CorpFinanceResult;
use super::types::{Tenant, TenantContext};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceKind {
Output,
Memory,
CostLedger,
Session,
Audit,
}
impl ResourceKind {
fn dir_name(self) -> &'static str {
match self {
ResourceKind::Output => "out",
ResourceKind::Memory => "memory",
ResourceKind::CostLedger => "cost-ledger",
ResourceKind::Session => "session",
ResourceKind::Audit => "audit",
}
}
}
pub fn provision_tenant(tenant: &Tenant, base_dir: &Path) -> CorpFinanceResult<TenantContext> {
if tenant.tenant_id.is_empty() {
return Err(CorpFinanceError::InvalidInput {
field: "tenant_id".to_string(),
reason: "must be non-empty".to_string(),
});
}
if !is_valid_tenant_id(&tenant.tenant_id) {
return Err(CorpFinanceError::InvalidInput {
field: "tenant_id".to_string(),
reason: "must be lowercase kebab-case (a-z, 0-9, '-')".to_string(),
});
}
let tenant_root = base_dir.join(&tenant.tenant_id);
fs::create_dir_all(&tenant_root).map_err(|e| CorpFinanceError::InvalidInput {
field: "base_dir".to_string(),
reason: format!(
"could not create tenant root {}: {}",
tenant_root.display(),
e
),
})?;
set_posix_0700(&tenant_root)?;
for kind in [
ResourceKind::Output,
ResourceKind::Memory,
ResourceKind::CostLedger,
ResourceKind::Session,
ResourceKind::Audit,
] {
let sub = tenant_root.join(kind.dir_name());
fs::create_dir_all(&sub).map_err(|e| CorpFinanceError::InvalidInput {
field: "resource_dir".to_string(),
reason: format!("could not create {}: {}", sub.display(), e),
})?;
}
Ok(TenantContext {
tenant_id: tenant.tenant_id.clone(),
output_root: tenant_root,
env_namespace: tenant.env_namespace.clone(),
})
}
fn is_valid_tenant_id(id: &str) -> bool {
!id.is_empty()
&& id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& !id.starts_with('-')
&& !id.ends_with('-')
}
#[cfg(unix)]
fn set_posix_0700(p: &Path) -> CorpFinanceResult<()> {
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o700);
fs::set_permissions(p, perms).map_err(|e| CorpFinanceError::InvalidInput {
field: "permissions".to_string(),
reason: format!("could not set 0700 on {}: {}", p.display(), e),
})
}
#[cfg(not(unix))]
fn set_posix_0700(_p: &Path) -> CorpFinanceResult<()> {
Ok(())
}
pub fn resolve_tenant_for_cli(args: &[String]) -> Option<TenantContext> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--tenant" {
if let Some(id) = iter.next() {
return Some(stub_context(id));
}
} else if let Some(rest) = arg.strip_prefix("--tenant=") {
return Some(stub_context(rest));
}
}
env::var("CFA_TENANT_ID").ok().map(|id| stub_context(&id))
}
pub fn resolve_tenant_for_mcp(metadata: &serde_json::Value) -> Option<TenantContext> {
metadata
.get("_meta")
.and_then(|m| m.get("tenant_id"))
.and_then(|v| v.as_str())
.map(stub_context)
}
pub fn resolve_tenant_for_plugin() -> Option<TenantContext> {
if let Ok(id) = env::var("CFA_TENANT_ID") {
return Some(stub_context(&id));
}
if let Ok(cwd) = env::current_dir() {
let marker = cwd.join(".cfa-tenant");
if marker.is_file() {
if let Ok(contents) = fs::read_to_string(&marker) {
let id = contents.trim();
if !id.is_empty() {
return Some(stub_context(id));
}
}
}
}
None
}
fn stub_context(id: &str) -> TenantContext {
TenantContext {
tenant_id: id.to_string(),
output_root: PathBuf::from("var/tenants").join(id).join("out"),
env_namespace: format!("TENANT_{}_", id.to_ascii_uppercase().replace('-', "")),
}
}
pub fn tenant_scoped_path(ctx: &TenantContext, kind: ResourceKind, name: &str) -> PathBuf {
ctx.output_root.join(kind.dir_name()).join(name)
}
pub fn enforce_isolation(ctx: &TenantContext, attempted_path: &Path) -> CorpFinanceResult<()> {
let root = &ctx.output_root;
for comp in attempted_path.components() {
if matches!(comp, Component::ParentDir) {
return Err(CorpFinanceError::InvalidInput {
field: "attempted_path".to_string(),
reason: format!(
"path traversal denied: '{}' contains '..' (tenant '{}')",
attempted_path.display(),
ctx.tenant_id
),
});
}
}
if attempted_path.is_absolute() && !attempted_path.starts_with(root) {
return Err(CorpFinanceError::InvalidInput {
field: "attempted_path".to_string(),
reason: format!(
"tenant isolation: '{}' is outside output_root '{}' (tenant '{}')",
attempted_path.display(),
root.display(),
ctx.tenant_id
),
});
}
Ok(())
}