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::{HomeStrategy, 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 home_strategy: &HomeStrategy,
68 use_etc_skel: bool,
69 use_selinux: bool,
70) -> Result<(), String> {
71 let name = info.name.trim_start_matches('.').replace(['/', '\\'], "");
74
75 debug!(?home_prefix_path, ?home_mount_prefix_path, ?info);
76
77 let home_prefix_path = home_prefix_path
80 .canonicalize()
81 .map_err(|e| format!("{e:?}"))?;
82
83 let home_mount_prefix_path = home_mount_prefix_path
85 .unwrap_or(&home_prefix_path)
86 .canonicalize()
87 .map_err(|e| format!("{e:?}"))?;
88
89 if !home_prefix_path.exists() || !home_prefix_path.is_dir() || !home_prefix_path.is_absolute() {
91 return Err("Invalid home_prefix from configuration - home_prefix path must exist, must be a directory, and must be absolute (not relative)".to_string());
92 }
93
94 if !home_mount_prefix_path.exists()
95 || !home_mount_prefix_path.is_dir()
96 || !home_mount_prefix_path.is_absolute()
97 {
98 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());
99 }
100
101 let hd_mount_path = Path::join(&home_mount_prefix_path, &name);
105
106 debug!(?hd_mount_path);
107
108 if let Some(pp) = hd_mount_path.parent() {
109 if pp != home_mount_prefix_path {
110 return Err("Invalid home directory name - not within home_mount_prefix".to_string());
111 }
112 } else {
113 return Err("Invalid/Corrupt home directory path - no prefix found".to_string());
114 }
115
116 debug!(?use_selinux, "selinux for home dir labeling");
118 #[cfg(all(target_family = "unix", feature = "selinux"))]
119 let labeler = if use_selinux {
120 selinux_util::SelinuxLabeler::new(info.gid, &home_mount_prefix_path)?
121 } else {
122 selinux_util::SelinuxLabeler::new_noop()
123 };
124
125 let hd_mount_path_exists = match home_strategy {
128 HomeStrategy::Symlink => hd_mount_path.exists(),
129 };
130
131 if !hd_mount_path_exists {
133 #[cfg(all(target_family = "unix", feature = "selinux"))]
135 labeler.do_setfscreatecon_for_path()?;
136
137 match home_strategy {
141 HomeStrategy::Symlink => {
142 let before = unsafe { umask(0o0027) };
144
145 if let Err(e) = fs::create_dir_all(&hd_mount_path) {
147 let _ = unsafe { umask(before) };
148 error!(err = ?e, ?hd_mount_path, "Unable to create directory");
149 return Err(format!("{e:?}"));
150 }
151 let _ = unsafe { umask(before) };
152
153 chown(&hd_mount_path, info.gid)?;
154 }
155 }
156
157 let skel_dir = Path::new("/etc/skel/");
159 if use_etc_skel && skel_dir.exists() {
160 info!("preparing homedir using /etc/skel");
161 for entry in WalkDir::new(skel_dir).into_iter().filter_map(|e| e.ok()) {
162 let dest = &hd_mount_path.join(
163 entry
164 .path()
165 .strip_prefix(skel_dir)
166 .map_err(|e| e.to_string())?,
167 );
168
169 #[cfg(all(target_family = "unix", feature = "selinux"))]
170 {
171 let p = entry
172 .path()
173 .strip_prefix(skel_dir)
174 .map_err(|e| e.to_string())?;
175 labeler.label_path(p)?;
176 }
177
178 if entry.path().is_dir() {
179 fs::create_dir_all(dest).map_err(|e| {
180 error!(err = ?e, ?dest, "Unable to create directory from /etc/skel");
181 e.to_string()
182 })?;
183 } else {
184 fs::copy(entry.path(), dest).map_err(|e| {
185 error!(err = ?e, ?dest, "Unable to copy from /etc/skel");
186 e.to_string()
187 })?;
188 }
189 chown(dest, info.gid)?;
190
191 #[cfg(all(target_family = "unix", feature = "selinux"))]
193 labeler.setup_equivalence_rule(&hd_mount_path)?;
194 }
195 }
196 }
197
198 #[cfg(all(target_family = "unix", feature = "selinux"))]
200 labeler.set_default_context_for_fs_objects()?;
201
202 let Some(alias) = info.alias.as_ref() else {
203 debug!("No home directory alias present, sucess.");
205 return Ok(());
206 };
207
208 let alias = alias.trim_start_matches('.').replace(['/', '\\'], "");
211
212 let alias_path = Path::join(&home_prefix_path, &alias);
213
214 if let Some(pp) = alias_path.parent() {
216 if pp != home_prefix_path {
217 return Err("Invalid home directory alias - not within home_prefix".to_string());
218 }
219 } else {
220 return Err("Invalid/Corrupt alias directory path - no prefix found".to_string());
221 }
222
223 match home_strategy {
224 HomeStrategy::Symlink => home_alias_update_symlink(&alias_path, &hd_mount_path),
225 }
226}
227
228fn home_alias_update_symlink(alias_path: &Path, hd_mount_path: &Path) -> Result<(), String> {
229 if !alias_path.exists() {
230 debug!("creating symlink {:?} -> {:?}", alias_path, hd_mount_path);
232 if let Err(e) = symlink(hd_mount_path, alias_path) {
233 error!(err = ?e, ?alias_path, "Unable to create alias path");
234 return Err(format!("{e:?}"));
235 }
236 return Ok(());
237 }
238
239 debug!("checking symlink {:?} -> {:?}", alias_path, hd_mount_path);
240 let attr = match fs::symlink_metadata(alias_path) {
241 Ok(a) => a,
242 Err(e) => {
243 error!(err = ?e, ?alias_path, "Unable to read alias path metadata");
244 return Err(format!("{e:?}"));
245 }
246 };
247
248 if !attr.file_type().is_symlink() {
249 warn!(
250 ?alias_path,
251 ?hd_mount_path,
252 "home directory alias path is not a symlink, unable to update"
253 );
254 return Ok(());
255 }
256
257 match fs::read_link(alias_path) {
259 Ok(current_target) if current_target == hd_mount_path => {
260 debug!(
261 ?alias_path,
262 ?current_target,
263 "alias symlink already correct, skipping update"
264 );
265 return Ok(());
266 }
267 Ok(current_target) => {
268 debug!(
269 ?alias_path,
270 ?current_target,
271 ?hd_mount_path,
272 "alias symlink target differs, updating atomically"
273 );
274 }
275 Err(e) => {
276 warn!(
277 err=?e, ?alias_path,
278 "unable to read existing symlink target, will replace atomically"
279 );
280 }
281 }
282
283 let alias_path_tmp = alias_path.with_extension("tmp");
285
286 if alias_path_tmp.exists() {
287 debug!("checking symlink temp {:?}", alias_path_tmp);
288 let attr = match fs::symlink_metadata(&alias_path_tmp) {
289 Ok(a) => a,
290 Err(e) => {
291 error!(err = ?e, ?alias_path_tmp, "Unable to read alias path temp metadata");
292 return Err(format!("{e:?}"));
293 }
294 };
295
296 if !attr.file_type().is_symlink() {
297 warn!(
298 ?alias_path,
299 ?alias_path_tmp,
300 ?hd_mount_path,
301 "home directory alias path temporary update location already exists, and is not a symlink, unable to update"
302 );
303 return Ok(());
304 }
305 }
306
307 let _ = fs::remove_file(&alias_path_tmp);
309
310 if let Err(e) = symlink(hd_mount_path, &alias_path_tmp) {
311 error!(err=?e, ?alias_path_tmp, "Unable to create temporary alias symlink");
312 return Err(format!("{e:?}"));
313 }
314
315 if let Err(e) = fs::rename(&alias_path_tmp, alias_path) {
317 error!(err=?e, from=?alias_path_tmp, to=?alias_path, "Unable to atomically replace alias symlink");
318 let _ = fs::remove_file(&alias_path_tmp);
320 return Err(format!("{e:?}"));
321 }
322
323 debug!(
324 "alias symlink updated atomically {:?} -> {:?}",
325 alias_path, hd_mount_path
326 );
327
328 Ok(())
329}
330
331async fn shadow_reload_task(
332 shadow_data_watch_tx: watch::Sender<EtcDb>,
333 mut shadow_broadcast_rx: broadcast::Receiver<bool>,
334) {
335 debug!("shadow reload task has started ...");
336
337 while shadow_broadcast_rx.recv().await.is_ok() {
338 match process_etc_passwd_group().await {
339 Ok(etc_db) => {
340 shadow_data_watch_tx.send_replace(etc_db);
341 debug!("shadow reload task sent");
342 }
343 Err(()) => {
344 error!("Unable to process etc db");
345 continue;
346 }
347 }
348 }
349
350 debug!("shadow reload task has stopped");
351}
352
353async fn handle_shadow_reload(shadow_data_watch_rx: &mut watch::Receiver<EtcDb>) -> TaskResponse {
354 debug!("Received shadow reload event.");
355 let etc_db: EtcDb = {
356 let etc_db_ref = shadow_data_watch_rx.borrow_and_update();
357 (*etc_db_ref).clone()
358 };
359 TaskResponse::NotifyShadowChange(etc_db)
361}
362
363async fn handle_unixd_request(
364 request: Option<Result<TaskRequestFrame, io::Error>>,
365 cfg: &UnixdConfig,
366) -> Result<TaskResponse, ()> {
367 debug!("Received unixd event.");
368 match request {
369 Some(Ok(TaskRequestFrame {
370 id,
371 req: TaskRequest::HomeDirectory(info),
372 })) => {
373 debug!("Received task -> HomeDirectory({:?})", info);
374
375 match create_home_directory(
376 &info,
377 cfg.home_prefix.as_ref(),
378 cfg.home_mount_prefix.as_ref(),
379 &cfg.home_strategy,
380 cfg.use_etc_skel,
381 cfg.selinux,
382 ) {
383 Ok(()) => Ok(TaskResponse::Success(id)),
384 Err(msg) => Ok(TaskResponse::Error(msg)),
385 }
386 }
387 other => {
388 error!("Error -> got un-handled Request Frame {other:?}");
389 Err(())
390 }
391 }
392}
393
394async fn handle_tasks(
395 stream: UnixStream,
396 ctl_broadcast_rx: &mut broadcast::Receiver<bool>,
397 shadow_data_watch_rx: &mut watch::Receiver<EtcDb>,
398 cfg: &UnixdConfig,
399) {
400 let codec: JsonCodec<TaskRequestFrame, TaskResponse> = JsonCodec::default();
401
402 let mut reqs = Framed::new(stream, codec);
403
404 shadow_data_watch_rx.mark_changed();
406
407 debug!("Task handler loop has started ...");
408
409 loop {
410 let msg = tokio::select! {
411 biased; _ = ctl_broadcast_rx.recv() => {
413 debug!("Received shutdown signal, breaking task handler loop ...");
415 return
416 }
417 Ok(_) = shadow_data_watch_rx.changed() => {
419 handle_shadow_reload(shadow_data_watch_rx).await
420 }
421 request = reqs.next() => {
422 match handle_unixd_request(request, cfg).await {
423 Ok(response) => {
424 response
425 }
426 Err(_) => {
427 error!("Error handling request, exiting task handler loop ...");
428 return;
429 }
430 }
431 }
432 };
433
434 if let Err(e) = reqs.send(msg).await {
435 error!(?e, "Error sending response to kanidm_unixd");
436 return;
437 }
438 }
439}
440
441#[instrument(level = "debug", skip_all)]
442async fn process_etc_passwd_group() -> Result<EtcDb, ()> {
443 let mut file = File::open(SYSTEM_PASSWD_PATH).await.map_err(|err| {
444 error!(?err);
445 })?;
446 let mut contents = vec![];
447 file.read_to_end(&mut contents).await.map_err(|err| {
448 error!(?err);
449 })?;
450
451 let users = parse_etc_passwd(contents.as_slice())
452 .map_err(|_| "Invalid passwd content")
453 .map_err(|err| {
454 error!(?err);
455 })?;
456
457 let mut file = File::open(SYSTEM_SHADOW_PATH).await.map_err(|err| {
458 error!(?err);
459 })?;
460 let mut contents = vec![];
461 file.read_to_end(&mut contents).await.map_err(|err| {
462 error!(?err);
463 })?;
464
465 let shadow = parse_etc_shadow(contents.as_slice())
466 .map_err(|_| "Invalid passwd content")
467 .map_err(|err| {
468 error!(?err);
469 })?;
470
471 let mut file = File::open(SYSTEM_GROUP_PATH).await.map_err(|err| {
472 error!(?err);
473 })?;
474 let mut contents = vec![];
475 file.read_to_end(&mut contents).await.map_err(|err| {
476 error!(?err);
477 })?;
478
479 let groups = parse_etc_group(contents.as_slice())
480 .map_err(|_| "Invalid group content")
481 .map_err(|err| {
482 error!(?err);
483 })?;
484
485 Ok(EtcDb {
486 users,
487 shadow,
488 groups,
489 })
490}
491
492fn setup_shadow_inotify_watcher(
493 shadow_broadcast_tx: broadcast::Sender<bool>,
494) -> Result<Debouncer<RecommendedWatcher, RecommendedCache>, ExitCode> {
495 let watcher = new_debouncer(
496 Duration::from_secs(5),
497 None,
498 move |event: Result<Vec<DebouncedEvent>, _>| {
499 let array_of_events = match event {
500 Ok(events) => events,
501 Err(array_errors) => {
502 for err in array_errors {
503 error!(?err, "inotify debounce error");
504 }
505 return;
506 }
507 };
508
509 let mut path_of_interest_was_changed = false;
510
511 for inode_event in array_of_events.iter() {
512 if !inode_event.kind.is_access()
513 && inode_event.paths.iter().any(|path| {
514 path == Path::new(SYSTEM_GROUP_PATH)
515 || path == Path::new(SYSTEM_PASSWD_PATH)
516 || path == Path::new(SYSTEM_SHADOW_PATH)
517 })
518 {
519 debug!(?inode_event, "Handling inotify modification event");
520
521 path_of_interest_was_changed = true
522 }
523 }
524
525 if path_of_interest_was_changed {
526 let _ = shadow_broadcast_tx.send(true);
527 } else {
528 trace!(?array_of_events, "IGNORED");
529 }
530 },
531 )
532 .and_then(|mut debouncer| {
533 debouncer
534 .watch(Path::new("/etc"), RecursiveMode::Recursive)
535 .map(|()| debouncer)
536 });
537
538 watcher.map_err(|err| {
539 error!(?err, "Failed to setup inotify");
540 ExitCode::FAILURE
541 })
542}
543
544#[tokio::main(flavor = "current_thread")]
545async fn main() -> ExitCode {
546 #[cfg(all(target_os = "linux", not(debug_assertions)))]
549 if let Err(code) = prctl::set_dumpable(false) {
550 error!(?code, "CRITICAL: Unable to set prctl flags");
551 return ExitCode::FAILURE;
552 }
553 let ceuid = get_effective_uid();
557 let cegid = get_effective_gid();
558
559 for arg in std::env::args() {
560 if arg.contains("--version") {
561 println!("kanidm_unixd_tasks {}", env!("CARGO_PKG_VERSION"));
562 return ExitCode::SUCCESS;
563 } else if arg.contains("--help") {
564 println!("kanidm_unixd_tasks {}", env!("CARGO_PKG_VERSION"));
565 println!("Usage: kanidm_unixd_tasks");
566 println!(" --version");
567 println!(" --help");
568 return ExitCode::SUCCESS;
569 }
570 }
571
572 #[allow(clippy::expect_used)]
573 tracing_forest::worker_task()
574 .set_global(true)
575 .map_sender(|sender| sender.or_stderr())
577 .build_on(|subscriber| {
578 subscriber.with(
579 EnvFilter::try_from_default_env()
580 .or_else(|_| EnvFilter::try_new("info"))
581 .expect("Failed to init envfilter"),
582 )
583 })
584 .on(async {
585 if ceuid != 0 || cegid != 0 {
586 error!("Refusing to run - this process *MUST* operate as root.");
587 return ExitCode::FAILURE;
588 }
589
590 let unixd_path = Path::new(DEFAULT_CONFIG_PATH);
591 let unixd_path_str = match unixd_path.to_str() {
592 Some(cps) => cps,
593 None => {
594 error!("Unable to turn unixd_path to str");
595 return ExitCode::FAILURE;
596 }
597 };
598
599 let cfg = match UnixdConfig::new().read_options_from_optional_config(unixd_path) {
600 Ok(v) => v,
601 Err(_) => {
602 error!("Failed to parse {}", unixd_path_str);
603 return ExitCode::FAILURE;
604 }
605 };
606
607 let task_sock_path = cfg.task_sock_path.clone();
608 debug!("Attempting to use {} ...", task_sock_path);
609
610 let (broadcast_tx, mut broadcast_rx) = broadcast::channel(4);
612 let mut d_broadcast_rx = broadcast_tx.subscribe();
613
614 let (shadow_broadcast_tx, shadow_broadcast_rx) = broadcast::channel(4);
617
618 let watcher = match setup_shadow_inotify_watcher(shadow_broadcast_tx.clone()) {
619 Ok(w) => w,
620 Err(exit) => return exit,
621 };
622
623 let etc_db = match process_etc_passwd_group().await {
625 Ok(etc_db) => etc_db,
626 Err(err) => {
627 warn!(?err, "unable to process {SYSTEM_PASSWD_PATH} and related files.");
628 EtcDb::default()
630 }
631 };
632
633 let (shadow_data_watch_tx, mut shadow_data_watch_rx) = watch::channel(etc_db);
634
635 let _shadow_task = tokio::spawn(async move {
636 shadow_reload_task(
637 shadow_data_watch_tx, shadow_broadcast_rx
638 ).await
639 });
640
641 let server = tokio::spawn(async move {
642 loop {
643 info!("Attempting to connect to kanidm_unixd ...");
644
645 tokio::select! {
646 _ = broadcast_rx.recv() => {
647 break;
648 }
649 connect_res = UnixStream::connect(&task_sock_path) => {
650 match connect_res {
651 Ok(stream) => {
652 info!("Found kanidm_unixd, waiting for tasks ...");
653
654 handle_tasks(stream, &mut d_broadcast_rx, &mut shadow_data_watch_rx, &cfg).await;
657 continue;
658 }
659 Err(e) => {
660 debug!("\\---> {:?}", e);
661 error!("Unable to find kanidm_unixd, sleeping ...");
662 time::sleep(Duration::from_millis(5000)).await;
664 }
665 }
666 }
667 } } });
670
671 info!("Server started ...");
672
673 #[cfg(target_os = "linux")]
675 let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
676
677 loop {
678 tokio::select! {
679 Ok(()) = tokio::signal::ctrl_c() => {
680 break
681 }
682 Some(()) = async move {
683 let sigterm = tokio::signal::unix::SignalKind::terminate();
684 #[allow(clippy::unwrap_used)]
685 tokio::signal::unix::signal(sigterm).unwrap().recv().await
686 } => {
687 break
688 }
689 Some(()) = async move {
690 let sigterm = tokio::signal::unix::SignalKind::alarm();
691 #[allow(clippy::unwrap_used)]
692 tokio::signal::unix::signal(sigterm).unwrap().recv().await
693 } => {
694 }
696 Some(()) = async move {
697 let sigterm = tokio::signal::unix::SignalKind::hangup();
698 #[allow(clippy::unwrap_used)]
699 tokio::signal::unix::signal(sigterm).unwrap().recv().await
700 } => {
701 }
703 Some(()) = async move {
704 let sigterm = tokio::signal::unix::SignalKind::user_defined1();
705 #[allow(clippy::unwrap_used)]
706 tokio::signal::unix::signal(sigterm).unwrap().recv().await
707 } => {
708 }
710
711 Some(()) = async move {
712 let sigterm = tokio::signal::unix::SignalKind::user_defined2();
713 #[allow(clippy::unwrap_used)]
714 tokio::signal::unix::signal(sigterm).unwrap().recv().await
715 } => {
716 }
718 }
719 }
720 info!("Signal received, shutting down");
721 if let Err(e) = broadcast_tx.send(true) {
723 error!("Unable to shutdown workers {:?}", e);
724 }
725
726 debug!("Dropping inotify watcher ...");
727 drop(watcher);
728
729 let _ = server.await;
730 ExitCode::SUCCESS
731 })
732 .await
733}