kanidm_unixd_tasks/
kanidm_unixd_tasks.rs

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    // Change the owner to the gid - remember, kanidm ONLY has gid's, the uid is implied.
57    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    // Final sanity check to prevent certain classes of attacks. This should *never*
71    // be possible, but we assert this to be sure.
72    let name = info.name.trim_start_matches('.').replace(['/', '\\'], "");
73
74    debug!(?home_prefix_path, ?home_mount_prefix_path, ?info);
75
76    // This is where the users home dir "is" and aliases from here go to the true storage
77    // mounts
78    let home_prefix_path = home_prefix_path
79        .canonicalize()
80        .map_err(|e| format!("{e:?}"))?;
81
82    // This is where the storage is *mounted*. If not set, falls back to the home_prefix.
83    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    // Does our home_prefix actually exist?
89    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    // This is now creating the actual home directory in the home_mount path.
101    // First we want to validate that the path is legitimate and hasn't tried
102    // to escape the home_mount prefix.
103    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    // Get a handle to the SELinux labeling interface
116    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    // Does the home directory exist? This is checking the *true* home mount storage.
125    if !hd_mount_path.exists() {
126        // Set the SELinux security context for file creation
127        #[cfg(all(target_family = "unix", feature = "selinux"))]
128        labeler.do_setfscreatecon_for_path()?;
129
130        // Set a umask
131        let before = unsafe { umask(0o0027) };
132
133        // Create the dir
134        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        // Copy in structure from /etc/skel/ if present
144        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                // Create equivalence rule in the SELinux policy
178                #[cfg(all(target_family = "unix", feature = "selinux"))]
179                labeler.setup_equivalence_rule(&hd_mount_path)?;
180            }
181        }
182    }
183
184    // Reset object creation SELinux context to default
185    #[cfg(all(target_family = "unix", feature = "selinux"))]
186    labeler.set_default_context_for_fs_objects()?;
187
188    // Do the aliases exist?
189    for alias in info.aliases.iter() {
190        // Sanity check the alias.
191        // let alias = alias.replace(".", "").replace("/", "").replace("\\", "");
192        let alias = alias.trim_start_matches('.').replace(['/', '\\'], "");
193
194        let alias_path = Path::join(&home_prefix_path, &alias);
195
196        // Assert the resulting alias path is consistent and correct within the home_prefix.
197        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                // If already correct, skip churn.
217                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                // Atomic replace: create temp symlink in same dir, then rename over existing.
243                let tmp = alias_path.with_extension("tmp");
244                // Best-effort cleanup of any stale tmp.
245                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                // Rename is atomic within the same directory; no disappearance window.
253                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                    // Cleanup temp on failure.
256                    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            // Does not exist. Create.
273            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    // process etc shadow and send it here.
312    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    // Immediately trigger that we should reload the shadow files for the new connected handler
356    shadow_data_watch_rx.mark_changed();
357
358    debug!("Task handler loop has started ...");
359
360    loop {
361        let msg = tokio::select! {
362            biased; // tell tokio to poll these in order
363            _ = ctl_broadcast_rx.recv() => {
364                // We received a shutdown signal.
365                debug!("Received shutdown signal, breaking task handler loop ...");
366                return
367            }
368            // We bias to *sending* messages in tasks.
369            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    // On linux when debug assertions are disabled, prevent ptrace
498    // from attaching to us.
499    #[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 cuid = get_current_uid();
505    // let cgid = get_current_gid();
506    // We only need to check effective id
507    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        // Fall back to stderr
527        .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            // This is the startup/shutdown control channel
562            let (broadcast_tx, mut broadcast_rx) = broadcast::channel(4);
563            let mut d_broadcast_rx = broadcast_tx.subscribe();
564
565            // This is to broadcast when we need to reload the shadow
566            // files.
567            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            // Setup the etcdb watch
575            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                    // Return an empty set instead.
580                    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                                    // Yep! Now let the main handler do its job.
606                                    // If it returns (dc, etc, then we loop and try again).
607                                    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                                    // Back off.
614                                    time::sleep(Duration::from_millis(5000)).await;
615                                }
616                            }
617                        }
618                    } // select
619                } // loop
620            });
621
622            info!("Server started ...");
623
624            // On linux, notify systemd.
625            #[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                        // Ignore
646                    }
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                        // Ignore
653                    }
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                        // Ignore
660                    }
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                        // Ignore
668                    }
669                }
670            }
671            info!("Signal received, shutting down");
672            // Send a broadcast that we are done.
673            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}