fix: cap default storage quota to available disk space (#92)

Previously the admin quota was hardcoded to 100 GB and the regular
user quota to 1 GB, regardless of actual disk capacity. On systems
with less than 100 GB free this produced misleading quota values.

Added the fs2 crate to query available disk space on the storage
filesystem. A new capped_quota() helper now returns
min(default_quota, available_disk_space) when assigning quotas
during user registration, admin setup, and OIDC provisioning.
This commit is contained in:
Dionisio
2026-02-13 21:58:54 +01:00
parent 177f82ca16
commit 40bf43b292
4 changed files with 91 additions and 13 deletions
Generated
+33
View File
@@ -771,6 +771,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures"
version = "0.3.31"
@@ -1698,6 +1708,7 @@ dependencies = [
"chrono",
"dotenv",
"flate2",
"fs2",
"futures",
"hex",
"http-body",
@@ -3292,6 +3303,28 @@ dependencies = [
"wasite",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
+1
View File
@@ -47,6 +47,7 @@ hex = "0.4.3"
http-body-util = "0.1.3"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots"] }
base64 = "0.22.1"
fs2 = "0.4"
[features]
default = []
@@ -3,6 +3,7 @@ use std::sync::RwLock;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
use std::path::PathBuf;
use crate::domain::entities::user::{User, UserRole};
use crate::domain::entities::session::Session;
use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort, PasswordHasherPort, TokenServicePort, OidcServicePort, OidcIdClaims};
@@ -36,12 +37,18 @@ struct OidcState {
config: Option<OidcConfig>,
}
/// Default quota: 100 GB
const DEFAULT_ADMIN_QUOTA: i64 = 107_374_182_400;
const DEFAULT_USER_QUOTA: i64 = 1_073_741_824; // 1 GB
pub struct AuthApplicationService {
user_storage: Arc<dyn UserStoragePort>,
session_storage: Arc<dyn SessionStoragePort>,
password_hasher: Arc<dyn PasswordHasherPort>,
token_service: Arc<dyn TokenServicePort>,
folder_service: Option<Arc<dyn FolderUseCase>>,
/// Path to the storage directory, used for disk-space–aware quota calculation
storage_path: PathBuf,
oidc: RwLock<OidcState>,
/// Pending OIDC authorization flows keyed by state token (CSRF + PKCE + nonce)
pending_oidc_flows: Mutex<HashMap<String, PendingOidcFlow>>,
@@ -55,6 +62,7 @@ impl AuthApplicationService {
session_storage: Arc<dyn SessionStoragePort>,
password_hasher: Arc<dyn PasswordHasherPort>,
token_service: Arc<dyn TokenServicePort>,
storage_path: PathBuf,
) -> Self {
Self {
user_storage,
@@ -62,11 +70,54 @@ impl AuthApplicationService {
password_hasher,
token_service,
folder_service: None,
storage_path,
oidc: RwLock::new(OidcState { service: None, config: None }),
pending_oidc_flows: Mutex::new(HashMap::new()),
pending_oidc_tokens: Mutex::new(HashMap::new()),
}
}
/// Returns the default quota for the given role, capped to the available
/// disk space on the filesystem that hosts the storage directory.
fn capped_quota(&self, role: &UserRole) -> i64 {
let base_quota = match role {
UserRole::Admin => DEFAULT_ADMIN_QUOTA,
_ => DEFAULT_USER_QUOTA,
};
match Self::available_disk_space(&self.storage_path) {
Some(avail) => {
let avail_i64 = avail as i64;
if avail_i64 < base_quota {
tracing::info!(
"Available disk space ({} bytes) is less than default {} quota ({} bytes) — capping quota",
avail_i64,
if *role == UserRole::Admin { "admin" } else { "user" },
base_quota,
);
avail_i64
} else {
base_quota
}
}
None => {
tracing::warn!("Could not determine available disk space, using default quota");
base_quota
}
}
}
/// Query the available space on the filesystem that contains `path`.
fn available_disk_space(path: &std::path::Path) -> Option<u64> {
use fs2::available_space;
match available_space(path) {
Ok(space) => Some(space),
Err(e) => {
tracing::warn!("Failed to query disk space for {:?}: {}", path, e);
None
}
}
}
/// Configures the folder service, needed to create personal folders
pub fn with_folder_service(mut self, folder_service: Arc<dyn FolderUseCase>) -> Self {
@@ -200,12 +251,8 @@ impl AuthApplicationService {
}
};
// Quota based on role: 100GB for admin, 1GB for regular users
let quota = if role == UserRole::Admin {
107374182400 // 100GB for admin
} else {
1024 * 1024 * 1024 // 1GB for regular users
};
// Quota based on role, capped to available disk space
let quota = self.capped_quota(&role);
// Validate password length before hashing
if dto.password.len() < 8 {
@@ -544,8 +591,8 @@ impl AuthApplicationService {
// 3. Create new admin user with the provided credentials but admin role
let admin_role = UserRole::Admin;
// Use 100GB for admin quota
let admin_quota = 107374182400;
// Admin quota, capped to available disk space
let admin_quota = self.capped_quota(&admin_role);
// Create the new admin user
let user = User::new(
@@ -937,11 +984,7 @@ impl AuthApplicationService {
// Determine role from OIDC groups
let role = self.map_oidc_role(&claims.groups, &oidc_config);
let quota = if role == UserRole::Admin {
107374182400 // 100GB
} else {
1024 * 1024 * 1024 // 1GB
};
let quota = self.capped_quota(&role);
// Sanitize username (max 32 chars, ensure uniqueness)
let mut username = oidc_username.chars().take(32).collect::<String>();
+1
View File
@@ -37,6 +37,7 @@ pub async fn create_auth_services(
session_repository,
password_hasher,
token_service.clone(),
config.storage_path.clone(),
);
// Configure folder service if available