use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct CompactionConfig {
pub enabled: bool,
pub max_l1_runs: usize,
pub max_l1_size_bytes: u64,
pub max_l1_age: Duration,
pub check_interval: Duration,
pub worker_threads: usize,
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
enabled: true,
max_l1_runs: 4,
max_l1_size_bytes: 256 * 1024 * 1024,
max_l1_age: Duration::from_secs(3600),
check_interval: Duration::from_secs(30),
worker_threads: 1,
}
}
}
#[derive(Clone, Debug)]
pub struct IndexRebuildConfig {
pub max_retries: u32,
pub retry_delay: Duration,
pub worker_check_interval: Duration,
pub growth_trigger_ratio: f64,
pub max_index_age: Option<Duration>,
pub auto_rebuild_enabled: bool,
}
impl Default for IndexRebuildConfig {
fn default() -> Self {
Self {
max_retries: 3,
retry_delay: Duration::from_secs(60),
worker_check_interval: Duration::from_secs(5),
growth_trigger_ratio: 0.5,
max_index_age: None,
auto_rebuild_enabled: false,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct WriteThrottleConfig {
pub soft_limit: usize,
pub hard_limit: usize,
pub base_delay: Duration,
}
impl Default for WriteThrottleConfig {
fn default() -> Self {
Self {
soft_limit: 8,
hard_limit: 16,
base_delay: Duration::from_millis(10),
}
}
}
#[derive(Clone, Debug)]
pub struct ObjectStoreConfig {
pub connect_timeout: Duration,
pub read_timeout: Duration,
pub write_timeout: Duration,
pub max_retries: u32,
pub retry_backoff_base: Duration,
pub retry_backoff_max: Duration,
}
impl Default for ObjectStoreConfig {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(10),
read_timeout: Duration::from_secs(30),
write_timeout: Duration::from_secs(60),
max_retries: 3,
retry_backoff_base: Duration::from_millis(100),
retry_backoff_max: Duration::from_secs(10),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct FileSandboxConfig {
pub enabled: bool,
pub allowed_paths: Vec<PathBuf>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DeploymentMode {
#[default]
Embedded,
Server,
}
#[derive(Clone, Debug)]
pub struct ServerConfig {
pub allowed_origins: Vec<String>,
pub api_key: Option<String>,
pub require_auth_for_metrics: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
allowed_origins: vec!["http://localhost:3000".to_string()],
api_key: None,
require_auth_for_metrics: false,
}
}
}
impl ServerConfig {
#[must_use]
pub fn development() -> Self {
Self {
allowed_origins: vec!["*".to_string()],
api_key: None,
require_auth_for_metrics: false,
}
}
#[must_use]
pub fn production(allowed_origins: Vec<String>, api_key: String) -> Self {
assert!(
!api_key.is_empty(),
"API key must not be empty for production"
);
Self {
allowed_origins,
api_key: Some(api_key),
require_auth_for_metrics: true,
}
}
pub fn security_warning(&self) -> Option<&'static str> {
if self.allowed_origins.contains(&"*".to_string()) && self.api_key.is_none() {
Some(
"Server config has permissive CORS (allow all origins) and no API key. \
This is insecure for production deployments.",
)
} else if self.allowed_origins.contains(&"*".to_string()) {
Some(
"Server config has permissive CORS (allow all origins). \
Consider restricting to specific origins for production.",
)
} else if self.api_key.is_none() {
Some(
"Server config has no API key authentication. \
Enable api_key for production deployments.",
)
} else {
None
}
}
}
impl FileSandboxConfig {
pub fn sandboxed(paths: Vec<PathBuf>) -> Self {
Self {
enabled: true,
allowed_paths: paths,
}
}
pub fn default_for_mode(mode: DeploymentMode) -> Self {
match mode {
DeploymentMode::Embedded => Self {
enabled: false,
allowed_paths: vec![],
},
DeploymentMode::Server => Self {
enabled: true,
allowed_paths: vec![
PathBuf::from("/var/lib/uni/data"),
PathBuf::from("/var/lib/uni/backups"),
],
},
}
}
pub fn security_warning(&self) -> Option<&'static str> {
if !self.enabled {
Some(
"File sandbox is DISABLED. This allows unrestricted filesystem access \
for BACKUP, COPY, and EXPORT commands. Enable sandbox for server \
deployments: file_sandbox.enabled = true",
)
} else {
None
}
}
pub fn is_potentially_insecure(&self) -> bool {
!self.enabled || self.allowed_paths.is_empty()
}
pub fn validate_path(&self, path: &str) -> Result<PathBuf, String> {
if !self.enabled {
return Ok(PathBuf::from(path));
}
if self.allowed_paths.is_empty() {
return Err("File sandbox is enabled but no allowed paths configured".to_string());
}
let input_path = Path::new(path);
let canonical = if input_path.exists() {
input_path
.canonicalize()
.map_err(|e| format!("Failed to canonicalize path: {}", e))?
} else {
let parent = input_path
.parent()
.ok_or_else(|| "Invalid path: no parent directory".to_string())?;
if !parent.exists() {
return Err(format!(
"Parent directory does not exist: {}",
parent.display()
));
}
let canonical_parent = parent
.canonicalize()
.map_err(|e| format!("Failed to canonicalize parent: {}", e))?;
let filename = input_path
.file_name()
.ok_or_else(|| "Invalid path: no filename".to_string())?;
canonical_parent.join(filename)
};
for allowed in &self.allowed_paths {
let canonical_allowed = if allowed.exists() {
allowed.canonicalize().unwrap_or_else(|_| allowed.clone())
} else {
allowed.clone()
};
if canonical.starts_with(&canonical_allowed) {
return Ok(canonical);
}
}
Err(format!(
"Path '{}' is outside allowed sandbox directories. Allowed: {:?}",
path, self.allowed_paths
))
}
}
#[derive(Clone, Debug)]
pub struct UniConfig {
pub cache_size: usize,
pub parallelism: usize,
pub batch_size: usize,
pub max_frontier_size: usize,
pub auto_flush_threshold: usize,
pub auto_flush_interval: Option<Duration>,
pub auto_flush_min_mutations: usize,
pub wal_enabled: bool,
pub compaction: CompactionConfig,
pub throttle: WriteThrottleConfig,
pub file_sandbox: FileSandboxConfig,
pub query_timeout: Duration,
pub max_query_memory: usize,
pub max_transaction_memory: usize,
pub max_compaction_rows: usize,
pub enable_vid_labels_index: bool,
pub max_recursive_cte_iterations: usize,
pub object_store: ObjectStoreConfig,
pub index_rebuild: IndexRebuildConfig,
}
impl Default for UniConfig {
fn default() -> Self {
let parallelism = thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
Self {
cache_size: 1024 * 1024 * 1024, parallelism,
batch_size: 1024, max_frontier_size: 1_000_000,
auto_flush_threshold: 10_000,
auto_flush_interval: Some(Duration::from_secs(5)),
auto_flush_min_mutations: 1,
wal_enabled: true,
compaction: CompactionConfig::default(),
throttle: WriteThrottleConfig::default(),
file_sandbox: FileSandboxConfig::default(),
query_timeout: Duration::from_secs(30),
max_query_memory: 1024 * 1024 * 1024, max_transaction_memory: 1024 * 1024 * 1024, max_compaction_rows: 5_000_000, enable_vid_labels_index: true, max_recursive_cte_iterations: 1000,
object_store: ObjectStoreConfig::default(),
index_rebuild: IndexRebuildConfig::default(),
}
}
}
#[derive(Clone, Debug)]
pub enum CloudStorageConfig {
S3 {
bucket: String,
region: Option<String>,
endpoint: Option<String>,
access_key_id: Option<String>,
secret_access_key: Option<String>,
session_token: Option<String>,
virtual_hosted_style: bool,
},
Gcs {
bucket: String,
service_account_path: Option<String>,
service_account_key: Option<String>,
},
Azure {
container: String,
account: String,
access_key: Option<String>,
sas_token: Option<String>,
},
}
impl CloudStorageConfig {
#[must_use]
pub fn s3_from_env(bucket: &str) -> Self {
Self::S3 {
bucket: bucket.to_string(),
region: std::env::var("AWS_REGION")
.or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
.ok(),
endpoint: std::env::var("AWS_ENDPOINT_URL").ok(),
access_key_id: std::env::var("AWS_ACCESS_KEY_ID").ok(),
secret_access_key: std::env::var("AWS_SECRET_ACCESS_KEY").ok(),
session_token: std::env::var("AWS_SESSION_TOKEN").ok(),
virtual_hosted_style: false,
}
}
#[must_use]
pub fn gcs_from_env(bucket: &str) -> Self {
Self::Gcs {
bucket: bucket.to_string(),
service_account_path: std::env::var("GOOGLE_APPLICATION_CREDENTIALS").ok(),
service_account_key: None,
}
}
#[must_use]
pub fn azure_from_env(container: &str) -> Self {
Self::Azure {
container: container.to_string(),
account: std::env::var("AZURE_STORAGE_ACCOUNT")
.expect("AZURE_STORAGE_ACCOUNT environment variable required"),
access_key: std::env::var("AZURE_STORAGE_ACCESS_KEY").ok(),
sas_token: std::env::var("AZURE_STORAGE_SAS_TOKEN").ok(),
}
}
#[must_use]
pub fn bucket_name(&self) -> &str {
match self {
Self::S3 { bucket, .. } => bucket,
Self::Gcs { bucket, .. } => bucket,
Self::Azure { container, .. } => container,
}
}
#[must_use]
pub fn to_url(&self) -> String {
match self {
Self::S3 { bucket, .. } => format!("s3://{bucket}"),
Self::Gcs { bucket, .. } => format!("gs://{bucket}"),
Self::Azure {
container, account, ..
} => format!("az://{account}/{container}"),
}
}
}
#[cfg(test)]
mod security_tests {
use super::*;
mod file_sandbox {
use super::*;
#[test]
fn test_sandbox_disabled_allows_all_paths() {
let config = FileSandboxConfig::default();
assert!(!config.enabled);
assert!(config.validate_path("/tmp/test").is_ok());
}
#[test]
fn test_sandbox_enabled_with_no_paths_rejects() {
let config = FileSandboxConfig {
enabled: true,
allowed_paths: vec![],
};
let result = config.validate_path("/tmp/test");
assert!(result.is_err());
assert!(result.unwrap_err().contains("no allowed paths configured"));
}
#[test]
fn test_sandbox_rejects_outside_path() {
let config = FileSandboxConfig {
enabled: true,
allowed_paths: vec![PathBuf::from("/var/lib/uni")],
};
let result = config.validate_path("/etc/passwd");
assert!(result.is_err());
assert!(result.unwrap_err().contains("outside allowed sandbox"));
}
#[test]
fn test_is_potentially_insecure() {
let disabled = FileSandboxConfig::default();
assert!(disabled.is_potentially_insecure());
let no_paths = FileSandboxConfig {
enabled: true,
allowed_paths: vec![],
};
assert!(no_paths.is_potentially_insecure());
let secure = FileSandboxConfig::sandboxed(vec![PathBuf::from("/data")]);
assert!(!secure.is_potentially_insecure());
}
#[test]
fn test_security_warning_when_disabled() {
let disabled = FileSandboxConfig::default();
assert!(disabled.security_warning().is_some());
let enabled = FileSandboxConfig::sandboxed(vec![PathBuf::from("/data")]);
assert!(enabled.security_warning().is_none());
}
#[test]
fn test_deployment_mode_defaults() {
let embedded = FileSandboxConfig::default_for_mode(DeploymentMode::Embedded);
assert!(!embedded.enabled);
let server = FileSandboxConfig::default_for_mode(DeploymentMode::Server);
assert!(server.enabled);
assert!(!server.allowed_paths.is_empty());
}
}
}