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