Skip to main content

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