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