mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-24 18:03:54 +02:00
Add file diagnosis (#2210)
This commit is contained in:
parent
fbc62ea51e
commit
88da55260a
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2896,6 +2896,7 @@ name = "kanidm_client"
|
|||
version = "1.1.0-rc.14-dev"
|
||||
dependencies = [
|
||||
"hyper",
|
||||
"kanidm_lib_file_permissions",
|
||||
"kanidm_proto",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -3055,6 +3056,7 @@ dependencies = [
|
|||
"hyper",
|
||||
"kanidm_build_profiles",
|
||||
"kanidm_lib_crypto",
|
||||
"kanidm_lib_file_permissions",
|
||||
"kanidm_proto",
|
||||
"kanidm_utils_users",
|
||||
"kanidmd_lib",
|
||||
|
|
|
@ -17,6 +17,7 @@ reqwest = { workspace = true, default-features = false, features = [
|
|||
"multipart",
|
||||
] }
|
||||
kanidm_proto = { workspace = true }
|
||||
kanidm_lib_file_permissions = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "std"] }
|
||||
|
|
|
@ -260,6 +260,8 @@ impl KanidmClientBuilder {
|
|||
// error. This check enforces that we get the CORRECT error message instead.
|
||||
if !config_path.as_ref().exists() {
|
||||
debug!("{:?} does not exist", config_path);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
|
||||
info!(%diag);
|
||||
return Ok(self);
|
||||
};
|
||||
|
||||
|
@ -290,6 +292,9 @@ impl KanidmClientBuilder {
|
|||
);
|
||||
}
|
||||
};
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
|
||||
info!(%diag);
|
||||
|
||||
return Ok(self);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,54 +1,9 @@
|
|||
use std::fs::Metadata;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::linux::fs::MetadataExt;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::os::macos::fs::MetadataExt;
|
||||
|
||||
// #[cfg(target_os = "windows")]
|
||||
// use std::os::windows::fs::MetadataExt;
|
||||
#[cfg(target_family = "unix")]
|
||||
use kanidm_utils_users::{get_current_gid, get_current_uid};
|
||||
#[cfg(target_family = "windows")]
|
||||
mod windows;
|
||||
#[cfg(target_family = "windows")]
|
||||
pub use windows::{diagnose_path, readonly};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
/// 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)
|
||||
)
|
||||
}
|
||||
|
||||
mod unix;
|
||||
#[cfg(target_family = "unix")]
|
||||
#[test]
|
||||
fn test_readonly() {
|
||||
let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml");
|
||||
println!("meta={:?} -> readonly={:?}", meta, readonly(&meta));
|
||||
assert!(readonly(&meta) == false);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
/// Check a given file's metadata is read-only for the current user (true = read-only) Stub function if you're building for windows!
|
||||
pub fn readonly(meta: &Metadata) -> bool {
|
||||
eprintln!(
|
||||
"Windows target asked to check metadata on {:?} returning false",
|
||||
meta
|
||||
);
|
||||
false
|
||||
}
|
||||
pub use unix::{diagnose_path, readonly};
|
||||
|
|
299
libs/file_permissions/src/unix.rs
Normal file
299
libs/file_permissions/src/unix.rs
Normal file
|
@ -0,0 +1,299 @@
|
|||
use std::fs::Metadata;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::linux::fs::MetadataExt;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::os::macos::fs::MetadataExt;
|
||||
|
||||
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,
|
||||
"\n note that accesibility does not account for ACL's or MAC"
|
||||
)?;
|
||||
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));
|
||||
assert!(readonly(&meta) == false);
|
||||
}
|
24
libs/file_permissions/src/windows.rs
Normal file
24
libs/file_permissions/src/windows.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
// #[cfg(target_os = "windows")]
|
||||
// use std::os::windows::fs::MetadataExt;
|
||||
|
||||
/// Check a given file's metadata is read-only for the current user (true = read-only) Stub function if you're building for windows!
|
||||
pub fn readonly(meta: &Metadata) -> bool {
|
||||
eprintln!(
|
||||
"Windows target asked to check metadata on {:?} returning false",
|
||||
meta
|
||||
);
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Diagnosis;
|
||||
|
||||
impl fmt::Display for Diagnosis {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Unable to diagnose path issues on windows 😢")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diagnose_path(path: &Path) -> Diagnosis {
|
||||
Diagnosis {}
|
||||
}
|
|
@ -32,6 +32,7 @@ kanidm_proto = { workspace = true }
|
|||
kanidm_utils_users = { workspace = true }
|
||||
kanidmd_lib = { workspace = true }
|
||||
kanidm_lib_crypto = { workspace = true }
|
||||
kanidm_lib_file_permissions = { workspace = true }
|
||||
ldap3_proto = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
openssl = { workspace = true }
|
||||
|
|
|
@ -113,14 +113,18 @@ pub struct ServerConfig {
|
|||
|
||||
impl ServerConfig {
|
||||
pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
|
||||
let mut f = File::open(config_path).map_err(|e| {
|
||||
let mut f = File::open(config_path.as_ref()).map_err(|e| {
|
||||
eprintln!("Unable to open config file [{:?}] 🥺", e);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
|
||||
eprintln!("{}", diag);
|
||||
e
|
||||
})?;
|
||||
|
||||
let mut contents = String::new();
|
||||
f.read_to_string(&mut contents).map_err(|e| {
|
||||
eprintln!("unable to read contents {:?}", e);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
|
||||
eprintln!("{}", diag);
|
||||
e
|
||||
})?;
|
||||
|
||||
|
|
|
@ -121,6 +121,8 @@ async fn submit_admin_req(path: &str, req: AdminTaskRequest, output_mode: Consol
|
|||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!(err = ?e, %path, "Unable to connect to socket path");
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(path.as_ref());
|
||||
info!(%diag);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -228,21 +230,12 @@ async fn kanidm_main() -> ExitCode {
|
|||
return ExitCode::FAILURE;
|
||||
};
|
||||
|
||||
let sconfig = match cfg_path.exists() {
|
||||
false => {
|
||||
config_error.push(format!(
|
||||
"Refusing to run - config file {} does not exist",
|
||||
cfg_path.to_str().unwrap_or("<invalid filename>")
|
||||
));
|
||||
None
|
||||
let sconfig = match ServerConfig::new(&cfg_path) {
|
||||
Ok(c) => Some(c),
|
||||
Err(e) => {
|
||||
config_error.push(format!("Config Parse failure {:?}", e));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
true => match ServerConfig::new(&cfg_path) {
|
||||
Ok(c) => Some(c),
|
||||
Err(e) => {
|
||||
config_error.push(format!("Config Parse failure {:?}", e));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// We only allow config file for log level now.
|
||||
|
@ -359,6 +352,8 @@ async fn kanidm_main() -> ExitCode {
|
|||
"DB folder {} may not exist, server startup may FAIL!",
|
||||
db_parent_path.to_str().unwrap_or("invalid file path")
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(&db_path);
|
||||
info!(%diag);
|
||||
}
|
||||
|
||||
let db_par_path_buf = db_parent_path.to_path_buf();
|
||||
|
@ -455,6 +450,8 @@ async fn kanidm_main() -> ExitCode {
|
|||
&i_path.to_str().unwrap_or("invalid file path"),
|
||||
e
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(&i_path);
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
|
@ -474,6 +471,8 @@ async fn kanidm_main() -> ExitCode {
|
|||
&i_path.to_str().unwrap_or("invalid file path"),
|
||||
e
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(&i_path);
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
|
|
|
@ -524,12 +524,16 @@ async fn main() -> ExitCode {
|
|||
"Client config missing from {} - cannot start up. Quitting.",
|
||||
cfg_path_str
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(cfg_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
} else {
|
||||
let cfg_meta = match metadata(&cfg_path) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Unable to read metadata for {} - {:?}", cfg_path_str, e);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(cfg_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
|
@ -558,12 +562,16 @@ async fn main() -> ExitCode {
|
|||
"unixd config missing from {} - cannot start up. Quitting.",
|
||||
unixd_path_str
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(unixd_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
} else {
|
||||
let unixd_meta = match metadata(&unixd_path) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Unable to read metadata for {} - {:?}", unixd_path_str, e);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(unixd_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
|
@ -624,6 +632,8 @@ async fn main() -> ExitCode {
|
|||
.to_str()
|
||||
.unwrap_or("<db_parent_path invalid>")
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(db_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
}
|
||||
|
||||
|
@ -672,6 +682,8 @@ async fn main() -> ExitCode {
|
|||
"Refusing to run - DB path {} already exists and is not a file.",
|
||||
db_path.to_str().unwrap_or("<db_path invalid>")
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(db_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
};
|
||||
|
||||
|
@ -683,6 +695,8 @@ async fn main() -> ExitCode {
|
|||
db_path.to_str().unwrap_or("<db_path invalid>"),
|
||||
e
|
||||
);
|
||||
let diag = kanidm_lib_file_permissions::diagnose_path(db_path.as_ref());
|
||||
info!(%diag);
|
||||
return ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue