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