#![deny(warnings)]
#![warn(unused_extern_crates)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
use std::{fs, io};
use bytes::{BufMut, BytesMut};
use futures::{SinkExt, StreamExt};
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_proto::{HomeDirectoryInfo, TaskRequest, TaskResponse};
use kanidm_unix_resolver::unix_config::UnixdConfig;
use kanidm_utils_users::{get_effective_gid, get_effective_uid};
use libc::{lchown, umask};
use sketching::tracing_forest::traits::*;
use sketching::tracing_forest::util::*;
use sketching::tracing_forest::{self};
use tokio::net::UnixStream;
use tokio::sync::broadcast;
use tokio::time;
use tokio_util::codec::{Decoder, Encoder, Framed};
use walkdir::WalkDir;
#[cfg(all(target_family = "unix", feature = "selinux"))]
use kanidm_unix_resolver::selinux_util;
struct TaskCodec;
impl Decoder for TaskCodec {
type Error = io::Error;
type Item = TaskRequest;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
match serde_json::from_slice::<TaskRequest>(src) {
Ok(msg) => {
src.clear();
Ok(Some(msg))
}
_ => Ok(None),
}
}
}
impl Encoder<TaskResponse> for TaskCodec {
type Error = io::Error;
fn encode(&mut self, msg: TaskResponse, dst: &mut BytesMut) -> Result<(), Self::Error> {
debug!("Attempting to send request -> {:?} ...", msg);
let data = serde_json::to_vec(&msg).map_err(|e| {
error!("socket encoding error -> {:?}", e);
io::Error::new(io::ErrorKind::Other, "JSON encode error")
})?;
dst.put(data.as_slice());
Ok(())
}
}
impl TaskCodec {
fn new() -> Self {
TaskCodec
}
}
fn chown(path: &Path, gid: u32) -> Result<(), String> {
let path_os = CString::new(path.as_os_str().as_bytes())
.map_err(|_| "Unable to create c-string".to_string())?;
if unsafe { lchown(path_os.as_ptr(), gid, gid) } != 0 {
return Err("Unable to set ownership".to_string());
}
Ok(())
}
fn create_home_directory(
info: &HomeDirectoryInfo,
home_prefix_path: &Path,
home_mount_prefix_path: Option<&PathBuf>,
use_etc_skel: bool,
use_selinux: bool,
) -> Result<(), String> {
let name = info.name.trim_start_matches('.').replace(['/', '\\'], "");
debug!(?home_prefix_path, ?home_mount_prefix_path, ?info);
let home_prefix_path = home_prefix_path
.canonicalize()
.map_err(|e| format!("{:?}", e))?;
let home_mount_prefix_path = home_mount_prefix_path
.unwrap_or(&home_prefix_path)
.canonicalize()
.map_err(|e| format!("{:?}", e))?;
if !home_prefix_path.exists() || !home_prefix_path.is_dir() || !home_prefix_path.is_absolute() {
return Err("Invalid home_prefix from configuration - home_prefix path must exist, must be a directory, and must be absolute (not relative)".to_string());
}
if !home_mount_prefix_path.exists()
|| !home_mount_prefix_path.is_dir()
|| !home_mount_prefix_path.is_absolute()
{
return Err("Invalid home_mount_prefix from configuration - home_prefix path must exist, must be a directory, and must be absolute (not relative)".to_string());
}
let hd_mount_path = Path::join(&home_mount_prefix_path, &name);
debug!(?hd_mount_path);
if let Some(pp) = hd_mount_path.parent() {
if pp != home_mount_prefix_path {
return Err("Invalid home directory name - not within home_mount_prefix".to_string());
}
} else {
return Err("Invalid/Corrupt home directory path - no prefix found".to_string());
}
debug!(?use_selinux, "selinux for home dir labeling");
#[cfg(all(target_family = "unix", feature = "selinux"))]
let labeler = if use_selinux {
selinux_util::SelinuxLabeler::new(info.gid, &home_mount_prefix_path)?
} else {
selinux_util::SelinuxLabeler::new_noop()
};
if !hd_mount_path.exists() {
#[cfg(all(target_family = "unix", feature = "selinux"))]
labeler.do_setfscreatecon_for_path()?;
let before = unsafe { umask(0o0027) };
if let Err(e) = fs::create_dir_all(&hd_mount_path) {
let _ = unsafe { umask(before) };
error!(err = ?e, ?hd_mount_path, "Unable to create directory");
return Err(format!("{:?}", e));
}
let _ = unsafe { umask(before) };
chown(&hd_mount_path, info.gid)?;
let skel_dir = Path::new("/etc/skel/");
if use_etc_skel && skel_dir.exists() {
info!("preparing homedir using /etc/skel");
for entry in WalkDir::new(skel_dir).into_iter().filter_map(|e| e.ok()) {
let dest = &hd_mount_path.join(
entry
.path()
.strip_prefix(skel_dir)
.map_err(|e| e.to_string())?,
);
#[cfg(all(target_family = "unix", feature = "selinux"))]
{
let p = entry
.path()
.strip_prefix(skel_dir)
.map_err(|e| e.to_string())?;
labeler.label_path(p)?;
}
if entry.path().is_dir() {
fs::create_dir_all(dest).map_err(|e| {
error!(err = ?e, ?dest, "Unable to create directory from /etc/skel");
e.to_string()
})?;
} else {
fs::copy(entry.path(), dest).map_err(|e| {
error!(err = ?e, ?dest, "Unable to copy from /etc/skel");
e.to_string()
})?;
}
chown(dest, info.gid)?;
#[cfg(all(target_family = "unix", feature = "selinux"))]
labeler.setup_equivalence_rule(&hd_mount_path)?;
}
}
}
#[cfg(all(target_family = "unix", feature = "selinux"))]
labeler.set_default_context_for_fs_objects()?;
for alias in info.aliases.iter() {
let alias = alias.trim_start_matches('.').replace(['/', '\\'], "");
let alias_path = Path::join(&home_prefix_path, &alias);
if let Some(pp) = alias_path.parent() {
if pp != home_prefix_path {
return Err("Invalid home directory alias - not within home_prefix".to_string());
}
} else {
return Err("Invalid/Corrupt alias directory path - no prefix found".to_string());
}
if alias_path.exists() {
debug!("checking symlink {:?} -> {:?}", alias_path, hd_mount_path);
let attr = match fs::symlink_metadata(&alias_path) {
Ok(a) => a,
Err(e) => {
error!(err = ?e, ?alias_path, "Unable to read alias path metadata");
return Err(format!("{:?}", e));
}
};
if attr.file_type().is_symlink() {
if let Err(e) = fs::remove_file(&alias_path) {
error!(err = ?e, ?alias_path, "Unable to remove existing alias path");
return Err(format!("{:?}", e));
}
debug!("updating symlink {:?} -> {:?}", alias_path, hd_mount_path);
if let Err(e) = symlink(&hd_mount_path, &alias_path) {
error!(err = ?e, ?alias_path, "Unable to update alias path");
return Err(format!("{:?}", e));
}
} else {
warn!(
?alias_path,
?hd_mount_path,
"home directory alias path is not a symlink, unable to update"
);
}
} else {
debug!("creating symlink {:?} -> {:?}", alias_path, hd_mount_path);
if let Err(e) = symlink(&hd_mount_path, &alias_path) {
error!(err = ?e, ?alias_path, "Unable to create alias path");
return Err(format!("{:?}", e));
}
}
}
Ok(())
}
async fn handle_tasks(stream: UnixStream, cfg: &UnixdConfig) {
let mut reqs = Framed::new(stream, TaskCodec::new());
loop {
match reqs.next().await {
Some(Ok(TaskRequest::HomeDirectory(info))) => {
debug!("Received task -> HomeDirectory({:?})", info);
let resp = match create_home_directory(
&info,
cfg.home_prefix.as_ref(),
cfg.home_mount_prefix.as_ref(),
cfg.use_etc_skel,
cfg.selinux,
) {
Ok(()) => TaskResponse::Success,
Err(msg) => TaskResponse::Error(msg),
};
if let Err(e) = reqs.send(resp).await {
error!("Error -> {:?}", e);
return;
}
}
other => {
error!("Error -> {:?}", other);
return;
}
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
#[cfg(all(target_os = "linux", not(debug_assertions)))]
if let Err(code) = prctl::set_dumpable(false) {
error!(?code, "CRITICAL: Unable to set prctl flags");
return ExitCode::FAILURE;
}
let ceuid = get_effective_uid();
let cegid = get_effective_gid();
for arg in std::env::args() {
if arg.contains("--version") {
println!("kanidm_unixd_tasks {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
} else if arg.contains("--help") {
println!("kanidm_unixd_tasks {}", env!("CARGO_PKG_VERSION"));
println!("Usage: kanidm_unixd_tasks");
println!(" --version");
println!(" --help");
return ExitCode::SUCCESS;
}
}
#[allow(clippy::expect_used)]
tracing_forest::worker_task()
.set_global(true)
.map_sender(|sender| sender.or_stderr())
.build_on(|subscriber| {
subscriber.with(
EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.expect("Failed to init envfilter"),
)
})
.on(async {
if ceuid != 0 || cegid != 0 {
error!("Refusing to run - this process *MUST* operate as root.");
return ExitCode::FAILURE;
}
let unixd_path = Path::new(DEFAULT_CONFIG_PATH);
let unixd_path_str = match unixd_path.to_str() {
Some(cps) => cps,
None => {
error!("Unable to turn unixd_path to str");
return ExitCode::FAILURE;
}
};
let cfg = match UnixdConfig::new().read_options_from_optional_config(unixd_path) {
Ok(v) => v,
Err(_) => {
error!("Failed to parse {}", unixd_path_str);
return ExitCode::FAILURE;
}
};
let task_sock_path = cfg.task_sock_path.clone();
debug!("Attempting to use {} ...", task_sock_path);
let (broadcast_tx, mut broadcast_rx) = broadcast::channel(4);
let mut d_broadcast_rx = broadcast_tx.subscribe();
let server = tokio::spawn(async move {
loop {
info!("Attempting to connect to kanidm_unixd ...");
tokio::select! {
_ = broadcast_rx.recv() => {
break;
}
connect_res = UnixStream::connect(&task_sock_path) => {
match connect_res {
Ok(stream) => {
info!("Found kanidm_unixd, waiting for tasks ...");
tokio::select! {
_ = d_broadcast_rx.recv() => {
break;
}
_ = handle_tasks(stream, &cfg) => {
continue;
}
}
}
Err(e) => {
debug!("\\---> {:?}", e);
error!("Unable to find kanidm_unixd, sleeping ...");
time::sleep(Duration::from_millis(5000)).await;
}
}
}
}
}
});
info!("Server started ...");
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
loop {
tokio::select! {
Ok(()) = tokio::signal::ctrl_c() => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
}
}
}
info!("Signal received, shutting down");
if let Err(e) = broadcast_tx.send(true) {
error!("Unable to shutdown workers {:?}", e);
}
let _ = server.await;
ExitCode::SUCCESS
})
.await
}