kanidmd/
main.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
13#[cfg(all(not(feature = "dhat-heap"), target_os = "linux"))]
14#[global_allocator]
15static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
16
17#[cfg(feature = "dhat-heap")]
18#[global_allocator]
19static ALLOC: dhat::Alloc = dhat::Alloc;
20
21use std::fs::{metadata, File};
22// This works on both unix and windows.
23use fs4::fs_std::FileExt;
24use kanidm_proto::messages::ConsoleOutputMode;
25use sketching::otel::TracingPipelineGuard;
26use std::io::Read;
27#[cfg(target_family = "unix")]
28use std::os::unix::fs::MetadataExt;
29use std::path::PathBuf;
30use std::process::ExitCode;
31
32use clap::{Args, Parser, Subcommand};
33use futures::{SinkExt, StreamExt};
34#[cfg(not(target_family = "windows"))] // not needed for windows builds
35use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
36use kanidmd_core::admin::{
37    AdminTaskRequest, AdminTaskResponse, ClientCodec, ProtoDomainInfo,
38    ProtoDomainUpgradeCheckReport, ProtoDomainUpgradeCheckStatus,
39};
40use kanidmd_core::config::{CliConfig, Configuration, EnvironmentConfig, ServerConfigUntagged};
41use kanidmd_core::{
42    backup_server_core, cert_generate_core, create_server_core, dbscan_get_id2entry_core,
43    dbscan_list_id2entry_core, dbscan_list_index_analysis_core, dbscan_list_index_core,
44    dbscan_list_indexes_core, dbscan_list_quarantined_core, dbscan_quarantine_id2entry_core,
45    dbscan_restore_quarantined_core, domain_rename_core, reindex_server_core, restore_server_core,
46    vacuum_server_core, verify_server_core,
47};
48use sketching::tracing_forest::util::*;
49use tokio::net::UnixStream;
50use tokio_util::codec::Framed;
51#[cfg(target_family = "windows")] // for windows builds
52use whoami;
53
54include!("./opt.rs");
55
56/// Get information on the windows username
57#[cfg(target_family = "windows")]
58fn get_user_details_windows() {
59    eprintln!(
60        "Running on windows, current username is: {:?}",
61        whoami::username()
62    );
63}
64
65async fn submit_admin_req(path: &str, req: AdminTaskRequest, output_mode: ConsoleOutputMode) {
66    // Connect to the socket.
67    let stream = match UnixStream::connect(path).await {
68        Ok(s) => s,
69        Err(e) => {
70            error!(err = ?e, %path, "Unable to connect to socket path");
71            let diag = kanidm_lib_file_permissions::diagnose_path(path.as_ref());
72            info!(%diag);
73            return;
74        }
75    };
76
77    let mut reqs = Framed::new(stream, ClientCodec);
78
79    if let Err(e) = reqs.send(req).await {
80        error!(err = ?e, "Unable to send request");
81        return;
82    };
83
84    if let Err(e) = reqs.flush().await {
85        error!(err = ?e, "Unable to flush request");
86        return;
87    }
88
89    trace!("flushed, waiting ...");
90
91    match reqs.next().await {
92        Some(Ok(AdminTaskResponse::RecoverAccount { password })) => match output_mode {
93            ConsoleOutputMode::JSON => {
94                let json_output = serde_json::json!({
95                    "password": password
96                });
97                println!("{json_output}");
98            }
99            ConsoleOutputMode::Text => {
100                info!(new_password = ?password)
101            }
102        },
103        Some(Ok(AdminTaskResponse::ShowReplicationCertificate { cert })) => match output_mode {
104            ConsoleOutputMode::JSON => {
105                println!("{{\"certificate\":\"{cert}\"}}")
106            }
107            ConsoleOutputMode::Text => {
108                info!(certificate = ?cert)
109            }
110        },
111
112        Some(Ok(AdminTaskResponse::DomainUpgradeCheck { report })) => {
113            match output_mode {
114                ConsoleOutputMode::JSON => {
115                    let json_output = serde_json::json!({
116                        "domain_upgrade_check": report
117                    });
118                    println!("{json_output}");
119                }
120                ConsoleOutputMode::Text => {
121                    let ProtoDomainUpgradeCheckReport {
122                        name,
123                        uuid,
124                        current_level,
125                        upgrade_level,
126                        report_items,
127                    } = report;
128
129                    info!("domain_name            : {}", name);
130                    info!("domain_uuid            : {}", uuid);
131                    info!("domain_current_level   : {}", current_level);
132                    info!("domain_upgrade_level   : {}", upgrade_level);
133
134                    for item in report_items {
135                        info!("------------------------");
136                        match item.status {
137                            ProtoDomainUpgradeCheckStatus::Pass6To7Gidnumber => {
138                                info!("upgrade_item           : gidnumber range validity");
139                                debug!("from_level             : {}", item.from_level);
140                                debug!("to_level               : {}", item.to_level);
141                                info!("status                 : PASS");
142                            }
143                            ProtoDomainUpgradeCheckStatus::Fail6To7Gidnumber => {
144                                info!("upgrade_item           : gidnumber range validity");
145                                debug!("from_level             : {}", item.from_level);
146                                debug!("to_level               : {}", item.to_level);
147                                info!("status                 : FAIL");
148                                info!("description            : The automatically allocated gidnumbers for posix accounts was found to allocate numbers into systemd-reserved ranges. These can no longer be used.");
149                                info!("action                 : Modify the gidnumber of affected entries so that they are in the range 65536 to 524287 OR reset the gidnumber to cause it to automatically regenerate.");
150                                for entry_id in item.affected_entries {
151                                    info!("affected_entry         : {}", entry_id);
152                                }
153                            }
154                            // ===========
155                            ProtoDomainUpgradeCheckStatus::Pass7To8SecurityKeys => {
156                                info!("upgrade_item           : security key usage");
157                                debug!("from_level             : {}", item.from_level);
158                                debug!("to_level               : {}", item.to_level);
159                                info!("status                 : PASS");
160                            }
161                            ProtoDomainUpgradeCheckStatus::Fail7To8SecurityKeys => {
162                                info!("upgrade_item           : security key usage");
163                                debug!("from_level             : {}", item.from_level);
164                                debug!("to_level               : {}", item.to_level);
165                                info!("status                 : FAIL");
166                                info!("description            : Security keys no longer function as a second factor due to the introduction of CTAP2 and greater forcing PIN interactions.");
167                                info!("action                 : Modify the accounts in question to remove their security key and add it as a passkey or enable TOTP");
168                                for entry_id in item.affected_entries {
169                                    info!("affected_entry         : {}", entry_id);
170                                }
171                            }
172                            // ===========
173                            ProtoDomainUpgradeCheckStatus::Pass7To8Oauth2StrictRedirectUri => {
174                                info!("upgrade_item           : oauth2 strict redirect uri enforcement");
175                                debug!("from_level             : {}", item.from_level);
176                                debug!("to_level               : {}", item.to_level);
177                                info!("status                 : PASS");
178                            }
179                            ProtoDomainUpgradeCheckStatus::Fail7To8Oauth2StrictRedirectUri => {
180                                info!("upgrade_item           : oauth2 strict redirect uri enforcement");
181                                debug!("from_level             : {}", item.from_level);
182                                debug!("to_level               : {}", item.to_level);
183                                info!("status                 : FAIL");
184                                info!("description            : To harden against possible public client open redirection vulnerabilities, redirect uris must now be registered ahead of time and are validated rather than the former origin verification process.");
185                                info!("action                 : Verify the redirect uri's for OAuth2 clients and then enable strict-redirect-uri on each client.");
186                                for entry_id in item.affected_entries {
187                                    info!("affected_entry         : {}", entry_id);
188                                }
189                            }
190                        }
191                    }
192                }
193            }
194        }
195
196        Some(Ok(AdminTaskResponse::DomainRaise { level })) => match output_mode {
197            ConsoleOutputMode::JSON => {
198                eprintln!("{{\"success\":\"{level}\"}}")
199            }
200            ConsoleOutputMode::Text => {
201                info!("success - raised domain level to {}", level)
202            }
203        },
204        Some(Ok(AdminTaskResponse::DomainShow { domain_info })) => match output_mode {
205            ConsoleOutputMode::JSON => {
206                let json_output = serde_json::json!({
207                    "domain_info": domain_info
208                });
209                println!("{json_output}");
210            }
211            ConsoleOutputMode::Text => {
212                let ProtoDomainInfo {
213                    name,
214                    displayname,
215                    uuid,
216                    level,
217                } = domain_info;
218
219                info!("domain_name   : {}", name);
220                info!("domain_display: {}", displayname);
221                info!("domain_uuid   : {}", uuid);
222                info!("domain_level  : {}", level);
223            }
224        },
225        Some(Ok(AdminTaskResponse::Success)) => match output_mode {
226            ConsoleOutputMode::JSON => {
227                eprintln!("\"success\"")
228            }
229            ConsoleOutputMode::Text => {
230                info!("success")
231            }
232        },
233        Some(Ok(AdminTaskResponse::Error)) => match output_mode {
234            ConsoleOutputMode::JSON => {
235                eprintln!("\"error\"")
236            }
237            ConsoleOutputMode::Text => {
238                info!("Error - you should inspect the logs.")
239            }
240        },
241        Some(Err(err)) => {
242            error!(?err, "Error during admin task operation");
243        }
244        None => {
245            error!("Error making request to admin socket");
246        }
247    }
248}
249
250/// Check what we're running as and various filesystem permissions.
251fn check_file_ownership(opt: &KanidmdParser) -> Result<(), ExitCode> {
252    // Get info about who we are.
253    #[cfg(target_family = "unix")]
254    let (cuid, ceuid) = {
255        let cuid = get_current_uid();
256        let ceuid = get_effective_uid();
257        let cgid = get_current_gid();
258        let cegid = get_effective_gid();
259
260        if cuid == 0 || ceuid == 0 || cgid == 0 || cegid == 0 {
261            warn!("This is running as uid == 0 (root) which may be a security risk.");
262            // eprintln!("ERROR: Refusing to run - this process must not operate as root.");
263            // std::process::exit(1);
264        }
265
266        if cuid != ceuid || cgid != cegid {
267            error!("{} != {} || {} != {}", cuid, ceuid, cgid, cegid);
268            error!("Refusing to run - uid and euid OR gid and egid must be consistent.");
269            return Err(ExitCode::FAILURE);
270        }
271        (cuid, ceuid)
272    };
273
274    if let Some(cfg_path) = &opt.config_path {
275        #[cfg(target_family = "unix")]
276        {
277            if let Some(cfg_meta) = match metadata(cfg_path) {
278                Ok(m) => Some(m),
279                Err(e) => {
280                    error!(
281                        "Unable to read metadata for configuration file '{}' - {:?}",
282                        cfg_path.display(),
283                        e
284                    );
285                    // return ExitCxode::FAILURE;
286                    None
287                }
288            } {
289                if !kanidm_lib_file_permissions::readonly(&cfg_meta) {
290                    warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...",
291                        cfg_path.to_str().unwrap_or("invalid file path"));
292                }
293
294                if cfg_meta.mode() & 0o007 != 0 {
295                    warn!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...",
296                        cfg_path.to_str().unwrap_or("invalid file path")
297                        );
298                }
299
300                if cfg_meta.uid() == cuid || cfg_meta.uid() == ceuid {
301                    warn!("WARNING: {} owned by the current uid, which may allow file permission changes. This could be a security risk ...",
302                        cfg_path.to_str().unwrap_or("invalid file path")
303                        );
304                }
305            }
306        }
307    }
308    Ok(())
309}
310
311// We have to do this because we can't use tracing until we've started the logging pipeline, and we can't start the logging pipeline until the tokio runtime's doing its thing.
312async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
313    // if we have a server config and it has an OTEL URL, then we'll start the logging pipeline now.
314
315    // TODO: only send to stderr when we're not in a TTY
316    let sub = match sketching::otel::start_logging_pipeline(
317        &config.otel_grpc_url,
318        config.log_level,
319        "kanidmd",
320    ) {
321        Err(err) => {
322            eprintln!("Error starting logger - {err:} - Bailing on startup!");
323            return ExitCode::FAILURE;
324        }
325        Ok(val) => val,
326    };
327
328    if let Err(err) = tracing::subscriber::set_global_default(sub).map_err(|err| {
329        eprintln!("Error starting logger - {err:} - Bailing on startup!");
330        ExitCode::FAILURE
331    }) {
332        return err;
333    };
334
335    // ************************************************
336    // HERE'S WHERE YOU CAN START USING THE LOGGER
337    // ************************************************
338
339    info!(version = %env!("KANIDM_PKG_VERSION"), "Starting Kanidmd");
340
341    // guard which shuts down the logging/tracing providers when we close out
342    let _otelguard = TracingPipelineGuard {};
343
344    // ===========================================================================
345    // Start pre-run checks
346
347    // Check the permissions of the files from the configuration.
348    if let Err(err) = check_file_ownership(&opt) {
349        return err;
350    };
351
352    if let Some(db_path) = config.db_path.as_ref() {
353        let db_pathbuf = db_path.to_path_buf();
354        // We can't check the db_path permissions because it may not exist yet!
355        if let Some(db_parent_path) = db_pathbuf.parent() {
356            if !db_parent_path.exists() {
357                warn!(
358                    "DB folder {} may not exist, server startup may FAIL!",
359                    db_parent_path.to_str().unwrap_or("invalid file path")
360                );
361                let diag = kanidm_lib_file_permissions::diagnose_path(&db_pathbuf);
362                info!(%diag);
363            }
364
365            let db_par_path_buf = db_parent_path.to_path_buf();
366            let i_meta = match metadata(&db_par_path_buf) {
367                Ok(m) => m,
368                Err(e) => {
369                    error!(
370                        "Unable to read metadata for database folder '{}' - {:?}",
371                        &db_par_path_buf.to_str().unwrap_or("invalid file path"),
372                        e
373                    );
374                    return ExitCode::FAILURE;
375                }
376            };
377            if !i_meta.is_dir() {
378                error!(
379                    "ERROR: Refusing to run - DB folder {} may not be a directory",
380                    db_par_path_buf.to_str().unwrap_or("invalid file path")
381                );
382                return ExitCode::FAILURE;
383            }
384
385            if kanidm_lib_file_permissions::readonly(&i_meta) {
386                warn!("WARNING: DB folder permissions on {} indicate it may not be RW. This could cause the server start up to fail!", db_par_path_buf.to_str().unwrap_or("invalid file path"));
387            }
388            #[cfg(not(target_os = "windows"))]
389            if i_meta.mode() & 0o007 != 0 {
390                warn!("WARNING: DB folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", db_par_path_buf.to_str().unwrap_or("invalid file path"));
391            }
392        }
393    } else {
394        error!("No db_path set in configuration, server startup will FAIL!");
395        return ExitCode::FAILURE;
396    }
397
398    let lock_was_setup = match &opt.commands {
399        // we aren't going to touch the DB so we can carry on
400        KanidmdOpt::ShowReplicationCertificate
401        | KanidmdOpt::RenewReplicationCertificate
402        | KanidmdOpt::RefreshReplicationConsumer { .. }
403        | KanidmdOpt::RecoverAccount { .. }
404        | KanidmdOpt::DisableAccount { .. }
405        | KanidmdOpt::HealthCheck(_) => None,
406        _ => {
407            // Okay - Lets now create our lock and go.
408            #[allow(clippy::expect_used)]
409            let klock_path = match config.db_path.clone() {
410                Some(val) => val.with_extension("klock"),
411                None => std::env::temp_dir().join("kanidmd.klock"),
412            };
413
414            let flock = match File::create(&klock_path) {
415                Ok(flock) => flock,
416                Err(err) => {
417                    error!(
418                        "ERROR: Refusing to start - unable to create kanidmd exclusive lock at {}",
419                        klock_path.display()
420                    );
421                    error!(?err);
422                    return ExitCode::FAILURE;
423                }
424            };
425
426            match flock.try_lock_exclusive() {
427                Ok(true) => debug!("Acquired kanidm exclusive lock"),
428                Ok(false) => {
429                    error!(
430                        "ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {}",
431                        klock_path.display()
432                    );
433                    error!("Is another kanidmd process running?");
434                    return ExitCode::FAILURE;
435                }
436                Err(err) => {
437                    error!(
438                        "ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {}",
439                        klock_path.display()
440                    );
441                    error!(?err);
442                    return ExitCode::FAILURE;
443                }
444            };
445
446            Some(klock_path)
447        }
448    };
449
450    let result_code = kanidm_main(config, opt).await;
451
452    if let Some(klock_path) = lock_was_setup {
453        if let Err(reason) = std::fs::remove_file(&klock_path) {
454            warn!(
455                ?reason,
456                "WARNING: Unable to clean up kanidmd exclusive lock at {}",
457                klock_path.display()
458            );
459        }
460    }
461
462    result_code
463}
464
465fn main() -> ExitCode {
466    // On linux when debug assertions are disabled, prevent ptrace
467    // from attaching to us.
468    #[cfg(all(target_os = "linux", not(debug_assertions)))]
469    if let Err(code) = prctl::set_dumpable(false) {
470        println!(
471            "CRITICAL: Unable to set prctl flags, which breaches our security model, quitting! {:?}", code
472        );
473        return ExitCode::FAILURE;
474    }
475
476    // We need enough backtrace depth to find leak sources if they exist.
477    #[cfg(feature = "dhat-heap")]
478    let _profiler = dhat::Profiler::builder().trim_backtraces(Some(40)).build();
479
480    // Read CLI args, determine what the user has asked us to do.
481    let opt = KanidmdParser::parse();
482
483    // print the app version and bail
484    if let KanidmdOpt::Version = &opt.commands {
485        println!("kanidmd {}", env!("KANIDM_PKG_VERSION"));
486        return ExitCode::SUCCESS;
487    };
488
489    if env!("KANIDM_SERVER_CONFIG_PATH").is_empty() {
490        println!("CRITICAL: Kanidmd was not built correctly and is missing a valid KANIDM_SERVER_CONFIG_PATH value");
491        return ExitCode::FAILURE;
492    }
493
494    let default_config_path = PathBuf::from(env!("KANIDM_SERVER_CONFIG_PATH"));
495
496    let maybe_config_path = if let Some(p) = &opt.config_path {
497        Some(p.clone())
498    } else {
499        // The user didn't ask for a file, lets check if the default path exists?
500        if default_config_path.exists() {
501            // It does, lets use it.
502            Some(default_config_path)
503        } else {
504            // No default config, and no config specified, lets assume the user
505            // has selected environment variables.
506            None
507        }
508    };
509
510    let maybe_sconfig = if let Some(config_path) = maybe_config_path {
511        match ServerConfigUntagged::new(config_path) {
512            Ok(c) => Some(c),
513            Err(err) => {
514                eprintln!("ERROR: Configuration Parse Failure: {err:?}");
515                return ExitCode::FAILURE;
516            }
517        }
518    } else {
519        eprintln!("WARNING: No configuration path was provided, relying on environment variables.");
520        None
521    };
522
523    let envconfig = match EnvironmentConfig::new() {
524        Ok(ec) => ec,
525        Err(err) => {
526            eprintln!("ERROR: Environment Configuration Parse Failure: {err:?}");
527            return ExitCode::FAILURE;
528        }
529    };
530
531    let cli_config = CliConfig {
532        output_mode: Some(opt.output_mode.to_owned().into()),
533    };
534
535    let is_server = matches!(&opt.commands, KanidmdOpt::Server);
536
537    let config = Configuration::build()
538        .add_env_config(envconfig)
539        .add_opt_toml_config(maybe_sconfig)
540        // We always set threads to 1 unless it's the main server.
541        .add_cli_config(cli_config)
542        .is_server_mode(is_server)
543        .finish();
544
545    let Some(config) = config else {
546        eprintln!(
547            "ERROR: Unable to build server configuration from provided configuration inputs."
548        );
549        return ExitCode::FAILURE;
550    };
551
552    // ===========================================================================
553    // Config ready
554
555    // Get information on the windows username
556    #[cfg(target_family = "windows")]
557    get_user_details_windows();
558
559    // Start the runtime
560    let maybe_rt = tokio::runtime::Builder::new_multi_thread()
561        .worker_threads(config.threads)
562        .enable_all()
563        .thread_name("kanidmd-thread-pool")
564        // .thread_stack_size(8 * 1024 * 1024)
565        // If we want a hook for thread start.
566        // .on_thread_start()
567        // In future, we can stop the whole process if a panic occurs.
568        // .unhandled_panic(tokio::runtime::UnhandledPanic::ShutdownRuntime)
569        .build();
570
571    let rt = match maybe_rt {
572        Ok(rt) => rt,
573        Err(err) => {
574            eprintln!("CRITICAL: Unable to start runtime! {err:?}");
575            return ExitCode::FAILURE;
576        }
577    };
578
579    rt.block_on(start_daemon(opt, config))
580}
581
582/// Build and execute the main server. The ServerConfig are the configuration options
583/// that we are processing into the config for the main server.
584async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
585    match &opt.commands {
586        KanidmdOpt::Server | KanidmdOpt::ConfigTest => {
587            let config_test = matches!(&opt.commands, KanidmdOpt::ConfigTest);
588            if config_test {
589                info!("Running in server configuration test mode ...");
590            } else {
591                info!("Running in server mode ...");
592            };
593
594            // Verify the TLs configs.
595            if let Some(tls_config) = config.tls_config.as_ref() {
596                {
597                    let i_meta = match metadata(&tls_config.chain) {
598                        Ok(m) => m,
599                        Err(e) => {
600                            error!(
601                                "Unable to read metadata for TLS chain file '{}' - {:?}",
602                                tls_config.chain.display(),
603                                e
604                            );
605                            let diag =
606                                kanidm_lib_file_permissions::diagnose_path(&tls_config.chain);
607                            info!(%diag);
608                            return ExitCode::FAILURE;
609                        }
610                    };
611                    if !kanidm_lib_file_permissions::readonly(&i_meta) {
612                        warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", tls_config.chain.display());
613                    }
614                }
615
616                {
617                    let i_meta = match metadata(&tls_config.key) {
618                        Ok(m) => m,
619                        Err(e) => {
620                            error!(
621                                "Unable to read metadata for TLS key file '{}' - {:?}",
622                                tls_config.key.display(),
623                                e
624                            );
625                            let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.key);
626                            info!(%diag);
627                            return ExitCode::FAILURE;
628                        }
629                    };
630                    if !kanidm_lib_file_permissions::readonly(&i_meta) {
631                        warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", tls_config.key.display());
632                    }
633                    #[cfg(not(target_os = "windows"))]
634                    if i_meta.mode() & 0o007 != 0 {
635                        warn!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...", tls_config.key.display());
636                    }
637                }
638
639                if let Some(ca_dir) = tls_config.client_ca.as_ref() {
640                    // check that the TLS client CA config option is what we expect
641                    let ca_dir_path = PathBuf::from(&ca_dir);
642                    if !ca_dir_path.exists() {
643                        error!(
644                            "TLS CA folder {} does not exist, server startup will FAIL!",
645                            ca_dir.display()
646                        );
647                        let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
648                        info!(%diag);
649                    }
650
651                    let i_meta = match metadata(&ca_dir_path) {
652                        Ok(m) => m,
653                        Err(e) => {
654                            error!(
655                                "Unable to read metadata for '{}' - {:?}",
656                                ca_dir.display(),
657                                e
658                            );
659                            let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
660                            info!(%diag);
661                            return ExitCode::FAILURE;
662                        }
663                    };
664                    if !i_meta.is_dir() {
665                        error!(
666                            "ERROR: Refusing to run - TLS Client CA folder {} may not be a directory",
667                            ca_dir.display()
668                        );
669                        return ExitCode::FAILURE;
670                    }
671                    if kanidm_lib_file_permissions::readonly(&i_meta) {
672                        warn!("WARNING: TLS Client CA folder permissions on {} indicate it may not be RW. This could cause the server start up to fail!", ca_dir.display());
673                    }
674                    #[cfg(not(target_os = "windows"))]
675                    if i_meta.mode() & 0o007 != 0 {
676                        warn!("WARNING: TLS Client CA folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", ca_dir.display());
677                    }
678                }
679            }
680
681            let sctx = create_server_core(config, config_test).await;
682            if !config_test {
683                // On linux, notify systemd.
684                #[cfg(target_os = "linux")]
685                {
686                    let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
687                    let _ = sd_notify::notify(
688                        true,
689                        &[sd_notify::NotifyState::Status("Started Kanidm 🦀")],
690                    );
691                };
692
693                match sctx {
694                    Ok(mut sctx) => {
695                        loop {
696                            #[cfg(target_family = "unix")]
697                            {
698                                let mut listener = sctx.subscribe();
699                                tokio::select! {
700                                    Ok(()) = tokio::signal::ctrl_c() => {
701                                        break
702                                    }
703                                    Some(()) = async move {
704                                        let sigterm = tokio::signal::unix::SignalKind::terminate();
705                                        #[allow(clippy::unwrap_used)]
706                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
707                                    } => {
708                                        break
709                                    }
710                                    Some(()) = async move {
711                                        let sigterm = tokio::signal::unix::SignalKind::alarm();
712                                        #[allow(clippy::unwrap_used)]
713                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
714                                    } => {
715                                        // Ignore
716                                    }
717                                    Some(()) = async move {
718                                        let sigterm = tokio::signal::unix::SignalKind::hangup();
719                                        #[allow(clippy::unwrap_used)]
720                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
721                                    } => {
722                                        // Reload TLS certificates
723                                        // systemd has a special reload handler for this.
724                                        #[cfg(target_os = "linux")]
725                                        {
726                                            if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
727                                                let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Reloading, monotonic_usec]);
728                                                let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Reloading ...")]);
729                                            } else {
730                                                error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
731                                            };
732                                        }
733
734                                        sctx.tls_acceptor_reload().await;
735
736                                        // Systemd freaks out if you send the ready state too fast after the
737                                        // reload state and can kill Kanidmd as a result.
738                                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
739
740                                        #[cfg(target_os = "linux")]
741                                        {
742                                            if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
743                                                let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready, monotonic_usec]);
744                                                let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Reload Success")]);
745                                            } else {
746                                                error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
747                                            };
748                                        }
749
750                                        info!("Reload complete");
751                                    }
752                                    Some(()) = async move {
753                                        let sigterm = tokio::signal::unix::SignalKind::user_defined1();
754                                        #[allow(clippy::unwrap_used)]
755                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
756                                    } => {
757                                        // Ignore
758                                    }
759                                    Some(()) = async move {
760                                        let sigterm = tokio::signal::unix::SignalKind::user_defined2();
761                                        #[allow(clippy::unwrap_used)]
762                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
763                                    } => {
764                                        // Ignore
765                                    }
766                                    // we got a message on thr broadcast from somewhere else
767                                    Ok(msg) = async move {
768                                        listener.recv().await
769                                    } => {
770                                        debug!("Main loop received message: {:?}", msg);
771                                        break
772                                    }
773                                }
774                            }
775                            #[cfg(target_family = "windows")]
776                            {
777                                tokio::select! {
778                                    Ok(()) = tokio::signal::ctrl_c() => {
779                                        break
780                                    }
781                                }
782                            }
783                        }
784                        info!("Signal received, shutting down");
785                        // Send a broadcast that we are done.
786                        sctx.shutdown().await;
787                    }
788                    Err(_) => {
789                        error!("Failed to start server core!");
790                        // We may need to return an exit code here, but that may take some re-architecting
791                        // to ensure we drop everything cleanly.
792                        return ExitCode::FAILURE;
793                    }
794                }
795                info!("Stopped 🛑 ");
796            }
797        }
798        KanidmdOpt::CertGenerate => {
799            info!("Running in certificate generate mode ...");
800            cert_generate_core(&config);
801        }
802        KanidmdOpt::Database {
803            commands: DbCommands::Backup(bopt),
804        } => {
805            info!("Running in backup mode ...");
806            backup_server_core(&config, &bopt.path);
807        }
808        KanidmdOpt::Database {
809            commands: DbCommands::Restore(ropt),
810        } => {
811            info!("Running in restore mode ...");
812            restore_server_core(&config, &ropt.path).await;
813        }
814        KanidmdOpt::Database {
815            commands: DbCommands::Verify,
816        } => {
817            info!("Running in db verification mode ...");
818            verify_server_core(&config).await;
819        }
820        KanidmdOpt::ShowReplicationCertificate => {
821            info!("Running show replication certificate ...");
822            let output_mode: ConsoleOutputMode = opt.output_mode.into();
823            submit_admin_req(
824                config.adminbindpath.as_str(),
825                AdminTaskRequest::ShowReplicationCertificate,
826                output_mode,
827            )
828            .await;
829        }
830        KanidmdOpt::RenewReplicationCertificate => {
831            info!("Running renew replication certificate ...");
832            let output_mode: ConsoleOutputMode = opt.output_mode.into();
833            submit_admin_req(
834                config.adminbindpath.as_str(),
835                AdminTaskRequest::RenewReplicationCertificate,
836                output_mode,
837            )
838            .await;
839        }
840        KanidmdOpt::RefreshReplicationConsumer { proceed } => {
841            info!("Running refresh replication consumer ...");
842            if !proceed {
843                error!("Unwilling to proceed. Check --help.");
844            } else {
845                let output_mode: ConsoleOutputMode = opt.output_mode.into();
846                submit_admin_req(
847                    config.adminbindpath.as_str(),
848                    AdminTaskRequest::RefreshReplicationConsumer,
849                    output_mode,
850                )
851                .await;
852            }
853        }
854        KanidmdOpt::RecoverAccount { name } => {
855            info!("Running account recovery ...");
856            let output_mode: ConsoleOutputMode = opt.output_mode.into();
857            submit_admin_req(
858                config.adminbindpath.as_str(),
859                AdminTaskRequest::RecoverAccount {
860                    name: name.to_owned(),
861                },
862                output_mode,
863            )
864            .await;
865        }
866        KanidmdOpt::DisableAccount { name } => {
867            info!("Running account disable ...");
868            let output_mode: ConsoleOutputMode = opt.output_mode.into();
869            submit_admin_req(
870                config.adminbindpath.as_str(),
871                AdminTaskRequest::DisableAccount {
872                    name: name.to_owned(),
873                },
874                output_mode,
875            )
876            .await;
877        }
878        KanidmdOpt::Database {
879            commands: DbCommands::Reindex,
880        } => {
881            info!("Running in reindex mode ...");
882            reindex_server_core(&config).await;
883        }
884        KanidmdOpt::DbScan {
885            commands: DbScanOpt::ListIndexes,
886        } => {
887            info!("👀 db scan - list indexes");
888            dbscan_list_indexes_core(&config);
889        }
890        KanidmdOpt::DbScan {
891            commands: DbScanOpt::ListId2Entry,
892        } => {
893            info!("👀 db scan - list id2entry");
894            dbscan_list_id2entry_core(&config);
895        }
896        KanidmdOpt::DbScan {
897            commands: DbScanOpt::ListIndexAnalysis,
898        } => {
899            info!("👀 db scan - list index analysis");
900            dbscan_list_index_analysis_core(&config);
901        }
902        KanidmdOpt::DbScan {
903            commands: DbScanOpt::ListIndex(dopt),
904        } => {
905            info!("👀 db scan - list index content - {}", dopt.index_name);
906            dbscan_list_index_core(&config, dopt.index_name.as_str());
907        }
908        KanidmdOpt::DbScan {
909            commands: DbScanOpt::GetId2Entry(dopt),
910        } => {
911            info!("👀 db scan - get id2 entry - {}", dopt.id);
912            dbscan_get_id2entry_core(&config, dopt.id);
913        }
914
915        KanidmdOpt::DbScan {
916            commands: DbScanOpt::QuarantineId2Entry { id },
917        } => {
918            info!("☣️  db scan - quarantine id2 entry - {}", id);
919            dbscan_quarantine_id2entry_core(&config, *id);
920        }
921
922        KanidmdOpt::DbScan {
923            commands: DbScanOpt::ListQuarantined,
924        } => {
925            info!("☣️  db scan - list quarantined");
926            dbscan_list_quarantined_core(&config);
927        }
928
929        KanidmdOpt::DbScan {
930            commands: DbScanOpt::RestoreQuarantined { id },
931        } => {
932            info!("☣️  db scan - restore quarantined entry - {}", id);
933            dbscan_restore_quarantined_core(&config, *id);
934        }
935
936        KanidmdOpt::DomainSettings {
937            commands: DomainSettingsCmds::Change,
938        } => {
939            info!("Running in domain name change mode ... this may take a long time ...");
940            domain_rename_core(&config).await;
941        }
942
943        KanidmdOpt::DomainSettings {
944            commands: DomainSettingsCmds::Show,
945        } => {
946            info!("Running domain show ...");
947            let output_mode: ConsoleOutputMode = opt.output_mode.into();
948            submit_admin_req(
949                config.adminbindpath.as_str(),
950                AdminTaskRequest::DomainShow,
951                output_mode,
952            )
953            .await;
954        }
955
956        KanidmdOpt::DomainSettings {
957            commands: DomainSettingsCmds::UpgradeCheck,
958        } => {
959            info!("Running domain upgrade check ...");
960            let output_mode: ConsoleOutputMode = opt.output_mode.into();
961            submit_admin_req(
962                config.adminbindpath.as_str(),
963                AdminTaskRequest::DomainUpgradeCheck,
964                output_mode,
965            )
966            .await;
967        }
968
969        KanidmdOpt::DomainSettings {
970            commands: DomainSettingsCmds::Raise,
971        } => {
972            info!("Running domain raise ...");
973            let output_mode: ConsoleOutputMode = opt.output_mode.to_owned().into();
974            submit_admin_req(
975                config.adminbindpath.as_str(),
976                AdminTaskRequest::DomainRaise,
977                output_mode,
978            )
979            .await;
980        }
981
982        KanidmdOpt::DomainSettings {
983            commands: DomainSettingsCmds::Remigrate { level },
984        } => {
985            info!("⚠️  Running domain remigrate ...");
986            let output_mode: ConsoleOutputMode = opt.output_mode.into();
987            submit_admin_req(
988                config.adminbindpath.as_str(),
989                AdminTaskRequest::DomainRemigrate { level: *level },
990                output_mode,
991            )
992            .await;
993        }
994
995        KanidmdOpt::Database {
996            commands: DbCommands::Vacuum,
997        } => {
998            info!("Running in vacuum mode ...");
999            vacuum_server_core(&config);
1000        }
1001        KanidmdOpt::HealthCheck(sopt) => {
1002            debug!("{sopt:?}");
1003
1004            let healthcheck_url = match &sopt.check_origin {
1005                true => format!("{}/status", config.origin),
1006                false => {
1007                    // the replace covers when you specify an ipv6-capable "all" address
1008                    format!(
1009                        "https://{}/status",
1010                        config.address.replace("[::]", "localhost")
1011                    )
1012                }
1013            };
1014
1015            info!("Checking {healthcheck_url}");
1016
1017            let mut client = reqwest::ClientBuilder::new()
1018                .danger_accept_invalid_certs(!sopt.verify_tls)
1019                .danger_accept_invalid_hostnames(!sopt.verify_tls)
1020                .https_only(true);
1021
1022            client = match &config.tls_config {
1023                None => client,
1024                Some(tls_config) => {
1025                    debug!(
1026                        "Trying to load {} to build a CA cert path",
1027                        tls_config.chain.display()
1028                    );
1029                    // if the ca_cert file exists, then we'll use it
1030                    let ca_cert_path = tls_config.chain.clone();
1031                    match ca_cert_path.exists() {
1032                        true => {
1033                            let mut cert_buf = Vec::new();
1034                            if let Err(err) = std::fs::File::open(&ca_cert_path)
1035                                .and_then(|mut file| file.read_to_end(&mut cert_buf))
1036                            {
1037                                error!(
1038                                    "Failed to read {:?} from filesystem: {:?}",
1039                                    ca_cert_path, err
1040                                );
1041                                return ExitCode::FAILURE;
1042                            }
1043
1044                            let ca_chain_parsed =
1045                                match reqwest::Certificate::from_pem_bundle(&cert_buf) {
1046                                    Ok(val) => val,
1047                                    Err(e) => {
1048                                        error!(
1049                                            "Failed to parse {:?} into CA chain!\nError: {:?}",
1050                                            ca_cert_path, e
1051                                        );
1052                                        return ExitCode::FAILURE;
1053                                    }
1054                                };
1055
1056                            // Need at least 2 certs for the leaf + chain. We skip the leaf.
1057                            for cert in ca_chain_parsed.into_iter().skip(1) {
1058                                client = client.add_root_certificate(cert)
1059                            }
1060                            client
1061                        }
1062                        false => {
1063                            warn!(
1064                                "Couldn't find ca cert {} but carrying on...",
1065                                tls_config.chain.display()
1066                            );
1067                            client
1068                        }
1069                    }
1070                }
1071            };
1072            #[allow(clippy::unwrap_used)]
1073            let client = client.build().unwrap();
1074
1075            let req = match client.get(&healthcheck_url).send().await {
1076                Ok(val) => val,
1077                Err(error) => {
1078                    let error_message = {
1079                        if error.is_timeout() {
1080                            format!("Timeout connecting to url={healthcheck_url}")
1081                        } else if error.is_connect() {
1082                            format!("Connection failed: {error}")
1083                        } else {
1084                            format!("Failed to complete healthcheck: {error:?}")
1085                        }
1086                    };
1087                    error!("CRITICAL: {error_message}");
1088                    return ExitCode::FAILURE;
1089                }
1090            };
1091            debug!("Request: {req:?}");
1092            let output_mode: ConsoleOutputMode = opt.output_mode.to_owned().into();
1093            match output_mode {
1094                ConsoleOutputMode::JSON => {
1095                    println!("{{\"result\":\"OK\"}}")
1096                }
1097                ConsoleOutputMode::Text => {
1098                    info!("OK")
1099                }
1100            }
1101        }
1102        KanidmdOpt::Version => {}
1103    }
1104    ExitCode::SUCCESS
1105}