Files
inbuxa-server/crates/common/src/storage/quota.rs
T
jcoffey-dev ad0db8b2b0 Multi-tenancy: the inbuxa-features crate, the permission ceiling and tenant disk quota (MT-12, MT-13, MT-14, MT-15, MT-16, MT-19, MT-20)
New crate crates/features (inbuxa-features), AGPL-3.0-only, holding the
tenancy rules. Hooks in common: a tenant's roles and permission lists cap
its people's permissions; a change to a tenant, or to a role a tenant holds,
drops its members' cached permissions; delivery and every other write check
the tenant's maxDiskQuota; usedDiskQuota reads the tenant usage counter.
The default Tenant Administrator role gains sysTenantGet and sysTenantQuery.
2026-09-18 15:19:58 -07:00

115 lines
3.3 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Server,
auth::AccountCache,
storage::{ObjectQuota, TenantQuota},
};
use registry::{
schema::enums::{StorageQuota, TenantStorageQuota},
types::EnumImpl,
};
use store::{ValueKey, write::ValueClass};
use trc::AddContext;
impl Server {
pub async fn get_used_quota_account(&self, account_id: u32) -> trc::Result<i64> {
self.core
.storage
.data
.get_counter(ValueKey {
account_id,
collection: 0,
document_id: 0,
class: ValueClass::Quota,
})
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))
}
// inbuxa: MT-20: storage used by all a tenant's members together
pub async fn get_used_quota_tenant(&self, tenant_id: u32) -> trc::Result<i64> {
inbuxa_features::tenancy::quota::used(&self.core.storage.data, tenant_id).await
}
pub async fn has_available_quota(
&self,
account: &AccountCache,
item_size: u64,
) -> trc::Result<()> {
if account.quota_disk != 0 {
let used_quota = self.get_used_quota_account(account.id).await?.max(0) as u64;
if used_quota + item_size > account.quota_disk {
return Err(trc::LimitEvent::Quota
.into_err()
.ctx(trc::Key::Limit, account.quota_disk)
.ctx(trc::Key::Size, used_quota));
}
}
// inbuxa: MT-19: the tenant's limit applies too, whichever is reached first
if let Some(tenant_id) = account.id_tenant {
let tenant = self.tenant(tenant_id).await?;
if tenant.quota_disk != 0 {
let used_quota = self.get_used_quota_tenant(tenant_id).await?.max(0) as u64;
if used_quota + item_size > tenant.quota_disk {
return Err(trc::LimitEvent::TenantQuota
.into_err()
.ctx(trc::Key::Id, tenant_id)
.ctx(trc::Key::Limit, tenant.quota_disk)
.ctx(trc::Key::Size, used_quota));
}
}
}
Ok(())
}
#[inline(always)]
pub fn object_quota(&self, user_quotas: Option<&ObjectQuota>, object: StorageQuota) -> u32 {
user_quotas.unwrap_or(&self.core.email.max_objects).0[object as usize]
}
}
impl ObjectQuota {
#[inline(always)]
pub fn set(&mut self, item: StorageQuota, max: u32) {
self.0[item as usize] = max;
}
#[inline(always)]
pub fn get(&self, item: StorageQuota) -> u32 {
self.0[item as usize]
}
}
impl TenantQuota {
#[inline(always)]
pub fn set(&mut self, item: TenantStorageQuota, max: u32) {
self.0[item as usize] = max;
}
#[inline(always)]
pub fn get(&self, item: TenantStorageQuota) -> u32 {
self.0[item as usize]
}
}
impl Default for ObjectQuota {
fn default() -> Self {
Self([u32::MAX; StorageQuota::COUNT - 1])
}
}
impl Default for TenantQuota {
fn default() -> Self {
Self([u32::MAX; TenantStorageQuota::COUNT - 1])
}
}