1use cidr::IpCidr;
8use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
9use kanidm_proto::internal::FsType;
10use kanidm_proto::messages::ConsoleOutputMode;
11use serde::Deserialize;
12use sketching::LogLevel;
13use std::fmt::{self, Display};
14use std::fs::File;
15use std::io::Read;
16use std::net::IpAddr;
17use std::path::{Path, PathBuf};
18use std::str::FromStr;
19use url::Url;
20
21use crate::repl::config::ReplicationConfiguration;
22
23#[derive(Debug, Deserialize)]
24struct VersionDetection {
25 #[serde(default)]
26 version: Version,
27}
28
29#[derive(Debug, Deserialize, Default)]
30pub enum Version {
32 #[serde(rename = "2")]
33 V2,
34
35 #[default]
36 Legacy,
37}
38
39#[allow(clippy::large_enum_variant)]
41pub enum ServerConfigUntagged {
42 Version(ServerConfigVersion),
43 Legacy(ServerConfig),
44}
45
46pub enum ServerConfigVersion {
47 V2 { values: ServerConfigV2 },
48}
49
50#[derive(Deserialize, Debug, Clone)]
51pub struct OnlineBackup {
52 pub path: Option<PathBuf>,
54 pub schedule: String,
72 #[serde(default = "default_online_backup_versions")]
73 pub versions: usize,
75 #[serde(default = "default_online_backup_enabled")]
77 pub enabled: bool,
78}
79
80impl Default for OnlineBackup {
81 fn default() -> Self {
82 OnlineBackup {
83 path: None, schedule: default_online_backup_schedule(),
85 versions: default_online_backup_versions(),
86 enabled: default_online_backup_enabled(),
87 }
88 }
89}
90
91fn default_online_backup_enabled() -> bool {
92 true
93}
94
95fn default_online_backup_schedule() -> String {
96 "@daily".to_string()
97}
98
99fn default_online_backup_versions() -> usize {
100 7
101}
102
103#[derive(Deserialize, Debug, Clone)]
104pub struct TlsConfiguration {
105 pub chain: PathBuf,
106 pub key: PathBuf,
107 pub client_ca: Option<PathBuf>,
108}
109
110#[derive(Deserialize, Debug, Clone, Default)]
111pub enum LdapAddressInfo {
112 #[default]
113 None,
114 #[serde(rename = "proxy-v2")]
115 ProxyV2(Vec<IpCidr>),
116}
117
118impl LdapAddressInfo {
119 pub fn trusted_proxy_v2(&self) -> Option<Vec<IpCidr>> {
120 if let Self::ProxyV2(trusted) = self {
121 Some(trusted.clone())
122 } else {
123 None
124 }
125 }
126}
127
128impl Display for LdapAddressInfo {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::None => f.write_str("none"),
132 Self::ProxyV2(trusted) => {
133 f.write_str("proxy-v2 [ ")?;
134 for ip in trusted {
135 write!(f, "{} ", ip)?;
136 }
137 f.write_str("]")
138 }
139 }
140 }
141}
142
143pub(crate) enum AddressSet {
144 NonContiguousIpSet(Vec<IpCidr>),
145 All,
146}
147
148impl AddressSet {
149 pub(crate) fn contains(&self, ip_addr: &IpAddr) -> bool {
150 match self {
151 Self::All => true,
152 Self::NonContiguousIpSet(range) => {
153 range.iter().any(|ip_cidr| ip_cidr.contains(ip_addr))
154 }
155 }
156 }
157}
158
159#[derive(Deserialize, Debug, Clone, Default)]
160pub enum HttpAddressInfo {
161 #[default]
162 None,
163 #[serde(rename = "x-forward-for")]
164 XForwardFor(Vec<IpCidr>),
165 #[serde(rename = "x-forward-for-all-source-trusted")]
168 XForwardForAllSourcesTrusted,
169 #[serde(rename = "proxy-v2")]
170 ProxyV2(Vec<IpCidr>),
171}
172
173impl HttpAddressInfo {
174 pub(crate) fn trusted_x_forward_for(&self) -> Option<AddressSet> {
175 match self {
176 Self::XForwardForAllSourcesTrusted => Some(AddressSet::All),
177 Self::XForwardFor(trusted) => Some(AddressSet::NonContiguousIpSet(trusted.clone())),
178 _ => None,
179 }
180 }
181
182 pub(crate) fn trusted_proxy_v2(&self) -> Option<Vec<IpCidr>> {
183 if let Self::ProxyV2(trusted) = self {
184 Some(trusted.clone())
185 } else {
186 None
187 }
188 }
189}
190
191impl Display for HttpAddressInfo {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Self::None => f.write_str("none"),
195
196 Self::XForwardFor(trusted) => {
197 f.write_str("x-forward-for [ ")?;
198 for ip in trusted {
199 write!(f, "{} ", ip)?;
200 }
201 f.write_str("]")
202 }
203 Self::XForwardForAllSourcesTrusted => {
204 f.write_str("x-forward-for [ ALL SOURCES TRUSTED ]")
205 }
206 Self::ProxyV2(trusted) => {
207 f.write_str("proxy-v2 [ ")?;
208 for ip in trusted {
209 write!(f, "{} ", ip)?;
210 }
211 f.write_str("]")
212 }
213 }
214 }
215}
216
217#[derive(Debug, Deserialize, Default)]
226#[serde(deny_unknown_fields)]
227pub struct ServerConfig {
228 domain: Option<String>,
230 origin: Option<Url>,
232 db_path: Option<PathBuf>,
234 db_fs_type: Option<kanidm_proto::internal::FsType>,
236
237 tls_chain: Option<PathBuf>,
239 tls_key: Option<PathBuf>,
241
242 tls_client_ca: Option<PathBuf>,
244
245 bindaddress: Option<String>,
249 ldapbindaddress: Option<String>,
255 role: Option<ServerRole>,
257 log_level: Option<LogLevel>,
259
260 online_backup: Option<OnlineBackup>,
262
263 trust_x_forward_for: Option<bool>,
265
266 adminbindpath: Option<String>,
268
269 thread_count: Option<usize>,
272
273 maximum_request_size_bytes: Option<usize>,
275
276 #[allow(dead_code)]
278 db_arc_size: Option<usize>,
279 #[serde(default)]
280 #[serde(rename = "replication")]
281 repl_config: Option<ReplicationConfiguration>,
283 otel_grpc_url: Option<String>,
285}
286
287impl ServerConfigUntagged {
288 pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
290 eprintln!("📜 Using config file: {:?}", config_path.as_ref());
292 let mut f: File = File::open(config_path.as_ref()).inspect_err(|e| {
293 eprintln!("Unable to open config file [{:?}] 🥺", e);
294 let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
295 eprintln!("{}", diag);
296 })?;
297
298 let mut contents = String::new();
299
300 f.read_to_string(&mut contents).inspect_err(|e| {
301 eprintln!("unable to read contents {:?}", e);
302 let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
303 eprintln!("{}", diag);
304 })?;
305
306 let config_version = toml::from_str::<VersionDetection>(contents.as_str())
308 .map(|vd| vd.version)
309 .map_err(|err| {
310 eprintln!(
311 "Unable to parse config version from '{:?}': {:?}",
312 config_path.as_ref(),
313 err
314 );
315 std::io::Error::new(std::io::ErrorKind::InvalidData, err)
316 })?;
317
318 match config_version {
319 Version::V2 => toml::from_str::<ServerConfigV2>(contents.as_str())
320 .map(|values| ServerConfigUntagged::Version(ServerConfigVersion::V2 { values })),
321 Version::Legacy => {
322 toml::from_str::<ServerConfig>(contents.as_str()).map(ServerConfigUntagged::Legacy)
323 }
324 }
325 .map_err(|err| {
326 eprintln!(
327 "Unable to parse config from '{:?}': {:?}",
328 config_path.as_ref(),
329 err
330 );
331 std::io::Error::new(std::io::ErrorKind::InvalidData, err)
332 })
333 }
334}
335
336#[derive(Debug, Deserialize, Default)]
337#[serde(deny_unknown_fields)]
338pub struct ServerConfigV2 {
339 #[allow(dead_code)]
340 version: String,
341 domain: Option<String>,
342 origin: Option<Url>,
343 db_path: Option<PathBuf>,
344 db_fs_type: Option<kanidm_proto::internal::FsType>,
345 tls_chain: Option<PathBuf>,
346 tls_key: Option<PathBuf>,
347 tls_client_ca: Option<PathBuf>,
348 bindaddress: Option<String>,
349 ldapbindaddress: Option<String>,
350 role: Option<ServerRole>,
351 log_level: Option<LogLevel>,
352 online_backup: Option<OnlineBackup>,
353
354 http_client_address_info: Option<HttpAddressInfo>,
355 ldap_client_address_info: Option<LdapAddressInfo>,
356
357 adminbindpath: Option<String>,
358 thread_count: Option<usize>,
359 maximum_request_size_bytes: Option<usize>,
360 #[allow(dead_code)]
361 db_arc_size: Option<usize>,
362 #[serde(default)]
363 #[serde(rename = "replication")]
364 repl_config: Option<ReplicationConfiguration>,
365 otel_grpc_url: Option<String>,
366}
367
368#[derive(Default)]
369pub struct CliConfig {
370 pub output_mode: Option<ConsoleOutputMode>,
371}
372
373#[derive(Default)]
374pub struct EnvironmentConfig {
375 domain: Option<String>,
376 origin: Option<Url>,
377 db_path: Option<PathBuf>,
378 tls_chain: Option<PathBuf>,
379 tls_key: Option<PathBuf>,
380 tls_client_ca: Option<PathBuf>,
381 bindaddress: Option<String>,
382 ldapbindaddress: Option<String>,
383 role: Option<ServerRole>,
384 log_level: Option<LogLevel>,
385 online_backup: Option<OnlineBackup>,
386 trust_x_forward_for: Option<bool>,
387 db_fs_type: Option<kanidm_proto::internal::FsType>,
388 adminbindpath: Option<String>,
389 db_arc_size: Option<usize>,
390 repl_config: Option<ReplicationConfiguration>,
391 otel_grpc_url: Option<String>,
392}
393
394impl EnvironmentConfig {
395 pub fn new() -> Result<Self, String> {
397 let mut env_config = Self::default();
398
399 for (key, value) in std::env::vars() {
400 let Some(key) = key.strip_prefix("KANIDM_") else {
401 continue;
402 };
403
404 let ignorable_build_fields = [
405 "CPU_FLAGS",
406 "DEFAULT_CONFIG_PATH",
407 "DEFAULT_UNIX_SHELL_PATH",
408 "HTMX_UI_PKG_PATH",
409 "PKG_VERSION",
410 "PKG_VERSION_HASH",
411 "PRE_RELEASE",
412 "PROFILE_NAME",
413 ];
414
415 if ignorable_build_fields.contains(&key) {
416 #[cfg(any(debug_assertions, test))]
417 eprintln!("-- Ignoring build-time env var KANIDM_{key}");
418 continue;
419 }
420
421 match key {
422 "DOMAIN" => {
423 env_config.domain = Some(value.to_string());
424 }
425 "ORIGIN" => {
426 let url = Url::parse(value.as_str())
427 .map_err(|err| format!("Failed to parse KANIDM_ORIGIN as URL: {}", err))?;
428 env_config.origin = Some(url);
429 }
430 "DB_PATH" => {
431 env_config.db_path = Some(PathBuf::from(value.to_string()));
432 }
433 "TLS_CHAIN" => {
434 env_config.tls_chain = Some(PathBuf::from(value.to_string()));
435 }
436 "TLS_KEY" => {
437 env_config.tls_key = Some(PathBuf::from(value.to_string()));
438 }
439 "TLS_CLIENT_CA" => {
440 env_config.tls_client_ca = Some(PathBuf::from(value.to_string()));
441 }
442 "BINDADDRESS" => {
443 env_config.bindaddress = Some(value.to_string());
444 }
445 "LDAPBINDADDRESS" => {
446 env_config.ldapbindaddress = Some(value.to_string());
447 }
448 "ROLE" => {
449 env_config.role = Some(ServerRole::from_str(&value).map_err(|err| {
450 format!("Failed to parse KANIDM_ROLE as ServerRole: {}", err)
451 })?);
452 }
453 "LOG_LEVEL" => {
454 env_config.log_level = LogLevel::from_str(&value)
455 .map_err(|err| {
456 format!("Failed to parse KANIDM_LOG_LEVEL as LogLevel: {}", err)
457 })
458 .ok();
459 }
460 "ONLINE_BACKUP_PATH" => {
461 if let Some(backup) = &mut env_config.online_backup {
462 backup.path = Some(PathBuf::from(value.to_string()));
463 } else {
464 env_config.online_backup = Some(OnlineBackup {
465 path: Some(PathBuf::from(value.to_string())),
466 ..Default::default()
467 });
468 }
469 }
470 "ONLINE_BACKUP_SCHEDULE" => {
471 if let Some(backup) = &mut env_config.online_backup {
472 backup.schedule = value.to_string();
473 } else {
474 env_config.online_backup = Some(OnlineBackup {
475 schedule: value.to_string(),
476 ..Default::default()
477 });
478 }
479 }
480 "ONLINE_BACKUP_VERSIONS" => {
481 let versions = value.parse().map_err(|_| {
482 "Failed to parse KANIDM_ONLINE_BACKUP_VERSIONS as usize".to_string()
483 })?;
484 if let Some(backup) = &mut env_config.online_backup {
485 backup.versions = versions;
486 } else {
487 env_config.online_backup = Some(OnlineBackup {
488 versions,
489 ..Default::default()
490 })
491 }
492 }
493 "TRUST_X_FORWARD_FOR" => {
494 env_config.trust_x_forward_for = value
495 .parse()
496 .map_err(|_| {
497 "Failed to parse KANIDM_TRUST_X_FORWARD_FOR as bool".to_string()
498 })
499 .ok();
500 }
501 "DB_FS_TYPE" => {
502 env_config.db_fs_type = FsType::try_from(value.as_str())
503 .map_err(|_| {
504 "Failed to parse KANIDM_DB_FS_TYPE env var to valid value!".to_string()
505 })
506 .ok();
507 }
508 "DB_ARC_SIZE" => {
509 env_config.db_arc_size = value
510 .parse()
511 .map_err(|_| "Failed to parse KANIDM_DB_ARC_SIZE as value".to_string())
512 .ok();
513 }
514 "ADMIN_BIND_PATH" => {
515 env_config.adminbindpath = Some(value.to_string());
516 }
517 "REPLICATION_ORIGIN" => {
518 let repl_origin = Url::parse(value.as_str()).map_err(|err| {
519 format!("Failed to parse KANIDM_REPLICATION_ORIGIN as URL: {}", err)
520 })?;
521 if let Some(repl) = &mut env_config.repl_config {
522 repl.origin = repl_origin
523 } else {
524 env_config.repl_config = Some(ReplicationConfiguration {
525 origin: repl_origin,
526 ..Default::default()
527 });
528 }
529 }
530 "REPLICATION_BINDADDRESS" => {
531 let repl_bind_address = value
532 .parse()
533 .map_err(|_| "Failed to parse replication bind address".to_string())?;
534 if let Some(repl) = &mut env_config.repl_config {
535 repl.bindaddress = repl_bind_address;
536 } else {
537 env_config.repl_config = Some(ReplicationConfiguration {
538 bindaddress: repl_bind_address,
539 ..Default::default()
540 });
541 }
542 }
543 "REPLICATION_TASK_POLL_INTERVAL" => {
544 let poll_interval = value
545 .parse()
546 .map_err(|_| {
547 "Failed to parse replication task poll interval as u64".to_string()
548 })
549 .ok();
550 if let Some(repl) = &mut env_config.repl_config {
551 repl.task_poll_interval = poll_interval;
552 } else {
553 env_config.repl_config = Some(ReplicationConfiguration {
554 task_poll_interval: poll_interval,
555 ..Default::default()
556 });
557 }
558 }
559 "OTEL_GRPC_URL" => {
560 env_config.otel_grpc_url = Some(value.to_string());
561 }
562
563 _ => eprintln!("Ignoring env var KANIDM_{key}"),
564 }
565 }
566
567 Ok(env_config)
568 }
569}
570
571#[derive(Debug, Deserialize, Clone, Copy, Default, Eq, PartialEq)]
572pub enum ServerRole {
573 #[default]
574 WriteReplica,
575 WriteReplicaNoUI,
576 ReadOnlyReplica,
577}
578
579impl Display for ServerRole {
580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 match self {
582 ServerRole::WriteReplica => f.write_str("write replica"),
583 ServerRole::WriteReplicaNoUI => f.write_str("write replica (no ui)"),
584 ServerRole::ReadOnlyReplica => f.write_str("read only replica"),
585 }
586 }
587}
588
589impl FromStr for ServerRole {
590 type Err = &'static str;
591
592 fn from_str(s: &str) -> Result<Self, Self::Err> {
593 match s {
594 "write_replica" => Ok(ServerRole::WriteReplica),
595 "write_replica_no_ui" => Ok(ServerRole::WriteReplicaNoUI),
596 "read_only_replica" => Ok(ServerRole::ReadOnlyReplica),
597 _ => Err("Must be one of write_replica, write_replica_no_ui, read_only_replica"),
598 }
599 }
600}
601
602#[derive(Debug, Clone)]
603pub struct IntegrationTestConfig {
604 pub admin_user: String,
605 pub admin_password: String,
606 pub idm_admin_user: String,
607 pub idm_admin_password: String,
608}
609
610#[derive(Debug, Clone)]
611pub struct IntegrationReplConfig {
612 }
618
619#[derive(Debug, Clone)]
621pub struct Configuration {
622 pub address: String,
623 pub ldapbindaddress: Option<String>,
624 pub adminbindpath: String,
625 pub threads: usize,
626 pub db_path: Option<PathBuf>,
628 pub db_fs_type: Option<FsType>,
629 pub db_arc_size: Option<usize>,
630 pub maximum_request: usize,
631
632 pub http_client_address_info: HttpAddressInfo,
633 pub ldap_client_address_info: LdapAddressInfo,
634
635 pub tls_config: Option<TlsConfiguration>,
636 pub integration_test_config: Option<Box<IntegrationTestConfig>>,
637 pub online_backup: Option<OnlineBackup>,
638 pub domain: String,
639 pub origin: Url,
640 pub role: ServerRole,
641 pub output_mode: ConsoleOutputMode,
642 pub log_level: LogLevel,
643 pub repl_config: Option<ReplicationConfiguration>,
645 pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
647 pub otel_grpc_url: Option<String>,
648}
649
650impl Configuration {
651 pub fn build() -> ConfigurationBuilder {
652 ConfigurationBuilder {
653 bindaddress: None,
654 ldapbindaddress: None,
655 adminbindpath: None,
656 threads: std::thread::available_parallelism()
657 .map(|t| t.get())
658 .unwrap_or_else(|_e| {
659 eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
660 4
661 }),
662 db_path: None,
663 db_fs_type: None,
664 db_arc_size: None,
665 maximum_request: 256 * 1024, http_client_address_info: HttpAddressInfo::default(),
667 ldap_client_address_info: LdapAddressInfo::default(),
668 tls_key: None,
669 tls_chain: None,
670 tls_client_ca: None,
671 online_backup: None,
672 domain: None,
673 origin: None,
674 output_mode: None,
675 log_level: None,
676 role: None,
677 repl_config: None,
678 otel_grpc_url: None,
679 }
680 }
681
682 pub fn new_for_test() -> Self {
683 #[allow(clippy::expect_used)]
684 Configuration {
685 address: DEFAULT_SERVER_ADDRESS.to_string(),
686 ldapbindaddress: None,
687 adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
688 threads: 1,
689 db_path: None,
690 db_fs_type: None,
691 db_arc_size: None,
692 maximum_request: 256 * 1024, http_client_address_info: HttpAddressInfo::default(),
694 ldap_client_address_info: LdapAddressInfo::default(),
695 tls_config: None,
696 integration_test_config: None,
697 online_backup: None,
698 domain: "idm.example.com".to_string(),
699 origin: Url::from_str("https://idm.example.com")
700 .expect("Failed to parse built-in string as URL"),
701 output_mode: ConsoleOutputMode::default(),
702 log_level: LogLevel::default(),
703 role: ServerRole::WriteReplica,
704 repl_config: None,
705 integration_repl_config: None,
706 otel_grpc_url: None,
707 }
708 }
709}
710
711impl fmt::Display for Configuration {
712 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
713 write!(f, "address: {}, ", self.address)?;
714 write!(f, "domain: {}, ", self.domain)?;
715 match &self.ldapbindaddress {
716 Some(la) => write!(f, "ldap address: {}, ", la),
717 None => write!(f, "ldap address: disabled, "),
718 }?;
719 write!(f, "origin: {} ", self.origin)?;
720 write!(f, "admin bind path: {}, ", self.adminbindpath)?;
721 write!(f, "thread count: {}, ", self.threads)?;
722 write!(
723 f,
724 "dbpath: {}, ",
725 self.db_path
726 .as_ref()
727 .map(|p| p.to_string_lossy().to_string())
728 .unwrap_or("MEMORY".to_string())
729 )?;
730 match self.db_arc_size {
731 Some(v) => write!(f, "arcsize: {}, ", v),
732 None => write!(f, "arcsize: AUTO, "),
733 }?;
734 write!(f, "max request size: {}b, ", self.maximum_request)?;
735 write!(
736 f,
737 "http client address info: {}, ",
738 self.http_client_address_info
739 )?;
740 write!(
741 f,
742 "ldap client address info: {}, ",
743 self.ldap_client_address_info
744 )?;
745
746 write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
747 match &self.online_backup {
748 Some(bck) => write!(
749 f,
750 "online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
751 bck.enabled,
752 bck.schedule,
753 bck.versions,
754 bck.path
755 .as_ref()
756 .map(|p| p.to_string_lossy().to_string())
757 .unwrap_or("<unset>".to_string())
758 ),
759 None => write!(f, "online_backup: disabled, "),
760 }?;
761 write!(
762 f,
763 "integration mode: {}, ",
764 self.integration_test_config.is_some()
765 )?;
766 write!(f, "console output format: {:?} ", self.output_mode)?;
767 write!(f, "log_level: {}", self.log_level)?;
768 write!(f, "role: {}, ", self.role)?;
769 match &self.repl_config {
770 Some(repl) => {
771 write!(f, "replication: enabled")?;
772 write!(f, "repl_origin: {} ", repl.origin)?;
773 write!(f, "repl_address: {} ", repl.bindaddress)?;
774 write!(
775 f,
776 "integration repl config mode: {}, ",
777 self.integration_repl_config.is_some()
778 )?;
779 }
780 None => {
781 write!(f, "replication: disabled, ")?;
782 }
783 }
784 write!(f, "otel_grpc_url: {:?}", self.otel_grpc_url)?;
785 Ok(())
786 }
787}
788
789#[derive(Debug, Clone)]
791pub struct ConfigurationBuilder {
792 bindaddress: Option<String>,
793 ldapbindaddress: Option<String>,
794 adminbindpath: Option<String>,
795 threads: usize,
796 db_path: Option<PathBuf>,
797 db_fs_type: Option<FsType>,
798 db_arc_size: Option<usize>,
799 maximum_request: usize,
800 http_client_address_info: HttpAddressInfo,
801 ldap_client_address_info: LdapAddressInfo,
802 tls_key: Option<PathBuf>,
803 tls_chain: Option<PathBuf>,
804 tls_client_ca: Option<PathBuf>,
805 online_backup: Option<OnlineBackup>,
806 domain: Option<String>,
807 origin: Option<Url>,
808 role: Option<ServerRole>,
809 output_mode: Option<ConsoleOutputMode>,
810 log_level: Option<LogLevel>,
811 repl_config: Option<ReplicationConfiguration>,
812 otel_grpc_url: Option<String>,
813}
814
815impl ConfigurationBuilder {
816 #![allow(clippy::needless_pass_by_value)]
817 pub fn add_cli_config(mut self, cli_config: CliConfig) -> Self {
818 if cli_config.output_mode.is_some() {
819 self.output_mode = cli_config.output_mode;
820 }
821
822 self
823 }
824
825 pub fn add_env_config(mut self, env_config: EnvironmentConfig) -> Self {
826 if env_config.bindaddress.is_some() {
827 self.bindaddress = env_config.bindaddress;
828 }
829
830 if env_config.ldapbindaddress.is_some() {
831 self.ldapbindaddress = env_config.ldapbindaddress;
832 }
833
834 if env_config.adminbindpath.is_some() {
835 self.adminbindpath = env_config.adminbindpath;
836 }
837
838 if env_config.db_path.is_some() {
839 self.db_path = env_config.db_path;
840 }
841
842 if env_config.db_fs_type.is_some() {
843 self.db_fs_type = env_config.db_fs_type;
844 }
845
846 if env_config.db_arc_size.is_some() {
847 self.db_arc_size = env_config.db_arc_size;
848 }
849
850 if env_config.trust_x_forward_for == Some(true) {
851 self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
852 }
853
854 if env_config.tls_key.is_some() {
855 self.tls_key = env_config.tls_key;
856 }
857
858 if env_config.tls_chain.is_some() {
859 self.tls_chain = env_config.tls_chain;
860 }
861
862 if env_config.tls_client_ca.is_some() {
863 self.tls_client_ca = env_config.tls_client_ca;
864 }
865
866 if env_config.online_backup.is_some() {
867 self.online_backup = env_config.online_backup;
868 }
869
870 if env_config.domain.is_some() {
871 self.domain = env_config.domain;
872 }
873
874 if env_config.origin.is_some() {
875 self.origin = env_config.origin;
876 }
877
878 if env_config.role.is_some() {
879 self.role = env_config.role;
880 }
881
882 if env_config.log_level.is_some() {
883 self.log_level = env_config.log_level;
884 }
885
886 if env_config.repl_config.is_some() {
887 self.repl_config = env_config.repl_config;
888 }
889
890 if env_config.otel_grpc_url.is_some() {
891 self.otel_grpc_url = env_config.otel_grpc_url;
892 }
893
894 self
895 }
896
897 pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
898 let Some(toml_config) = toml_config else {
900 return self;
901 };
902
903 match toml_config {
904 ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
905 self.add_v2_config(values)
906 }
907 ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
908 }
909 }
910
911 fn add_legacy_config(mut self, config: ServerConfig) -> Self {
912 if config.domain.is_some() {
913 self.domain = config.domain;
914 }
915
916 if config.origin.is_some() {
917 self.origin = config.origin;
918 }
919
920 if config.db_path.is_some() {
921 self.db_path = config.db_path;
922 }
923
924 if config.db_fs_type.is_some() {
925 self.db_fs_type = config.db_fs_type;
926 }
927
928 if config.tls_key.is_some() {
929 self.tls_key = config.tls_key;
930 }
931
932 if config.tls_chain.is_some() {
933 self.tls_chain = config.tls_chain;
934 }
935
936 if config.tls_client_ca.is_some() {
937 self.tls_client_ca = config.tls_client_ca;
938 }
939
940 if config.bindaddress.is_some() {
941 self.bindaddress = config.bindaddress;
942 }
943
944 if config.ldapbindaddress.is_some() {
945 self.ldapbindaddress = config.ldapbindaddress;
946 }
947
948 if config.adminbindpath.is_some() {
949 self.adminbindpath = config.adminbindpath;
950 }
951
952 if config.role.is_some() {
953 self.role = config.role;
954 }
955
956 if config.log_level.is_some() {
957 self.log_level = config.log_level;
958 }
959
960 if let Some(threads) = config.thread_count {
961 self.threads = threads;
962 }
963
964 if let Some(maximum) = config.maximum_request_size_bytes {
965 self.maximum_request = maximum;
966 }
967
968 if config.db_arc_size.is_some() {
969 self.db_arc_size = config.db_arc_size;
970 }
971
972 if config.trust_x_forward_for == Some(true) {
973 self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
974 }
975
976 if config.online_backup.is_some() {
977 self.online_backup = config.online_backup;
978 }
979
980 if config.repl_config.is_some() {
981 self.repl_config = config.repl_config;
982 }
983
984 if config.otel_grpc_url.is_some() {
985 self.otel_grpc_url = config.otel_grpc_url;
986 }
987
988 self
989 }
990
991 fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
992 if config.domain.is_some() {
993 self.domain = config.domain;
994 }
995
996 if config.origin.is_some() {
997 self.origin = config.origin;
998 }
999
1000 if config.db_path.is_some() {
1001 self.db_path = config.db_path;
1002 }
1003
1004 if config.db_fs_type.is_some() {
1005 self.db_fs_type = config.db_fs_type;
1006 }
1007
1008 if config.tls_key.is_some() {
1009 self.tls_key = config.tls_key;
1010 }
1011
1012 if config.tls_chain.is_some() {
1013 self.tls_chain = config.tls_chain;
1014 }
1015
1016 if config.tls_client_ca.is_some() {
1017 self.tls_client_ca = config.tls_client_ca;
1018 }
1019
1020 if config.bindaddress.is_some() {
1021 self.bindaddress = config.bindaddress;
1022 }
1023
1024 if config.ldapbindaddress.is_some() {
1025 self.ldapbindaddress = config.ldapbindaddress;
1026 }
1027
1028 if config.adminbindpath.is_some() {
1029 self.adminbindpath = config.adminbindpath;
1030 }
1031
1032 if config.role.is_some() {
1033 self.role = config.role;
1034 }
1035
1036 if config.log_level.is_some() {
1037 self.log_level = config.log_level;
1038 }
1039
1040 if let Some(threads) = config.thread_count {
1041 self.threads = threads;
1042 }
1043
1044 if let Some(maximum) = config.maximum_request_size_bytes {
1045 self.maximum_request = maximum;
1046 }
1047
1048 if config.db_arc_size.is_some() {
1049 self.db_arc_size = config.db_arc_size;
1050 }
1051
1052 if let Some(http_client_address_info) = config.http_client_address_info {
1053 self.http_client_address_info = http_client_address_info
1054 }
1055
1056 if let Some(ldap_client_address_info) = config.ldap_client_address_info {
1057 self.ldap_client_address_info = ldap_client_address_info
1058 }
1059
1060 if config.online_backup.is_some() {
1061 self.online_backup = config.online_backup;
1062 }
1063
1064 if config.repl_config.is_some() {
1065 self.repl_config = config.repl_config;
1066 }
1067
1068 if config.otel_grpc_url.is_some() {
1069 self.otel_grpc_url = config.otel_grpc_url;
1070 }
1071
1072 self
1073 }
1074
1075 pub fn is_server_mode(mut self, is_server: bool) -> Self {
1077 if is_server {
1078 self.threads = 1;
1079 }
1080 self
1081 }
1082
1083 pub fn finish(self) -> Option<Configuration> {
1084 let ConfigurationBuilder {
1085 bindaddress,
1086 ldapbindaddress,
1087 adminbindpath,
1088 threads,
1089 db_path,
1090 db_fs_type,
1091 db_arc_size,
1092 maximum_request,
1093 http_client_address_info,
1094 ldap_client_address_info,
1095 tls_key,
1096 tls_chain,
1097 tls_client_ca,
1098 mut online_backup,
1099 domain,
1100 origin,
1101 role,
1102 output_mode,
1103 log_level,
1104 repl_config,
1105 otel_grpc_url,
1106 } = self;
1107
1108 let tls_config = match (tls_key, tls_chain, tls_client_ca) {
1109 (Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
1110 chain,
1111 key,
1112 client_ca,
1113 }),
1114 _ => {
1115 eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
1116 return None;
1117 }
1118 };
1119
1120 let domain = domain.or_else(|| {
1121 eprintln!("ERROR: domain was not set.");
1122 None
1123 })?;
1124
1125 let origin = origin.or_else(|| {
1126 eprintln!("ERROR: origin was not set.");
1127 None
1128 })?;
1129
1130 if let Some(online_backup_ref) = online_backup.as_mut() {
1131 if online_backup_ref.path.is_none() {
1132 if let Some(db_path) = db_path.as_ref() {
1133 if let Some(db_parent_path) = db_path.parent() {
1134 online_backup_ref.path = Some(db_parent_path.to_path_buf());
1135 } else {
1136 eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
1137 return None;
1138 }
1139 } else {
1140 eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
1141 return None;
1142 }
1143 }
1144 };
1145
1146 let adminbindpath =
1148 adminbindpath.unwrap_or(env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string());
1149 let address = bindaddress.unwrap_or(DEFAULT_SERVER_ADDRESS.to_string());
1150 let output_mode = output_mode.unwrap_or_default();
1151 let role = role.unwrap_or(ServerRole::WriteReplica);
1152 let log_level = log_level.unwrap_or_default();
1153
1154 Some(Configuration {
1155 address,
1156 ldapbindaddress,
1157 adminbindpath,
1158 threads,
1159 db_path,
1160 db_fs_type,
1161 db_arc_size,
1162 maximum_request,
1163 http_client_address_info,
1164 ldap_client_address_info,
1165 tls_config,
1166 online_backup,
1167 domain,
1168 origin,
1169 role,
1170 output_mode,
1171 log_level,
1172 repl_config,
1173 otel_grpc_url,
1174 integration_repl_config: None,
1175 integration_test_config: None,
1176 })
1177 }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
1183 use std::net::{Ipv4Addr, Ipv6Addr};
1184
1185 #[test]
1186 fn assert_cidr_parsing_behaviour() {
1187 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.1\"").unwrap();
1189 let expect_ip_cidr = IpCidr::from(Ipv4Addr::new(127, 0, 0, 1));
1190 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1191
1192 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.0/8\"").unwrap();
1193 let expect_ip_cidr = IpCidr::from(Ipv4Cidr::new(Ipv4Addr::new(127, 0, 0, 0), 8).unwrap());
1194 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1195
1196 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::1\"").unwrap();
1198 let expect_ip_cidr = IpCidr::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0001));
1199 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1200
1201 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::/64\"").unwrap();
1202 let expect_ip_cidr = IpCidr::from(
1203 Ipv6Cidr::new(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 64).unwrap(),
1204 );
1205 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1206 }
1207}