use std::fs;
use std::io;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
pub const DATA_DIR_ALREADY_OWNED: &str = "DATA_DIR_ALREADY_OWNED";
fn is_private_mode(mode: u32) -> bool {
mode & 0o077 == 0
}
fn current_uid() -> u32 {
current_euid()
}
#[allow(unsafe_code)]
fn current_euid() -> u32 {
unsafe { libc::geteuid() }
}
fn is_owned_by_current_user(meta: &fs::Metadata) -> bool {
meta.uid() == current_uid()
}
pub fn ensure_private_dir(path: &Path) -> io::Result<()> {
if path.exists() {
let meta = fs::symlink_metadata(path)?;
if meta.file_type().is_symlink() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("data directory is a symlink: {}", path.display()),
));
}
if !meta.is_dir() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("data path is not a directory: {}", path.display()),
));
}
if !is_owned_by_current_user(&meta) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"data directory not owned by current user: {}",
path.display()
),
));
}
if !is_private_mode(meta.mode()) {
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
let meta = fs::metadata(path)?;
if !is_private_mode(meta.mode()) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"data directory has permissive permissions: 0o{:o}",
meta.mode() & 0o777
),
));
}
}
} else {
fs::create_dir_all(path)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
pub fn check_private_file(path: &Path, required: bool) -> io::Result<()> {
match fs::symlink_metadata(path) {
Ok(meta) => {
if meta.file_type().is_symlink() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("file is a symlink: {}", path.display()),
));
}
if !meta.is_file() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("path is not a regular file: {}", path.display()),
));
}
if !is_owned_by_current_user(&meta) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("file not owned by current user: {}", path.display()),
));
}
if !is_private_mode(meta.mode()) {
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
let meta = fs::metadata(path)?;
if !is_private_mode(meta.mode()) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"file has permissive permissions: 0o{:o}",
meta.mode() & 0o777
),
));
}
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound && !required => return Ok(()),
Err(e) => return Err(e),
}
Ok(())
}
pub fn secure_create_file(path: &Path) -> io::Result<fs::File> {
let file = fs::OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(path)?;
check_private_file(path, true)?;
Ok(file)
}
pub fn validate_data_store(data_dir: &Path, key: Option<&Path>) -> io::Result<()> {
ensure_private_dir(data_dir)?;
for name in ["agent-graph.db", "agent-graph.db-wal", "agent-graph.db-shm"] {
check_private_file(&data_dir.join(name), false)?;
}
if let Some(key) = key {
check_private_file(key, true)?;
}
Ok(())
}
pub fn recheck_wal_permissions(data_dir: &Path) -> io::Result<()> {
for name in ["agent-graph.db-wal", "agent-graph.db-shm"] {
let path = data_dir.join(name);
if path.exists() {
check_private_file(&path, false)?;
}
}
Ok(())
}
#[allow(dead_code)]
fn _path(_: PathBuf) {}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn ensure_private_dir_creates_with_0700() {
let dir = tempdir().unwrap();
let new_dir = dir.path().join("new_private");
ensure_private_dir(&new_dir).unwrap();
let meta = fs::metadata(&new_dir).unwrap();
assert_eq!(meta.mode() & 0o777, 0o700);
}
#[test]
fn ensure_private_dir_rejects_symlink() {
let dir = tempdir().unwrap();
let real = dir.path().join("real");
let link = dir.path().join("link");
fs::create_dir(&real).unwrap();
std::os::unix::fs::symlink(&real, &link).unwrap();
let result = ensure_private_dir(&link);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("symlink"));
}
#[test]
fn check_private_file_missing_optional_ok() {
let dir = tempdir().unwrap();
let missing = dir.path().join("nonexistent");
let result = check_private_file(&missing, false);
assert!(result.is_ok());
}
#[test]
fn check_private_file_missing_required_fails() {
let dir = tempdir().unwrap();
let missing = dir.path().join("nonexistent");
let result = check_private_file(&missing, true);
assert!(result.is_err());
}
#[test]
fn check_private_file_rejects_symlink() {
let dir = tempdir().unwrap();
let real = dir.path().join("real_file");
let link = dir.path().join("link_file");
fs::write(&real, "test").unwrap();
std::os::unix::fs::symlink(&real, &link).unwrap();
let result = check_private_file(&link, true);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("symlink"));
}
#[test]
fn validate_data_store_succeeds_on_fresh_dir() {
let dir = tempdir().unwrap();
let result = validate_data_store(dir.path(), None);
assert!(result.is_ok());
}
#[test]
fn secure_create_file_creates_with_0600() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test_file");
let _file = secure_create_file(&file_path).unwrap();
let meta = fs::metadata(&file_path).unwrap();
assert_eq!(meta.mode() & 0o777, 0o600);
}
#[test]
fn recheck_wal_permissions_ok_when_no_wal() {
let dir = tempdir().unwrap();
let result = recheck_wal_permissions(dir.path());
assert!(result.is_ok());
}
}