2023-10-12 04:09:54 +02:00
|
|
|
use std::fs::Metadata;
|
|
|
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
use std::os::linux::fs::MetadataExt;
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
use std::os::macos::fs::MetadataExt;
|
|
|
|
|
2024-06-15 07:20:11 +02:00
|
|
|
#[cfg(target_os = "illumos")]
|
|
|
|
use std::os::illumos::fs::MetadataExt;
|
|
|
|
|
2023-10-12 04:09:54 +02:00
|
|
|
use kanidm_utils_users::{get_current_gid, get_current_uid};
|
|
|
|
|
|
|
|
use std::fmt;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
/// Check a given file's metadata is read-only for the current user (true = read-only)
|
|
|
|
pub fn readonly(meta: &Metadata) -> bool {
|
|
|
|
// Who are we running as?
|
|
|
|
let cuid = get_current_uid();
|
|
|
|
let cgid = get_current_gid();
|
|
|
|
|
|
|
|
// Who owns the file?
|
|
|
|
// Who is the group owner of the file?
|
|
|
|
let f_gid = meta.st_gid();
|
|
|
|
let f_uid = meta.st_uid();
|
|
|
|
|
|
|
|
let f_mode = meta.st_mode();
|
|
|
|
|
|
|
|
!(
|
|
|
|
// If we are the owner, we have write perms as we can alter the DAC rights
|
|
|
|
cuid == f_uid ||
|
|
|
|
// If we are the group owner, check the mode bits do not have write.
|
|
|
|
(cgid == f_gid && (f_mode & 0o0020) != 0) ||
|
|
|
|
// Finally, check that everyone bits don't have write.
|
|
|
|
((f_mode & 0o0002) != 0)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum PathStatus {
|
|
|
|
Dir {
|
|
|
|
f_gid: u32,
|
|
|
|
f_uid: u32,
|
|
|
|
f_mode: u32,
|
|
|
|
access: bool,
|
|
|
|
},
|
|
|
|
Link {
|
|
|
|
f_gid: u32,
|
|
|
|
f_uid: u32,
|
|
|
|
f_mode: u32,
|
|
|
|
access: bool,
|
|
|
|
},
|
|
|
|
File {
|
|
|
|
f_gid: u32,
|
|
|
|
f_uid: u32,
|
|
|
|
f_mode: u32,
|
|
|
|
access: bool,
|
|
|
|
},
|
|
|
|
Error(std::io::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Diagnosis {
|
|
|
|
cuid: u32,
|
|
|
|
cgid: u32,
|
|
|
|
path: PathBuf,
|
|
|
|
abs_path: Result<PathBuf, std::io::Error>,
|
|
|
|
ancestors: Vec<(PathBuf, PathStatus)>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Diagnosis {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
writeln!(f, "-- diagnosis for path: {}", self.path.to_string_lossy())?;
|
|
|
|
let indent = match &self.abs_path {
|
|
|
|
Ok(abs) => {
|
|
|
|
let abs_str = abs.to_string_lossy();
|
|
|
|
writeln!(f, "canonicalised to: {}", abs_str)?;
|
|
|
|
abs_str.len() + 1
|
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
writeln!(f, "unable to canonicalise path")?;
|
|
|
|
self.path.to_string_lossy().len() + 1
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
writeln!(f, "running as: {}:{}", self.cuid, self.cgid)?;
|
|
|
|
|
|
|
|
writeln!(f, "path permissions\n")?;
|
|
|
|
for (anc, status) in &self.ancestors {
|
|
|
|
match &status {
|
|
|
|
PathStatus::Dir {
|
|
|
|
f_gid,
|
|
|
|
f_uid,
|
|
|
|
f_mode,
|
|
|
|
access,
|
|
|
|
} => {
|
|
|
|
writeln!(
|
|
|
|
f,
|
|
|
|
" {:indent$}: DIR access: {} owner: {} group: {} mode: {}",
|
|
|
|
anc.to_string_lossy(),
|
|
|
|
access,
|
|
|
|
f_uid,
|
|
|
|
f_gid,
|
|
|
|
mode_to_string(*f_mode)
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
PathStatus::Link {
|
|
|
|
f_gid,
|
|
|
|
f_uid,
|
|
|
|
f_mode,
|
|
|
|
access,
|
|
|
|
} => {
|
|
|
|
writeln!(
|
|
|
|
f,
|
|
|
|
" {:indent$}: LINK access: {} owner: {} group: {} mode: {}",
|
|
|
|
anc.to_string_lossy(),
|
|
|
|
access,
|
|
|
|
f_uid,
|
|
|
|
f_gid,
|
|
|
|
mode_to_string(*f_mode)
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
PathStatus::File {
|
|
|
|
f_gid,
|
|
|
|
f_uid,
|
|
|
|
f_mode,
|
|
|
|
access,
|
|
|
|
} => {
|
|
|
|
writeln!(
|
|
|
|
f,
|
|
|
|
" {:indent$}: FILE access: {} owner: {} group: {} mode: {}",
|
|
|
|
anc.to_string_lossy(),
|
|
|
|
access,
|
|
|
|
f_uid,
|
|
|
|
f_gid,
|
|
|
|
mode_to_string(*f_mode)
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
PathStatus::Error(err) => {
|
|
|
|
writeln!(f, " {:indent$}: ERROR: {:?}", anc.to_string_lossy(), err)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(
|
|
|
|
f,
|
2023-10-13 00:50:36 +02:00
|
|
|
"\n note that accessibility does not account for ACL's or MAC"
|
2023-10-12 04:09:54 +02:00
|
|
|
)?;
|
|
|
|
writeln!(f, "-- end diagnosis")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn diagnose_path(path: &Path) -> Diagnosis {
|
|
|
|
// Who are we?
|
|
|
|
let cuid = get_current_uid();
|
|
|
|
let cgid = get_current_gid();
|
|
|
|
|
|
|
|
// clone the path
|
|
|
|
let path: PathBuf = path.into();
|
|
|
|
|
|
|
|
// Display the abs/resolved path.
|
|
|
|
let abs_path = path.canonicalize();
|
|
|
|
|
|
|
|
// For each segment, from the root inc root
|
|
|
|
// show the path -> owner/group mode
|
|
|
|
// or show that we have permission denied.
|
|
|
|
let mut all_ancestors: Vec<_> = match &abs_path {
|
|
|
|
Ok(ap) => ap.ancestors().collect(),
|
|
|
|
Err(_) => path.ancestors().collect(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut ancestors = Vec::with_capacity(all_ancestors.len());
|
|
|
|
|
|
|
|
// Now pop from the right to start from the root.
|
|
|
|
while let Some(anc) = all_ancestors.pop() {
|
|
|
|
let status = match anc.metadata() {
|
|
|
|
Ok(meta) => {
|
|
|
|
let f_gid = meta.st_gid();
|
|
|
|
let f_uid = meta.st_uid();
|
|
|
|
let f_mode = meta.st_mode();
|
|
|
|
if meta.is_dir() {
|
|
|
|
let access = x_accessible(cuid, cgid, f_uid, f_gid, f_mode);
|
|
|
|
|
|
|
|
PathStatus::Dir {
|
|
|
|
f_gid,
|
|
|
|
f_uid,
|
|
|
|
f_mode,
|
|
|
|
access,
|
|
|
|
}
|
|
|
|
} else if meta.is_symlink() {
|
|
|
|
let access = x_accessible(cuid, cgid, f_uid, f_gid, f_mode);
|
|
|
|
|
|
|
|
PathStatus::Link {
|
|
|
|
f_gid,
|
|
|
|
f_uid,
|
|
|
|
f_mode,
|
|
|
|
access,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let access = accessible(cuid, cgid, f_uid, f_gid, f_mode);
|
|
|
|
|
|
|
|
PathStatus::File {
|
|
|
|
f_gid,
|
|
|
|
f_uid,
|
|
|
|
f_mode,
|
|
|
|
access,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(e) => PathStatus::Error(e),
|
|
|
|
};
|
|
|
|
|
|
|
|
ancestors.push((anc.into(), status))
|
|
|
|
}
|
|
|
|
|
|
|
|
Diagnosis {
|
|
|
|
cuid,
|
|
|
|
cgid,
|
|
|
|
path,
|
|
|
|
abs_path,
|
|
|
|
ancestors,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn x_accessible(cuid: u32, cgid: u32, f_uid: u32, f_gid: u32, f_mode: u32) -> bool {
|
|
|
|
(cuid == f_uid && f_mode & 0o500 == 0o500)
|
|
|
|
|| (cgid == f_gid && f_mode & 0o050 == 0o050)
|
|
|
|
|| f_mode & 0o005 == 0o005
|
|
|
|
}
|
|
|
|
|
|
|
|
fn accessible(cuid: u32, cgid: u32, f_uid: u32, f_gid: u32, f_mode: u32) -> bool {
|
|
|
|
(cuid == f_uid && f_mode & 0o400 == 0o400)
|
|
|
|
|| (cgid == f_gid && f_mode & 0o040 == 0o040)
|
|
|
|
|| f_mode & 0o004 == 0o004
|
|
|
|
}
|
|
|
|
|
|
|
|
fn mode_to_string(mode: u32) -> String {
|
|
|
|
let mut mode_str = String::with_capacity(9);
|
|
|
|
if mode & 0o400 != 0 {
|
|
|
|
mode_str.push('r')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o200 != 0 {
|
|
|
|
mode_str.push('w')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o100 != 0 {
|
|
|
|
mode_str.push('x')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o040 != 0 {
|
|
|
|
mode_str.push('r')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o020 != 0 {
|
|
|
|
mode_str.push('w')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o010 != 0 {
|
|
|
|
mode_str.push('x')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o004 != 0 {
|
|
|
|
mode_str.push('r')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o002 != 0 {
|
|
|
|
mode_str.push('w')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode & 0o001 != 0 {
|
|
|
|
mode_str.push('x')
|
|
|
|
} else {
|
|
|
|
mode_str.push('-')
|
|
|
|
}
|
|
|
|
|
|
|
|
mode_str
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_readonly() {
|
|
|
|
let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml");
|
|
|
|
println!("meta={:?} -> readonly={:?}", meta, readonly(&meta));
|
2024-02-20 09:21:33 +01:00
|
|
|
assert!(!readonly(&meta));
|
2023-10-12 04:09:54 +02:00
|
|
|
}
|