1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::expect_used)]
7#![deny(clippy::panic)]
8#![deny(clippy::unreachable)]
9#![deny(clippy::await_holding_lock)]
10#![deny(clippy::needless_pass_by_value)]
11#![deny(clippy::trivially_copy_pass_by_ref)]
12
13use futures::{SinkExt, StreamExt};
14use kanidm_unix_common::constants::{
15 DEFAULT_CONFIG_PATH, SYSTEM_GROUP_PATH, SYSTEM_PASSWD_PATH, SYSTEM_SHADOW_PATH,
16};
17use kanidm_unix_common::json_codec::JsonCodec;
18use kanidm_unix_common::unix_config::UnixdConfig;
19use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd, parse_etc_shadow, EtcDb};
20use kanidm_unix_common::unix_proto::{
21 HomeDirectoryInfo, TaskRequest, TaskRequestFrame, TaskResponse,
22};
23use kanidm_utils_users::{get_effective_gid, get_effective_uid};
24use libc::{lchown, umask};
25use notify_debouncer_full::notify::RecommendedWatcher;
26use notify_debouncer_full::Debouncer;
27use notify_debouncer_full::RecommendedCache;
28use notify_debouncer_full::{new_debouncer, notify::RecursiveMode, DebouncedEvent};
29use sketching::tracing_forest::traits::*;
30use sketching::tracing_forest::util::*;
31use sketching::tracing_forest::{self};
32use std::ffi::CString;
33use std::os::unix::ffi::OsStrExt;
34use std::os::unix::fs::symlink;
35use std::path::{Path, PathBuf};
36use std::process::ExitCode;
37use std::time::Duration;
38use std::{fs, io};
39use tokio::fs::File;
40use tokio::io::AsyncReadExt;
41use tokio::net::UnixStream;
42use tokio::sync::broadcast;
43use tokio::sync::watch;
44use tokio::time;
45use tokio_util::codec::Framed;
46use tracing::instrument;
47use walkdir::WalkDir;
48
49#[cfg(all(target_family = "unix", feature = "selinux"))]
50use kanidm_unix_common::selinux_util;
51
52fn chown(path: &Path, gid: u32) -> Result<(), String> {
53 let path_os = CString::new(path.as_os_str().as_bytes())
54 .map_err(|_| "Unable to create c-string".to_string())?;
55
56 if unsafe { lchown(path_os.as_ptr(), gid, gid) } != 0 {
58 return Err("Unable to set ownership".to_string());
59 }
60 Ok(())
61}
62
63fn create_home_directory(
64 info: &HomeDirectoryInfo,
65 home_prefix_path: &Path,
66 home_mount_prefix_path: Option<&PathBuf>,
67 use_etc_skel: bool,
68 use_selinux: bool,
69) -> Result<(), String> {
70 let name = info.name.trim_start_matches('.').replace(['/', '\\'], "");
73
74 debug!(?home_prefix_path, ?home_mount_prefix_path, ?info);
75
76 let home_prefix_path = home_prefix_path
79 .canonicalize()
80 .map_err(|e| format!("{e:?}"))?;
81
82 let home_mount_prefix_path = home_mount_prefix_path
84 .unwrap_or(&home_prefix_path)
85 .canonicalize()
86 .map_err(|e| format!("{e:?}"))?;
87
88 if !home_prefix_path.exists() || !home_prefix_path.is_dir() || !home_prefix_path.is_absolute() {
90 return Err("Invalid home_prefix from configuration - home_prefix path must exist, must be a directory, and must be absolute (not relative)".to_string());
91 }
92
93 if !home_mount_prefix_path.exists()
94 || !home_mount_prefix_path.is_dir()
95 || !home_mount_prefix_path.is_absolute()
96 {
97 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());
98 }
99
100 let hd_mount_path = Path::join(&home_mount_prefix_path, &name);
104
105 debug!(?hd_mount_path);
106
107 if let Some(pp) = hd_mount_path.parent() {
108 if pp != home_mount_prefix_path {
109 return Err("Invalid home directory name - not within home_mount_prefix".to_string());
110 }
111 } else {
112 return Err("Invalid/Corrupt home directory path - no prefix found".to_string());
113 }
114
115 debug!(?use_selinux, "selinux for home dir labeling");
117 #[cfg(all(target_family = "unix", feature = "selinux"))]
118 let labeler = if use_selinux {
119 selinux_util::SelinuxLabeler::new(info.gid, &home_mount_prefix_path)?
120 } else {
121 selinux_util::SelinuxLabeler::new_noop()
122 };
123
124 if !hd_mount_path.exists() {
126 #[cfg(all(target_family = "unix", feature = "selinux"))]
128 labeler.do_setfscreatecon_for_path()?;
129
130 let before = unsafe { umask(0o0027) };
132
133 if let Err(e) = fs::create_dir_all(&hd_mount_path) {
135 let _ = unsafe { umask(before) };
136 error!(err = ?e, ?hd_mount_path, "Unable to create directory");
137 return Err(format!("{e:?}"));
138 }
139 let _ = unsafe { umask(before) };
140
141 chown(&hd_mount_path, info.gid)?;
142
143 let skel_dir = Path::new("/etc/skel/");
145 if use_etc_skel && skel_dir.exists() {
146 info!("preparing homedir using /etc/skel");
147 for entry in WalkDir::new(skel_dir).into_iter().filter_map(|e| e.ok()) {
148 let dest = &hd_mount_path.join(
149 entry
150 .path()
151 .strip_prefix(skel_dir)
152 .map_err(|e| e.to_string())?,
153 );
154
155 #[cfg(all(target_family = "unix", feature = "selinux"))]
156 {
157 let p = entry
158 .path()
159 .strip_prefix(skel_dir)
160 .map_err(|e| e.to_string())?;
161 labeler.label_path(p)?;
162 }
163
164 if entry.path().is_dir() {
165 fs::create_dir_all(dest).map_err(|e| {
166 error!(err = ?e, ?dest, "Unable to create directory from /etc/skel");
167 e.to_string()
168 })?;
169 } else {
170 fs::copy(entry.path(), dest).map_err(|e| {
171 error!(err = ?e, ?dest, "Unable to copy from /etc/skel");
172 e.to_string()
173 })?;
174 }
175 chown(dest, info.gid)?;
176
177 #[cfg(all(target_family = "unix", feature = "selinux"))]
179 labeler.setup_equivalence_rule(&hd_mount_path)?;
180 }
181 }
182 }
183
184 #[cfg(all(target_family = "unix", feature = "selinux"))]
186 labeler.set_default_context_for_fs_objects()?;
187
188 for alias in info.aliases.iter() {
190 let alias = alias.trim_start_matches('.').replace(['/', '\\'], "");
193
194 let alias_path = Path::join(&home_prefix_path, &alias);
195
196 if let Some(pp) = alias_path.parent() {
198 if pp != home_prefix_path {
199 return Err("Invalid home directory alias - not within home_prefix".to_string());
200 }
201 } else {
202 return Err("Invalid/Corrupt alias directory path - no prefix found".to_string());
203 }
204
205 if alias_path.exists() {
206 debug!("checking symlink {:?} -> {:?}", alias_path, hd_mount_path);
207 let attr = match fs::symlink_metadata(&alias_path) {
208 Ok(a) => a,
209 Err(e) => {
210 error!(err = ?e, ?alias_path, "Unable to read alias path metadata");
211 return Err(format!("{e:?}"));
212 }
213 };
214
215 if attr.file_type().is_symlink() {
216 match fs::read_link(&alias_path) {
218 Ok(current_target) if current_target == hd_mount_path => {
219 debug!(
220 ?alias_path,
221 ?current_target,
222 "alias symlink already correct, skipping update"
223 );
224 continue;
225 }
226 Ok(current_target) => {
227 debug!(
228 ?alias_path,
229 ?current_target,
230 ?hd_mount_path,
231 "alias symlink target differs, updating atomically"
232 );
233 }
234 Err(e) => {
235 warn!(
236 err=?e, ?alias_path,
237 "unable to read existing symlink target, will replace atomically"
238 );
239 }
240 }
241
242 let tmp = alias_path.with_extension("tmp");
244 let _ = fs::remove_file(&tmp);
246
247 if let Err(e) = symlink(&hd_mount_path, &tmp) {
248 error!(err=?e, ?tmp, "Unable to create temporary alias symlink");
249 return Err(format!("{e:?}"));
250 }
251
252 if let Err(e) = fs::rename(&tmp, &alias_path) {
254 error!(err=?e, from=?tmp, to=?alias_path, "Unable to atomically replace alias symlink");
255 let _ = fs::remove_file(&tmp);
257 return Err(format!("{e:?}"));
258 }
259
260 debug!(
261 "alias symlink updated atomically {:?} -> {:?}",
262 alias_path, hd_mount_path
263 );
264 } else {
265 warn!(
266 ?alias_path,
267 ?hd_mount_path,
268 "home directory alias path is not a symlink, unable to update"
269 );
270 }
271 } else {
272 debug!("creating symlink {:?} -> {:?}", alias_path, hd_mount_path);
274 if let Err(e) = symlink(&hd_mount_path, &alias_path) {
275 error!(err = ?e, ?alias_path, "Unable to create alias path");
276 return Err(format!("{e:?}"));
277 }
278 }
279 }
280 Ok(())
281}
282
283async fn shadow_reload_task(
284 shadow_data_watch_tx: watch::Sender<EtcDb>,
285 mut shadow_broadcast_rx: broadcast::Receiver<bool>,
286) {
287 debug!("shadow reload task has started ...");
288
289 while shadow_broadcast_rx.recv().await.is_ok() {
290 match process_etc_passwd_group().await {
291 Ok(etc_db) => {
292 shadow_data_watch_tx.send_replace(etc_db);
293 debug!("shadow reload task sent");
294 }
295 Err(()) => {
296 error!("Unable to process etc db");
297 continue;
298 }
299 }
300 }
301
302 debug!("shadow reload task has stopped");
303}
304
305async fn handle_shadow_reload(shadow_data_watch_rx: &mut watch::Receiver<EtcDb>) -> TaskResponse {
306 debug!("Received shadow reload event.");
307 let etc_db: EtcDb = {
308 let etc_db_ref = shadow_data_watch_rx.borrow_and_update();
309 (*etc_db_ref).clone()
310 };
311 TaskResponse::NotifyShadowChange(etc_db)
313}
314
315async fn handle_unixd_request(
316 request: Option<Result<TaskRequestFrame, io::Error>>,
317 cfg: &UnixdConfig,
318) -> Result<TaskResponse, ()> {
319 debug!("Received unixd event.");
320 match request {
321 Some(Ok(TaskRequestFrame {
322 id,
323 req: TaskRequest::HomeDirectory(info),
324 })) => {
325 debug!("Received task -> HomeDirectory({:?})", info);
326
327 match create_home_directory(
328 &info,
329 cfg.home_prefix.as_ref(),
330 cfg.home_mount_prefix.as_ref(),
331 cfg.use_etc_skel,
332 cfg.selinux,
333 ) {
334 Ok(()) => Ok(TaskResponse::Success(id)),
335 Err(msg) => Ok(TaskResponse::Error(msg)),
336 }
337 }
338 other => {
339 error!("Error -> got un-handled Request Frame {other:?}");
340 Err(())
341 }
342 }
343}
344
345async fn handle_tasks(
346 stream: UnixStream,
347 ctl_broadcast_rx: &mut broadcast::Receiver<bool>,
348 shadow_data_watch_rx: &mut watch::Receiver<EtcDb>,
349 cfg: &UnixdConfig,
350) {
351 let codec: JsonCodec<TaskRequestFrame, TaskResponse> = JsonCodec::default();
352
353 let mut reqs = Framed::new(stream, codec);
354
355 shadow_data_watch_rx.mark_changed();
357
358 debug!("Task handler loop has started ...");
359
360 loop {
361 let msg = tokio::select! {
362 biased; _ = ctl_broadcast_rx.recv() => {
364 debug!("Received shutdown signal, breaking task handler loop ...");
366 return
367 }
368 Ok(_) = shadow_data_watch_rx.changed() => {
370 handle_shadow_reload(shadow_data_watch_rx).await
371 }
372 request = reqs.next() => {
373 match handle_unixd_request(request, cfg).await {
374 Ok(response) => {
375 response
376 }
377 Err(_) => {
378 error!("Error handling request, exiting task handler loop ...");
379 return;
380 }
381 }
382 }
383 };
384
385 if let Err(e) = reqs.send(msg).await {
386 error!(?e, "Error sending response to kanidm_unixd");
387 return;
388 }
389 }
390}
391
392#[instrument(level = "debug", skip_all)]
393async fn process_etc_passwd_group() -> Result<EtcDb, ()> {
394 let mut file = File::open(SYSTEM_PASSWD_PATH).await.map_err(|err| {
395 error!(?err);
396 })?;
397 let mut contents = vec![];
398 file.read_to_end(&mut contents).await.map_err(|err| {
399 error!(?err);
400 })?;
401
402 let users = parse_etc_passwd(contents.as_slice())
403 .map_err(|_| "Invalid passwd content")
404 .map_err(|err| {
405 error!(?err);
406 })?;
407
408 let mut file = File::open(SYSTEM_SHADOW_PATH).await.map_err(|err| {
409 error!(?err);
410 })?;
411 let mut contents = vec![];
412 file.read_to_end(&mut contents).await.map_err(|err| {
413 error!(?err);
414 })?;
415
416 let shadow = parse_etc_shadow(contents.as_slice())
417 .map_err(|_| "Invalid passwd content")
418 .map_err(|err| {
419 error!(?err);
420 })?;
421
422 let mut file = File::open(SYSTEM_GROUP_PATH).await.map_err(|err| {
423 error!(?err);
424 })?;
425 let mut contents = vec![];
426 file.read_to_end(&mut contents).await.map_err(|err| {
427 error!(?err);
428 })?;
429
430 let groups = parse_etc_group(contents.as_slice())
431 .map_err(|_| "Invalid group content")
432 .map_err(|err| {
433 error!(?err);
434 })?;
435
436 Ok(EtcDb {
437 users,
438 shadow,
439 groups,
440 })
441}
442
443fn setup_shadow_inotify_watcher(
444 shadow_broadcast_tx: broadcast::Sender<bool>,
445) -> Result<Debouncer<RecommendedWatcher, RecommendedCache>, ExitCode> {
446 let watcher = new_debouncer(
447 Duration::from_secs(5),
448 None,
449 move |event: Result<Vec<DebouncedEvent>, _>| {
450 let array_of_events = match event {
451 Ok(events) => events,
452 Err(array_errors) => {
453 for err in array_errors {
454 error!(?err, "inotify debounce error");
455 }
456 return;
457 }
458 };
459
460 let mut path_of_interest_was_changed = false;
461
462 for inode_event in array_of_events.iter() {
463 if !inode_event.kind.is_access()
464 && inode_event.paths.iter().any(|path| {
465 path == Path::new(SYSTEM_GROUP_PATH)
466 || path == Path::new(SYSTEM_PASSWD_PATH)
467 || path == Path::new(SYSTEM_SHADOW_PATH)
468 })
469 {
470 debug!(?inode_event, "Handling inotify modification event");
471
472 path_of_interest_was_changed = true
473 }
474 }
475
476 if path_of_interest_was_changed {
477 let _ = shadow_broadcast_tx.send(true);
478 } else {
479 trace!(?array_of_events, "IGNORED");
480 }
481 },
482 )
483 .and_then(|mut debouncer| {
484 debouncer
485 .watch(Path::new("/etc"), RecursiveMode::Recursive)
486 .map(|()| debouncer)
487 });
488
489 watcher.map_err(|err| {
490 error!(?err, "Failed to setup inotify");
491 ExitCode::FAILURE
492 })
493}
494
495#[tokio::main(flavor = "current_thread")]
496async fn main() -> ExitCode {
497 #[cfg(all(target_os = "linux", not(debug_assertions)))]
500 if let Err(code) = prctl::set_dumpable(false) {
501 error!(?code, "CRITICAL: Unable to set prctl flags");
502 return ExitCode::FAILURE;
503 }
504 let ceuid = get_effective_uid();
508 let cegid = get_effective_gid();
509
510 for arg in std::env::args() {
511 if arg.contains("--version") {
512 println!("kanidm_unixd_tasks {}", env!("CARGO_PKG_VERSION"));
513 return ExitCode::SUCCESS;
514 } else if arg.contains("--help") {
515 println!("kanidm_unixd_tasks {}", env!("CARGO_PKG_VERSION"));
516 println!("Usage: kanidm_unixd_tasks");
517 println!(" --version");
518 println!(" --help");
519 return ExitCode::SUCCESS;
520 }
521 }
522
523 #[allow(clippy::expect_used)]
524 tracing_forest::worker_task()
525 .set_global(true)
526 .map_sender(|sender| sender.or_stderr())
528 .build_on(|subscriber| {
529 subscriber.with(
530 EnvFilter::try_from_default_env()
531 .or_else(|_| EnvFilter::try_new("info"))
532 .expect("Failed to init envfilter"),
533 )
534 })
535 .on(async {
536 if ceuid != 0 || cegid != 0 {
537 error!("Refusing to run - this process *MUST* operate as root.");
538 return ExitCode::FAILURE;
539 }
540
541 let unixd_path = Path::new(DEFAULT_CONFIG_PATH);
542 let unixd_path_str = match unixd_path.to_str() {
543 Some(cps) => cps,
544 None => {
545 error!("Unable to turn unixd_path to str");
546 return ExitCode::FAILURE;
547 }
548 };
549
550 let cfg = match UnixdConfig::new().read_options_from_optional_config(unixd_path) {
551 Ok(v) => v,
552 Err(_) => {
553 error!("Failed to parse {}", unixd_path_str);
554 return ExitCode::FAILURE;
555 }
556 };
557
558 let task_sock_path = cfg.task_sock_path.clone();
559 debug!("Attempting to use {} ...", task_sock_path);
560
561 let (broadcast_tx, mut broadcast_rx) = broadcast::channel(4);
563 let mut d_broadcast_rx = broadcast_tx.subscribe();
564
565 let (shadow_broadcast_tx, shadow_broadcast_rx) = broadcast::channel(4);
568
569 let watcher = match setup_shadow_inotify_watcher(shadow_broadcast_tx.clone()) {
570 Ok(w) => w,
571 Err(exit) => return exit,
572 };
573
574 let etc_db = match process_etc_passwd_group().await {
576 Ok(etc_db) => etc_db,
577 Err(err) => {
578 warn!(?err, "unable to process {SYSTEM_PASSWD_PATH} and related files.");
579 EtcDb::default()
581 }
582 };
583
584 let (shadow_data_watch_tx, mut shadow_data_watch_rx) = watch::channel(etc_db);
585
586 let _shadow_task = tokio::spawn(async move {
587 shadow_reload_task(
588 shadow_data_watch_tx, shadow_broadcast_rx
589 ).await
590 });
591
592 let server = tokio::spawn(async move {
593 loop {
594 info!("Attempting to connect to kanidm_unixd ...");
595
596 tokio::select! {
597 _ = broadcast_rx.recv() => {
598 break;
599 }
600 connect_res = UnixStream::connect(&task_sock_path) => {
601 match connect_res {
602 Ok(stream) => {
603 info!("Found kanidm_unixd, waiting for tasks ...");
604
605 handle_tasks(stream, &mut d_broadcast_rx, &mut shadow_data_watch_rx, &cfg).await;
608 continue;
609 }
610 Err(e) => {
611 debug!("\\---> {:?}", e);
612 error!("Unable to find kanidm_unixd, sleeping ...");
613 time::sleep(Duration::from_millis(5000)).await;
615 }
616 }
617 }
618 } } });
621
622 info!("Server started ...");
623
624 #[cfg(target_os = "linux")]
626 let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
627
628 loop {
629 tokio::select! {
630 Ok(()) = tokio::signal::ctrl_c() => {
631 break
632 }
633 Some(()) = async move {
634 let sigterm = tokio::signal::unix::SignalKind::terminate();
635 #[allow(clippy::unwrap_used)]
636 tokio::signal::unix::signal(sigterm).unwrap().recv().await
637 } => {
638 break
639 }
640 Some(()) = async move {
641 let sigterm = tokio::signal::unix::SignalKind::alarm();
642 #[allow(clippy::unwrap_used)]
643 tokio::signal::unix::signal(sigterm).unwrap().recv().await
644 } => {
645 }
647 Some(()) = async move {
648 let sigterm = tokio::signal::unix::SignalKind::hangup();
649 #[allow(clippy::unwrap_used)]
650 tokio::signal::unix::signal(sigterm).unwrap().recv().await
651 } => {
652 }
654 Some(()) = async move {
655 let sigterm = tokio::signal::unix::SignalKind::user_defined1();
656 #[allow(clippy::unwrap_used)]
657 tokio::signal::unix::signal(sigterm).unwrap().recv().await
658 } => {
659 }
661
662 Some(()) = async move {
663 let sigterm = tokio::signal::unix::SignalKind::user_defined2();
664 #[allow(clippy::unwrap_used)]
665 tokio::signal::unix::signal(sigterm).unwrap().recv().await
666 } => {
667 }
669 }
670 }
671 info!("Signal received, shutting down");
672 if let Err(e) = broadcast_tx.send(true) {
674 error!("Unable to shutdown workers {:?}", e);
675 }
676
677 debug!("Dropping inotify watcher ...");
678 drop(watcher);
679
680 let _ = server.await;
681 ExitCode::SUCCESS
682 })
683 .await
684}