use std::path::{Path, PathBuf};
use everruns_core::typed_id::PrincipalId;
#[derive(Debug, Clone)]
pub struct LocalProfile {
pub data_dir: PathBuf,
pub workspace_root: PathBuf,
pub base_url: String,
pub org_public_id: String,
pub owner_principal_id: PrincipalId,
}
impl Default for LocalProfile {
fn default() -> Self {
let data_dir = default_data_dir();
Self {
workspace_root: data_dir.join("workspace"),
data_dir,
base_url: "http://localhost:9300".to_string(),
org_public_id: everruns_core::DEFAULT_ORG_PUBLIC_ID.to_string(),
owner_principal_id: PrincipalId::from_seed(1),
}
}
}
impl LocalProfile {
pub fn new(data_dir: impl Into<PathBuf>) -> Self {
let data_dir = data_dir.into();
Self {
workspace_root: data_dir.join("workspace"),
data_dir,
..Self::default()
}
}
pub fn db_path(&self) -> PathBuf {
self.data_dir.join("local.db")
}
pub fn with_workspace_root(mut self, root: impl Into<PathBuf>) -> Self {
self.workspace_root = root.into();
self
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn with_org_public_id(mut self, org_public_id: impl Into<String>) -> Self {
self.org_public_id = org_public_id.into();
self
}
pub fn with_owner_principal_id(mut self, owner_principal_id: PrincipalId) -> Self {
self.owner_principal_id = owner_principal_id;
self
}
pub fn ensure_dirs(&self) -> std::io::Result<()> {
ensure_private_dir(&self.data_dir)?;
ensure_private_dir(&self.workspace_root)?;
Ok(())
}
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
}
fn default_data_dir() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(std::env::temp_dir)
.join("everruns")
.join("local")
}
#[cfg(unix)]
fn ensure_private_dir(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
std::fs::create_dir_all(path)?;
let metadata = std::fs::symlink_metadata(path)?;
if metadata.file_type().is_symlink() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"refusing to use symlinked local profile directory {}",
path.display()
),
));
}
if !metadata.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("local profile path is not a directory: {}", path.display()),
));
}
if metadata.uid() != current_uid() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"local profile directory is not owned by the current user: {}",
path.display()
),
));
}
if metadata.permissions().mode() & 0o077 != 0 {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_private_dir(path: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(path)
}
#[cfg(unix)]
fn current_uid() -> u32 {
unsafe { libc::getuid() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_profile_uses_user_local_data_dir() {
assert!(
!LocalProfile::default()
.data_dir
.starts_with(std::env::temp_dir())
);
}
#[cfg(unix)]
#[test]
fn ensure_dirs_hardens_permissions() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().unwrap();
let profile = LocalProfile::new(root.path().join("profile"));
profile.ensure_dirs().unwrap();
let data_mode = std::fs::metadata(&profile.data_dir)
.unwrap()
.permissions()
.mode()
& 0o777;
let workspace_mode = std::fs::metadata(&profile.workspace_root)
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(data_mode, 0o700);
assert_eq!(workspace_mode, 0o700);
}
}