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