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::{Configuration, 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    info!(version = %env!("KANIDM_PKG_VERSION"), "Starting Kanidmd");
345
346    // guard which shuts down the logging/tracing providers when we close out
347    let _otelguard = TracingPipelineGuard(provider); // TODO: fix this up
348
349    // ===========================================================================
350    // Start pre-run checks
351
352    // Check the permissions of the files from the configuration.
353    if let Err(err) = check_file_ownership(&opt) {
354        return err;
355    };
356
357    if let Some(db_path) = config.db_path.as_ref() {
358        let db_pathbuf = db_path.to_path_buf();
359        // We can't check the db_path permissions because it may not exist yet!
360        if let Some(db_parent_path) = db_pathbuf.parent() {
361            if !db_parent_path.exists() {
362                warn!(
363                    "DB folder {} may not exist, server startup may FAIL!",
364                    db_parent_path.to_str().unwrap_or("invalid file path")
365                );
366                let diag = kanidm_lib_file_permissions::diagnose_path(&db_pathbuf);
367                info!(%diag);
368            }
369
370            let db_par_path_buf = db_parent_path.to_path_buf();
371            let i_meta = match metadata(&db_par_path_buf) {
372                Ok(m) => m,
373                Err(e) => {
374                    error!(
375                        "Unable to read metadata for database folder '{}' - {:?}",
376                        &db_par_path_buf.to_str().unwrap_or("invalid file path"),
377                        e
378                    );
379                    return ExitCode::FAILURE;
380                }
381            };
382            if !i_meta.is_dir() {
383                error!(
384                    "ERROR: Refusing to run - DB folder {} may not be a directory",
385                    db_par_path_buf.to_str().unwrap_or("invalid file path")
386                );
387                return ExitCode::FAILURE;
388            }
389
390            if kanidm_lib_file_permissions::readonly(&i_meta) {
391                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"));
392            }
393            #[cfg(not(target_os = "windows"))]
394            if i_meta.mode() & 0o007 != 0 {
395                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"));
396            }
397        }
398    } else {
399        error!("No db_path set in configuration, server startup will FAIL!");
400        return ExitCode::FAILURE;
401    }
402
403    let lock_was_setup = match &opt.commands {
404        // we aren't going to touch the DB so we can carry on
405        KanidmdOpt::ShowReplicationCertificate
406        | KanidmdOpt::RenewReplicationCertificate
407        | KanidmdOpt::RefreshReplicationConsumer { .. }
408        | KanidmdOpt::RecoverAccount { .. }
409        | KanidmdOpt::DisableAccount { .. }
410        | KanidmdOpt::HealthCheck(_) => None,
411        _ => {
412            // Okay - Lets now create our lock and go.
413            #[allow(clippy::expect_used)]
414            let klock_path = match config.db_path.clone() {
415                Some(val) => val.with_extension("klock"),
416                None => std::env::temp_dir().join("kanidmd.klock"),
417            };
418
419            let flock = match File::create(&klock_path) {
420                Ok(flock) => flock,
421                Err(err) => {
422                    error!(
423                        "ERROR: Refusing to start - unable to create kanidmd exclusive lock at {}",
424                        klock_path.display()
425                    );
426                    error!(?err);
427                    return ExitCode::FAILURE;
428                }
429            };
430
431            match flock.try_lock_exclusive() {
432                Ok(true) => debug!("Acquired kanidm exclusive lock"),
433                Ok(false) => {
434                    error!(
435                        "ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {}",
436                        klock_path.display()
437                    );
438                    error!("Is another kanidmd process running?");
439                    return ExitCode::FAILURE;
440                }
441                Err(err) => {
442                    error!(
443                        "ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {}",
444                        klock_path.display()
445                    );
446                    error!(?err);
447                    return ExitCode::FAILURE;
448                }
449            };
450
451            Some(klock_path)
452        }
453    };
454
455    let result_code = kanidm_main(config, opt).await;
456
457    if let Some(klock_path) = lock_was_setup {
458        if let Err(reason) = std::fs::remove_file(&klock_path) {
459            warn!(
460                ?reason,
461                "WARNING: Unable to clean up kanidmd exclusive lock at {}",
462                klock_path.display()
463            );
464        }
465    }
466
467    result_code
468}
469
470fn main() -> ExitCode {
471    // On linux when debug assertions are disabled, prevent ptrace
472    // from attaching to us.
473    #[cfg(all(target_os = "linux", not(debug_assertions)))]
474    if let Err(code) = prctl::set_dumpable(false) {
475        println!(
476            "CRITICAL: Unable to set prctl flags, which breaches our security model, quitting! {:?}", code
477        );
478        return ExitCode::FAILURE;
479    }
480
481    // We need enough backtrace depth to find leak sources if they exist.
482    #[cfg(feature = "dhat-heap")]
483    let _profiler = dhat::Profiler::builder().trim_backtraces(Some(40)).build();
484
485    // Read CLI args, determine what the user has asked us to do.
486    let opt = KanidmdParser::parse();
487
488    // print the app version and bail
489    if let KanidmdOpt::Version = &opt.commands {
490        println!("kanidmd {}", env!("KANIDM_PKG_VERSION"));
491        return ExitCode::SUCCESS;
492    };
493
494    if env!("KANIDM_SERVER_CONFIG_PATH").is_empty() {
495        println!("CRITICAL: Kanidmd was not built correctly and is missing a valid KANIDM_SERVER_CONFIG_PATH value");
496        return ExitCode::FAILURE;
497    }
498
499    let default_config_path = PathBuf::from(env!("KANIDM_SERVER_CONFIG_PATH"));
500
501    let maybe_config_path = if let Some(p) = &opt.config_path {
502        Some(p.clone())
503    } else {
504        // The user didn't ask for a file, lets check if the default path exists?
505        if default_config_path.exists() {
506            // It does, lets use it.
507            Some(default_config_path)
508        } else {
509            // No default config, and no config specified, lets assume the user
510            // has selected environment variables.
511            None
512        }
513    };
514
515    let maybe_sconfig = if let Some(config_path) = maybe_config_path {
516        match ServerConfigUntagged::new(config_path) {
517            Ok(c) => Some(c),
518            Err(err) => {
519                eprintln!("ERROR: Configuration Parse Failure: {err:?}");
520                return ExitCode::FAILURE;
521            }
522        }
523    } else {
524        eprintln!("WARNING: No configuration path was provided, relying on environment variables.");
525        None
526    };
527
528    let is_server = matches!(&opt.commands, KanidmdOpt::Server);
529
530    let config = Configuration::build()
531        .add_opt_toml_config(maybe_sconfig)
532        .add_cli_config(&opt.kanidmd_options)
533        // set threads to 1 unless it's the main server.
534        .is_server_mode(is_server)
535        .finish();
536
537    let Some(config) = config else {
538        eprintln!(
539            "ERROR: Unable to build server configuration from provided configuration inputs."
540        );
541        return ExitCode::FAILURE;
542    };
543
544    // ===========================================================================
545    // Config ready
546
547    // Get information on the windows username
548    #[cfg(target_family = "windows")]
549    get_user_details_windows();
550
551    // Start the runtime
552    let maybe_rt = tokio::runtime::Builder::new_multi_thread()
553        .worker_threads(config.threads)
554        .enable_all()
555        .thread_name("kanidmd-thread-pool")
556        // .thread_stack_size(8 * 1024 * 1024)
557        // If we want a hook for thread start.
558        // .on_thread_start()
559        // In future, we can stop the whole process if a panic occurs.
560        // .unhandled_panic(tokio::runtime::UnhandledPanic::ShutdownRuntime)
561        .build();
562
563    let rt = match maybe_rt {
564        Ok(rt) => rt,
565        Err(err) => {
566            eprintln!("CRITICAL: Unable to start runtime! {err:?}");
567            return ExitCode::FAILURE;
568        }
569    };
570
571    rt.block_on(start_daemon(opt, config))
572}
573
574/// Build and execute the main server. The ServerConfig are the configuration options
575/// that we are processing into the config for the main server.
576async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
577    match &opt.commands {
578        KanidmdOpt::Server | KanidmdOpt::ConfigTest => {
579            let config_test = matches!(&opt.commands, KanidmdOpt::ConfigTest);
580            if config_test {
581                info!("Running in server configuration test mode ...");
582            } else {
583                info!("Running in server mode ...");
584            };
585
586            // Verify the TLs configs.
587            if let Some(tls_config) = config.tls_config.as_ref() {
588                {
589                    let i_meta = match metadata(&tls_config.chain) {
590                        Ok(m) => m,
591                        Err(e) => {
592                            error!(
593                                "Unable to read metadata for TLS chain file '{}' - {:?}",
594                                tls_config.chain.display(),
595                                e
596                            );
597                            let diag =
598                                kanidm_lib_file_permissions::diagnose_path(&tls_config.chain);
599                            info!(%diag);
600                            return ExitCode::FAILURE;
601                        }
602                    };
603                    if !kanidm_lib_file_permissions::readonly(&i_meta) {
604                        warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", tls_config.chain.display());
605                    }
606                }
607
608                {
609                    let i_meta = match metadata(&tls_config.key) {
610                        Ok(m) => m,
611                        Err(e) => {
612                            error!(
613                                "Unable to read metadata for TLS key file '{}' - {:?}",
614                                tls_config.key.display(),
615                                e
616                            );
617                            let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.key);
618                            info!(%diag);
619                            return ExitCode::FAILURE;
620                        }
621                    };
622                    if !kanidm_lib_file_permissions::readonly(&i_meta) {
623                        warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", tls_config.key.display());
624                    }
625                    #[cfg(not(target_os = "windows"))]
626                    if i_meta.mode() & 0o007 != 0 {
627                        warn!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...", tls_config.key.display());
628                    }
629                }
630
631                if let Some(ca_dir) = tls_config.client_ca.as_ref() {
632                    // check that the TLS client CA config option is what we expect
633                    let ca_dir_path = PathBuf::from(&ca_dir);
634                    if !ca_dir_path.exists() {
635                        error!(
636                            "TLS CA folder {} does not exist, server startup will FAIL!",
637                            ca_dir.display()
638                        );
639                        let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
640                        info!(%diag);
641                    }
642
643                    let i_meta = match metadata(&ca_dir_path) {
644                        Ok(m) => m,
645                        Err(e) => {
646                            error!(
647                                "Unable to read metadata for '{}' - {:?}",
648                                ca_dir.display(),
649                                e
650                            );
651                            let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
652                            info!(%diag);
653                            return ExitCode::FAILURE;
654                        }
655                    };
656                    if !i_meta.is_dir() {
657                        error!(
658                            "ERROR: Refusing to run - TLS Client CA folder {} may not be a directory",
659                            ca_dir.display()
660                        );
661                        return ExitCode::FAILURE;
662                    }
663                    if kanidm_lib_file_permissions::readonly(&i_meta) {
664                        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());
665                    }
666                    #[cfg(not(target_os = "windows"))]
667                    if i_meta.mode() & 0o007 != 0 {
668                        warn!("WARNING: TLS Client CA folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", ca_dir.display());
669                    }
670                }
671            }
672
673            let sctx = create_server_core(config, config_test).await;
674            if !config_test {
675                // On linux, notify systemd.
676                #[cfg(target_os = "linux")]
677                {
678                    let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
679                    let _ = sd_notify::notify(
680                        true,
681                        &[sd_notify::NotifyState::Status("Started Kanidm 🦀")],
682                    );
683                };
684
685                match sctx {
686                    Ok(mut sctx) => {
687                        loop {
688                            #[cfg(target_family = "unix")]
689                            {
690                                let mut listener = sctx.subscribe();
691                                tokio::select! {
692                                    Ok(()) = tokio::signal::ctrl_c() => {
693                                        break
694                                    }
695                                    Some(()) = async move {
696                                        let sigterm = tokio::signal::unix::SignalKind::terminate();
697                                        #[allow(clippy::unwrap_used)]
698                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
699                                    } => {
700                                        break
701                                    }
702                                    Some(()) = async move {
703                                        let sigterm = tokio::signal::unix::SignalKind::alarm();
704                                        #[allow(clippy::unwrap_used)]
705                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
706                                    } => {
707                                        // Ignore
708                                    }
709                                    Some(()) = async move {
710                                        let sigterm = tokio::signal::unix::SignalKind::hangup();
711                                        #[allow(clippy::unwrap_used)]
712                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
713                                    } => {
714                                        // Reload TLS certificates
715                                        sctx.tls_acceptor_reload().await;
716                                        info!("Reload complete");
717                                    }
718                                    Some(()) = async move {
719                                        let sigterm = tokio::signal::unix::SignalKind::user_defined1();
720                                        #[allow(clippy::unwrap_used)]
721                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
722                                    } => {
723                                        // Ignore
724                                    }
725                                    Some(()) = async move {
726                                        let sigterm = tokio::signal::unix::SignalKind::user_defined2();
727                                        #[allow(clippy::unwrap_used)]
728                                        tokio::signal::unix::signal(sigterm).unwrap().recv().await
729                                    } => {
730                                        // Ignore
731                                    }
732                                    // we got a message on thr broadcast from somewhere else
733                                    Ok(msg) = async move {
734                                        listener.recv().await
735                                    } => {
736                                        debug!("Main loop received message: {:?}", msg);
737                                        break
738                                    }
739                                }
740                            }
741                            #[cfg(target_family = "windows")]
742                            {
743                                tokio::select! {
744                                    Ok(()) = tokio::signal::ctrl_c() => {
745                                        break
746                                    }
747                                }
748                            }
749                        }
750                        info!("Signal received, shutting down");
751                        // Send a broadcast that we are done.
752                        sctx.shutdown().await;
753                    }
754                    Err(_) => {
755                        error!("Failed to start server core!");
756                        // We may need to return an exit code here, but that may take some re-architecting
757                        // to ensure we drop everything cleanly.
758                        return ExitCode::FAILURE;
759                    }
760                }
761                info!("Stopped 🛑 ");
762            }
763        }
764        KanidmdOpt::CertGenerate => {
765            info!("Running in certificate generate mode ...");
766            cert_generate_core(&config);
767        }
768        KanidmdOpt::Database {
769            commands: DbCommands::Backup(bopt),
770        } => {
771            info!("Running in backup mode ...");
772
773            backup_server_core(&config, &bopt.path);
774        }
775        KanidmdOpt::Database {
776            commands: DbCommands::Restore(ropt),
777        } => {
778            info!("Running in restore mode ...");
779            restore_server_core(&config, &ropt.path).await;
780        }
781        KanidmdOpt::Database {
782            commands: DbCommands::Verify,
783        } => {
784            info!("Running in db verification mode ...");
785            verify_server_core(&config).await;
786        }
787        KanidmdOpt::ShowReplicationCertificate => {
788            info!("Running show replication certificate ...");
789            submit_admin_req(
790                config.adminbindpath.as_str(),
791                AdminTaskRequest::ShowReplicationCertificate,
792                opt.kanidmd_options.output_mode,
793            )
794            .await;
795        }
796        KanidmdOpt::RenewReplicationCertificate => {
797            info!("Running renew replication certificate ...");
798            submit_admin_req(
799                config.adminbindpath.as_str(),
800                AdminTaskRequest::RenewReplicationCertificate,
801                opt.kanidmd_options.output_mode,
802            )
803            .await;
804        }
805        KanidmdOpt::RefreshReplicationConsumer { proceed } => {
806            info!("Running refresh replication consumer ...");
807            if !proceed {
808                error!("Unwilling to proceed. Check --help.");
809            } else {
810                submit_admin_req(
811                    config.adminbindpath.as_str(),
812                    AdminTaskRequest::RefreshReplicationConsumer,
813                    opt.kanidmd_options.output_mode,
814                )
815                .await;
816            }
817        }
818        KanidmdOpt::RecoverAccount { name } => {
819            info!("Running account recovery ...");
820
821            submit_admin_req(
822                config.adminbindpath.as_str(),
823                AdminTaskRequest::RecoverAccount {
824                    name: name.to_owned(),
825                },
826                opt.kanidmd_options.output_mode,
827            )
828            .await;
829        }
830        KanidmdOpt::DisableAccount { name } => {
831            info!("Running account disable ...");
832
833            submit_admin_req(
834                config.adminbindpath.as_str(),
835                AdminTaskRequest::DisableAccount {
836                    name: name.to_owned(),
837                },
838                opt.kanidmd_options.output_mode,
839            )
840            .await;
841        }
842        KanidmdOpt::Database {
843            commands: DbCommands::Reindex,
844        } => {
845            info!("Running in reindex mode ...");
846            reindex_server_core(&config).await;
847        }
848        KanidmdOpt::DbScan {
849            commands: DbScanOpt::ListIndexes,
850        } => {
851            info!("👀 db scan - list indexes");
852            dbscan_list_indexes_core(&config);
853        }
854        KanidmdOpt::DbScan {
855            commands: DbScanOpt::ListId2Entry,
856        } => {
857            info!("👀 db scan - list id2entry");
858            dbscan_list_id2entry_core(&config);
859        }
860        KanidmdOpt::DbScan {
861            commands: DbScanOpt::ListIndexAnalysis,
862        } => {
863            info!("👀 db scan - list index analysis");
864            dbscan_list_index_analysis_core(&config);
865        }
866        KanidmdOpt::DbScan {
867            commands: DbScanOpt::ListIndex(dopt),
868        } => {
869            info!("👀 db scan - list index content - {}", dopt.index_name);
870            dbscan_list_index_core(&config, dopt.index_name.as_str());
871        }
872        KanidmdOpt::DbScan {
873            commands: DbScanOpt::GetId2Entry(dopt),
874        } => {
875            info!("👀 db scan - get id2 entry - {}", dopt.id);
876            dbscan_get_id2entry_core(&config, dopt.id);
877        }
878
879        KanidmdOpt::DbScan {
880            commands: DbScanOpt::QuarantineId2Entry { id },
881        } => {
882            info!("☣️  db scan - quarantine id2 entry - {}", id);
883            dbscan_quarantine_id2entry_core(&config, *id);
884        }
885
886        KanidmdOpt::DbScan {
887            commands: DbScanOpt::ListQuarantined,
888        } => {
889            info!("☣️  db scan - list quarantined");
890            dbscan_list_quarantined_core(&config);
891        }
892
893        KanidmdOpt::DbScan {
894            commands: DbScanOpt::RestoreQuarantined { id },
895        } => {
896            info!("☣️  db scan - restore quarantined entry - {}", id);
897            dbscan_restore_quarantined_core(&config, *id);
898        }
899
900        KanidmdOpt::DomainSettings {
901            commands: DomainSettingsCmds::Change,
902        } => {
903            info!("Running in domain name change mode ... this may take a long time ...");
904            domain_rename_core(&config).await;
905        }
906
907        KanidmdOpt::DomainSettings {
908            commands: DomainSettingsCmds::Show,
909        } => {
910            info!("Running domain show ...");
911
912            submit_admin_req(
913                config.adminbindpath.as_str(),
914                AdminTaskRequest::DomainShow,
915                opt.kanidmd_options.output_mode,
916            )
917            .await;
918        }
919
920        KanidmdOpt::DomainSettings {
921            commands: DomainSettingsCmds::UpgradeCheck,
922        } => {
923            info!("Running domain upgrade check ...");
924
925            submit_admin_req(
926                config.adminbindpath.as_str(),
927                AdminTaskRequest::DomainUpgradeCheck,
928                opt.kanidmd_options.output_mode,
929            )
930            .await;
931        }
932
933        KanidmdOpt::DomainSettings {
934            commands: DomainSettingsCmds::Raise,
935        } => {
936            info!("Running domain raise ...");
937
938            submit_admin_req(
939                config.adminbindpath.as_str(),
940                AdminTaskRequest::DomainRaise,
941                opt.kanidmd_options.output_mode,
942            )
943            .await;
944        }
945
946        KanidmdOpt::DomainSettings {
947            commands: DomainSettingsCmds::Remigrate { level },
948        } => {
949            info!("⚠️  Running domain remigrate ...");
950
951            submit_admin_req(
952                config.adminbindpath.as_str(),
953                AdminTaskRequest::DomainRemigrate { level: *level },
954                opt.kanidmd_options.output_mode,
955            )
956            .await;
957        }
958
959        KanidmdOpt::Database {
960            commands: DbCommands::Vacuum,
961        } => {
962            info!("Running in vacuum mode ...");
963            vacuum_server_core(&config);
964        }
965        KanidmdOpt::HealthCheck(sopt) => {
966            debug!("{sopt:?}");
967
968            let healthcheck_url = match &sopt.check_origin {
969                true => format!("{}/status", config.origin),
970                false => {
971                    // the replace covers when you specify an ipv6-capable "all" address
972                    format!(
973                        "https://{}/status",
974                        config.address[0].replace("[::]", "localhost")
975                    )
976                }
977            };
978
979            info!("Checking {healthcheck_url}");
980
981            let mut client = reqwest::ClientBuilder::new()
982                .danger_accept_invalid_certs(!sopt.verify_tls)
983                .danger_accept_invalid_hostnames(!sopt.verify_tls)
984                .https_only(true);
985
986            client = match &config.tls_config {
987                None => client,
988                Some(tls_config) => {
989                    debug!(
990                        "Trying to load {} to build a CA cert path",
991                        tls_config.chain.display()
992                    );
993                    // if the ca_cert file exists, then we'll use it
994                    let ca_cert_path = tls_config.chain.clone();
995                    match ca_cert_path.exists() {
996                        true => {
997                            let mut cert_buf = Vec::new();
998                            if let Err(err) = std::fs::File::open(&ca_cert_path)
999                                .and_then(|mut file| file.read_to_end(&mut cert_buf))
1000                            {
1001                                error!(
1002                                    "Failed to read {:?} from filesystem: {:?}",
1003                                    ca_cert_path, err
1004                                );
1005                                return ExitCode::FAILURE;
1006                            }
1007
1008                            let ca_chain_parsed =
1009                                match reqwest::Certificate::from_pem_bundle(&cert_buf) {
1010                                    Ok(val) => val,
1011                                    Err(e) => {
1012                                        error!(
1013                                            "Failed to parse {:?} into CA chain!\nError: {:?}",
1014                                            ca_cert_path, e
1015                                        );
1016                                        return ExitCode::FAILURE;
1017                                    }
1018                                };
1019
1020                            // Need at least 2 certs for the leaf + chain. We skip the leaf.
1021                            for cert in ca_chain_parsed.into_iter().skip(1) {
1022                                client = client.add_root_certificate(cert)
1023                            }
1024                            client
1025                        }
1026                        false => {
1027                            warn!(
1028                                "Couldn't find ca cert {} but carrying on...",
1029                                tls_config.chain.display()
1030                            );
1031                            client
1032                        }
1033                    }
1034                }
1035            };
1036            #[allow(clippy::unwrap_used)]
1037            let client = client.build().unwrap();
1038
1039            let req = match client.get(&healthcheck_url).send().await {
1040                Ok(val) => val,
1041                Err(error) => {
1042                    let error_message = {
1043                        if error.is_timeout() {
1044                            format!("Timeout connecting to url={healthcheck_url}")
1045                        } else if error.is_connect() {
1046                            format!("Connection failed: {error}")
1047                        } else {
1048                            format!("Failed to complete healthcheck: {error:?}")
1049                        }
1050                    };
1051                    error!("CRITICAL: {error_message}");
1052                    return ExitCode::FAILURE;
1053                }
1054            };
1055            debug!("Request: {req:?}");
1056            match opt.kanidmd_options.output_mode {
1057                ConsoleOutputMode::JSON => {
1058                    println!("{{\"result\":\"OK\"}}")
1059                }
1060                ConsoleOutputMode::Text => {
1061                    info!("OK")
1062                }
1063            }
1064        }
1065        KanidmdOpt::Version => {}
1066    }
1067    ExitCode::SUCCESS
1068}