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")] use whoami;
25
26use std::fs::{metadata, File};
27use 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#[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 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 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 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 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 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 } }
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
316fn check_file_ownership(opt: &KanidmdParser) -> Result<(), ExitCode> {
318 #[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 }
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 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 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 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 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
497async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
499 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 info!(version = %env!("KANIDM_PKG_VERSION"), "Starting Kanidmd");
524
525 let _otelguard = TracingPipelineGuard(provider);
527
528 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 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 KanidmdOpt::ShowReplicationCertificate
585 | KanidmdOpt::RenewReplicationCertificate
586 | KanidmdOpt::RefreshReplicationConsumer { .. }
587 | KanidmdOpt::RecoverAccount { .. }
588 | KanidmdOpt::DisableAccount { .. } => None,
589 _ => {
590 #[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 #[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 #[cfg(feature = "dhat-heap")]
653 let _profiler = dhat::Profiler::builder().trim_backtraces(Some(40)).build();
654
655 let opt = KanidmdParser::parse();
657
658 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 if default_config_path.exists() {
676 Some(default_config_path)
678 } else {
679 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 .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 #[cfg(target_family = "windows")]
719 get_user_details_windows();
720
721 let maybe_rt = tokio::runtime::Builder::new_multi_thread()
723 .worker_threads(config.threads)
724 .enable_all()
725 .thread_name("kanidmd-thread-pool")
726 .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 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
750async 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 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 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 #[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 }
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 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 }
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 }
907 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 sctx.shutdown().await;
929 }
930 Err(_) => {
931 error!("Failed to start server core!");
932 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}