use std::collections::HashMap;
use std::sync::RwLock;
use serde::{Deserialize, Serialize};
use tracing::info;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantCeiling {
pub tenant_id: u32,
pub max_collections: u64,
pub max_storage_bytes: u64,
pub audit_min_retention_days: u32,
}
pub struct CeilingStore {
ceilings: RwLock<HashMap<u32, TenantCeiling>>,
}
impl CeilingStore {
pub fn new() -> Self {
Self {
ceilings: RwLock::new(HashMap::new()),
}
}
pub fn define(&self, ceiling: TenantCeiling) {
let tid = ceiling.tenant_id;
let mut ceilings = self.ceilings.write().unwrap_or_else(|p| p.into_inner());
info!(
tenant_id = tid,
max_collections = ceiling.max_collections,
max_storage_bytes = ceiling.max_storage_bytes,
audit_min_retention_days = ceiling.audit_min_retention_days,
"ceiling defined"
);
ceilings.insert(tid, ceiling);
}
pub fn get(&self, tenant_id: u32) -> Option<TenantCeiling> {
let ceilings = self.ceilings.read().unwrap_or_else(|p| p.into_inner());
ceilings.get(&tenant_id).cloned()
}
pub fn check_collection_limit(&self, tenant_id: u32, current_count: u64) -> crate::Result<()> {
let ceilings = self.ceilings.read().unwrap_or_else(|p| p.into_inner());
if let Some(c) = ceilings.get(&tenant_id)
&& c.max_collections > 0
&& current_count >= c.max_collections
{
return Err(crate::Error::RejectedAuthz {
tenant_id: crate::types::TenantId::new(tenant_id),
resource: format!("ceiling exceeded: max_collections = {}", c.max_collections),
});
}
Ok(())
}
pub fn check_storage_limit(
&self,
tenant_id: u32,
current_bytes: u64,
additional_bytes: u64,
) -> crate::Result<()> {
let ceilings = self.ceilings.read().unwrap_or_else(|p| p.into_inner());
if let Some(c) = ceilings.get(&tenant_id)
&& c.max_storage_bytes > 0
&& current_bytes + additional_bytes > c.max_storage_bytes
{
return Err(crate::Error::RejectedAuthz {
tenant_id: crate::types::TenantId::new(tenant_id),
resource: format!(
"ceiling exceeded: max_storage = {} bytes",
c.max_storage_bytes
),
});
}
Ok(())
}
pub fn is_audit_protected(&self, tenant_id: u32, entry_age_days: u32) -> bool {
let ceilings = self.ceilings.read().unwrap_or_else(|p| p.into_inner());
if let Some(c) = ceilings.get(&tenant_id) {
return entry_age_days < c.audit_min_retention_days;
}
false
}
pub fn is_audit_deletion_forbidden() -> bool {
true
}
pub fn list(&self) -> Vec<TenantCeiling> {
let ceilings = self.ceilings.read().unwrap_or_else(|p| p.into_inner());
ceilings.values().cloned().collect()
}
}
impl Default for CeilingStore {
fn default() -> Self {
Self::new()
}
}
pub fn parse_storage_size(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(n) = s.strip_suffix("TiB") {
n.trim()
.parse::<u64>()
.ok()
.map(|n| n * 1024 * 1024 * 1024 * 1024)
} else if let Some(n) = s.strip_suffix("GiB") {
n.trim().parse::<u64>().ok().map(|n| n * 1024 * 1024 * 1024)
} else if let Some(n) = s.strip_suffix("MiB") {
n.trim().parse::<u64>().ok().map(|n| n * 1024 * 1024)
} else {
s.parse::<u64>().ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ceiling_enforcement() {
let store = CeilingStore::new();
store.define(TenantCeiling {
tenant_id: 1,
max_collections: 10,
max_storage_bytes: 1024 * 1024 * 1024,
audit_min_retention_days: 365,
});
assert!(store.check_collection_limit(1, 5).is_ok());
assert!(store.check_collection_limit(1, 10).is_err());
assert!(store.check_collection_limit(2, 999).is_ok()); }
#[test]
fn audit_always_forbidden() {
assert!(CeilingStore::is_audit_deletion_forbidden());
}
#[test]
fn parse_sizes() {
assert_eq!(parse_storage_size("1TiB"), Some(1024 * 1024 * 1024 * 1024));
assert_eq!(parse_storage_size("100GiB"), Some(100 * 1024 * 1024 * 1024));
assert_eq!(parse_storage_size("500MiB"), Some(500 * 1024 * 1024));
}
}