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