use std::fs::{File, TryLockError};
use std::io;
use std::path::{Path, PathBuf};
use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt};
#[cfg(unix)]
use cap_std::fs::PermissionsExt;
use cap_std::fs::{Dir, OpenOptions};
use crate::filesystem::{ConfinedDir, PRIVATE_DIR_MODE, PRIVATE_FILE_MODE, note_open};
const WRITER_LOCK_FILE: &str = "writer.lock";
#[derive(Debug, thiserror::Error)]
pub enum HardenError {
#[error(
"the store at `{data_dir}` is held by a live server: its writer lock at `{lock_path}` \
is taken. Stop the server (`aion stop`), run this verb, then start it again — the \
walk needs the store to itself"
)]
WriterLockHeld {
data_dir: PathBuf,
lock_path: PathBuf,
},
#[error("could not take the store's writer lock at `{lock_path}`: {error}")]
Lock {
lock_path: PathBuf,
#[source]
error: io::Error,
},
#[error("could not open the store at `{data_dir}`: {error}")]
Open {
data_dir: PathBuf,
#[source]
error: io::Error,
},
#[error("hardening `{entry}` under `{data_dir}` failed: {error}")]
Walk {
data_dir: PathBuf,
entry: String,
#[source]
error: io::Error,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HardenReport {
pub directories: u64,
pub files: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HardenProgress {
pub entry: String,
pub files: u64,
pub total_files: u64,
}
pub fn harden_store_tree(
data_dir: &Path,
mut progress: impl FnMut(&HardenProgress),
) -> Result<HardenReport, HardenError> {
let _held = take_writer_lock(data_dir)?;
let root = ConfinedDir::open(data_dir).map_err(|error| HardenError::Open {
data_dir: data_dir.to_path_buf(),
error,
})?;
let mut report = HardenReport {
directories: 1,
files: 0,
};
let mut walk = |report: &mut HardenReport| -> io::Result<()> {
let mut entries = Vec::new();
for entry in root.dir().entries()? {
let entry = entry?;
entries.push((entry.file_name(), entry.file_type()?));
}
entries.sort_by(|left, right| left.0.cmp(&right.0));
for (name, file_type) in entries {
let entry = name.to_string_lossy().into_owned();
let files_before = report.files;
harden_entry(root.dir(), &name, file_type, report)
.map_err(|error| io::Error::new(error.kind(), format!("{entry}: {error}")))?;
progress(&HardenProgress {
entry,
files: report.files - files_before,
total_files: report.files,
});
}
Ok(())
};
walk(&mut report).map_err(|error| {
let (entry, error) = split_entry(error);
HardenError::Walk {
data_dir: data_dir.to_path_buf(),
entry,
error,
}
})?;
Ok(report)
}
fn split_entry(error: io::Error) -> (String, io::Error) {
let text = error.to_string();
match text.split_once(": ") {
Some((entry, rest)) => (
entry.to_owned(),
io::Error::new(error.kind(), rest.to_owned()),
),
None => (String::new(), error),
}
}
fn take_writer_lock(data_dir: &Path) -> Result<Option<File>, HardenError> {
let lock_path = data_dir.join(WRITER_LOCK_FILE);
let file = match File::options().read(true).write(true).open(&lock_path) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(HardenError::Lock { lock_path, error }),
};
match file.try_lock() {
Ok(()) => Ok(Some(file)),
Err(TryLockError::WouldBlock) => Err(HardenError::WriterLockHeld {
data_dir: data_dir.to_path_buf(),
lock_path,
}),
Err(TryLockError::Error(error)) => Err(HardenError::Lock { lock_path, error }),
}
}
fn harden_entry(
parent: &Dir,
name: &std::ffi::OsStr,
file_type: cap_std::fs::FileType,
report: &mut HardenReport,
) -> io::Result<()> {
if file_type.is_symlink() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"sensitive state contains a symbolic link",
));
}
if file_type.is_dir() {
note_open();
let child = parent.open_dir_nofollow(name)?;
harden_dir(&child, report)
} else if file_type.is_file() {
harden_file(parent, name, report)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"sensitive state contains something that is neither a file nor a directory",
))
}
}
fn harden_dir(dir: &Dir, report: &mut HardenReport) -> io::Result<()> {
#[cfg(unix)]
dir.set_permissions(
Path::new("."),
cap_std::fs::Permissions::from_mode(PRIVATE_DIR_MODE),
)?;
report.directories += 1;
for entry in dir.entries()? {
let entry = entry?;
let name = entry.file_name();
let file_type = entry.file_type()?;
harden_entry(dir, &name, file_type, report).map_err(|error| {
io::Error::new(error.kind(), format!("{}: {error}", name.to_string_lossy()))
})?;
}
Ok(())
}
fn harden_file(parent: &Dir, name: &std::ffi::OsStr, report: &mut HardenReport) -> io::Result<()> {
let mut options = OpenOptions::new();
options.read(true).follow(FollowSymlinks::No);
note_open();
let file = parent.open_with(name, &options)?;
#[cfg(unix)]
file.set_permissions(cap_std::fs::Permissions::from_mode(PRIVATE_FILE_MODE))?;
#[cfg(not(unix))]
drop(file);
report.files += 1;
Ok(())
}
#[cfg(test)]
#[path = "store_harden_tests.rs"]
mod tests;