1use cidr::IpCidr;
8use kanidm_proto::backup::BackupCompression;
9use kanidm_proto::config::ServerRole;
10use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
11use kanidm_proto::internal::FsType;
12use kanidm_proto::messages::ConsoleOutputMode;
13use serde::Deserialize;
14use serde_with::{formats::PreferOne, serde_as, OneOrMany};
15use sketching::LogLevel;
16use std::fmt::{self, Display};
17use std::fs::File;
18use std::io::Read;
19use std::net::IpAddr;
20use std::path::{Path, PathBuf};
21use std::str::FromStr;
22use std::sync::Arc;
23use url::Url;
24
25use crate::repl::config::ReplicationConfiguration;
26
27#[derive(Debug, Deserialize)]
28struct VersionDetection {
29 #[serde(default)]
30 version: Version,
31}
32
33#[derive(Debug, Deserialize, Default)]
34pub enum Version {
36 #[serde(rename = "2")]
37 V2,
38
39 #[default]
40 Legacy,
41}
42
43#[allow(clippy::large_enum_variant)]
45pub enum ServerConfigUntagged {
46 Version(ServerConfigVersion),
47 Legacy(ServerConfig),
48}
49
50pub enum ServerConfigVersion {
51 V2 { values: ServerConfigV2 },
52}
53
54#[derive(Deserialize, Debug, Clone)]
55pub struct OnlineBackup {
56 pub path: Option<PathBuf>,
58 pub schedule: String,
76 #[serde(default = "default_online_backup_versions")]
77 pub versions: usize,
79 #[serde(default = "default_online_backup_enabled")]
81 pub enabled: bool,
82
83 #[serde(default)]
84 pub compression: BackupCompression,
85}
86
87impl Default for OnlineBackup {
88 fn default() -> Self {
89 OnlineBackup {
90 path: None, schedule: default_online_backup_schedule(),
92 versions: default_online_backup_versions(),
93 enabled: default_online_backup_enabled(),
94 compression: BackupCompression::default(),
95 }
96 }
97}
98
99fn default_online_backup_enabled() -> bool {
100 true
101}
102
103fn default_online_backup_schedule() -> String {
104 "@daily".to_string()
105}
106
107fn default_online_backup_versions() -> usize {
108 7
109}
110
111#[derive(Deserialize, Debug, Clone)]
112pub struct TlsConfiguration {
113 pub chain: PathBuf,
114 pub key: PathBuf,
115 pub client_ca: Option<PathBuf>,
116}
117
118#[derive(Debug, Default)]
119pub enum TcpAddressInfo {
120 #[default]
121 None,
122 ProxyV2(Vec<IpCidr>),
123 ProxyV1(Vec<IpCidr>),
124}
125
126#[derive(Deserialize, Debug, Clone, Default)]
127pub enum LdapAddressInfo {
128 #[default]
129 None,
130 #[serde(rename = "proxy-v2")]
131 ProxyV2(Vec<IpCidr>),
132 #[serde(rename = "proxy-v1")]
133 ProxyV1(Vec<IpCidr>),
134}
135
136impl LdapAddressInfo {
137 pub fn trusted_tcp_info(&self) -> Arc<TcpAddressInfo> {
138 Arc::new(match self {
139 LdapAddressInfo::None => TcpAddressInfo::None,
140 LdapAddressInfo::ProxyV2(trusted) => TcpAddressInfo::ProxyV2(trusted.clone()),
141 LdapAddressInfo::ProxyV1(trusted) => TcpAddressInfo::ProxyV1(trusted.clone()),
142 })
143 }
144}
145
146impl Display for LdapAddressInfo {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match self {
149 Self::None => f.write_str("none"),
150 Self::ProxyV2(trusted) => {
151 f.write_str("proxy-v2 [ ")?;
152 for ip in trusted {
153 write!(f, "{ip} ")?;
154 }
155 f.write_str("]")
156 }
157 Self::ProxyV1(trusted) => {
158 f.write_str("proxy-v1 [ ")?;
159 for ip in trusted {
160 write!(f, "{ip} ")?;
161 }
162 f.write_str("]")
163 }
164 }
165 }
166}
167
168pub(crate) enum AddressSet {
169 NonContiguousIpSet(Vec<IpCidr>),
170 All,
171}
172
173impl AddressSet {
174 pub(crate) fn contains(&self, ip_addr: &IpAddr) -> bool {
175 match self {
176 Self::All => true,
177 Self::NonContiguousIpSet(range) => {
178 range.iter().any(|ip_cidr| ip_cidr.contains(ip_addr))
179 }
180 }
181 }
182}
183
184#[derive(Deserialize, Debug, Clone, Default)]
185pub enum HttpAddressInfo {
186 #[default]
187 None,
188 #[serde(rename = "x-forward-for")]
189 XForwardFor(Vec<IpCidr>),
190 #[serde(rename = "x-forward-for-all-source-trusted")]
193 XForwardForAllSourcesTrusted,
194 #[serde(rename = "proxy-v2")]
195 ProxyV2(Vec<IpCidr>),
196 #[serde(rename = "proxy-v1")]
197 ProxyV1(Vec<IpCidr>),
198}
199
200impl HttpAddressInfo {
201 pub(crate) fn trusted_x_forward_for(&self) -> Option<AddressSet> {
202 match self {
203 Self::XForwardForAllSourcesTrusted => Some(AddressSet::All),
204 Self::XForwardFor(trusted) => Some(AddressSet::NonContiguousIpSet(trusted.clone())),
205 _ => None,
206 }
207 }
208
209 pub fn trusted_tcp_info(&self) -> Arc<TcpAddressInfo> {
210 Arc::new(match self {
211 Self::ProxyV2(trusted) => TcpAddressInfo::ProxyV2(trusted.clone()),
212 Self::ProxyV1(trusted) => TcpAddressInfo::ProxyV1(trusted.clone()),
213 Self::None | Self::XForwardFor(_) | Self::XForwardForAllSourcesTrusted => {
214 TcpAddressInfo::None
215 }
216 })
217 }
218}
219
220impl Display for HttpAddressInfo {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 match self {
223 Self::None => f.write_str("none"),
224
225 Self::XForwardFor(trusted) => {
226 f.write_str("x-forward-for [ ")?;
227 for ip in trusted {
228 write!(f, "{ip} ")?;
229 }
230 f.write_str("]")
231 }
232 Self::XForwardForAllSourcesTrusted => {
233 f.write_str("x-forward-for [ ALL SOURCES TRUSTED ]")
234 }
235 Self::ProxyV2(trusted) => {
236 f.write_str("proxy-v2 [ ")?;
237 for ip in trusted {
238 write!(f, "{ip} ")?;
239 }
240 f.write_str("]")
241 }
242 Self::ProxyV1(trusted) => {
243 f.write_str("proxy-v1 [ ")?;
244 for ip in trusted {
245 write!(f, "{ip} ")?;
246 }
247 f.write_str("]")
248 }
249 }
250 }
251}
252
253#[derive(Debug, Deserialize, Default)]
262#[serde(deny_unknown_fields)]
263pub struct ServerConfig {
264 domain: Option<String>,
266 origin: Option<Url>,
268 db_path: Option<PathBuf>,
270 db_fs_type: Option<kanidm_proto::internal::FsType>,
272
273 tls_chain: Option<PathBuf>,
275 tls_key: Option<PathBuf>,
277
278 tls_client_ca: Option<PathBuf>,
280
281 bindaddress: Option<String>,
285 ldapbindaddress: Option<String>,
291 role: Option<ServerRole>,
293 log_level: Option<LogLevel>,
295
296 online_backup: Option<OnlineBackup>,
298
299 trust_x_forward_for: Option<bool>,
301
302 adminbindpath: Option<String>,
304
305 thread_count: Option<usize>,
308
309 maximum_request_size_bytes: Option<usize>,
311
312 #[allow(dead_code)]
314 db_arc_size: Option<usize>,
315 #[serde(default)]
316 #[serde(rename = "replication")]
317 repl_config: Option<ReplicationConfiguration>,
319 otel_grpc_url: Option<String>,
321}
322
323impl ServerConfigUntagged {
324 pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
326 eprintln!("📜 Using config file: {:?}", config_path.as_ref());
328 let mut f: File = File::open(config_path.as_ref()).inspect_err(|e| {
329 eprintln!("Unable to open config file [{e:?}] 🥺");
330 let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
331 eprintln!("{diag}");
332 })?;
333
334 let mut contents = String::new();
335
336 f.read_to_string(&mut contents).inspect_err(|e| {
337 eprintln!("unable to read contents {e:?}");
338 let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
339 eprintln!("{diag}");
340 })?;
341
342 let config_version = toml::from_str::<VersionDetection>(contents.as_str())
344 .map(|vd| vd.version)
345 .map_err(|err| {
346 eprintln!(
347 "Unable to parse config version from '{:?}': {:?}",
348 config_path.as_ref(),
349 err
350 );
351 std::io::Error::new(std::io::ErrorKind::InvalidData, err)
352 })?;
353
354 match config_version {
355 Version::V2 => toml::from_str::<ServerConfigV2>(contents.as_str())
356 .map(|values| ServerConfigUntagged::Version(ServerConfigVersion::V2 { values })),
357 Version::Legacy => {
358 toml::from_str::<ServerConfig>(contents.as_str()).map(ServerConfigUntagged::Legacy)
359 }
360 }
361 .map_err(|err| {
362 eprintln!(
363 "Unable to parse config from '{:?}': {:?}",
364 config_path.as_ref(),
365 err
366 );
367 std::io::Error::new(std::io::ErrorKind::InvalidData, err)
368 })
369 }
370}
371
372#[serde_as]
373#[derive(Debug, Deserialize, Default)]
374#[serde(deny_unknown_fields)]
375pub struct ServerConfigV2 {
376 #[allow(dead_code)]
377 version: String,
378 domain: Option<String>,
379 origin: Option<Url>,
380 db_path: Option<PathBuf>,
381 db_fs_type: Option<kanidm_proto::internal::FsType>,
382 tls_chain: Option<PathBuf>,
383 tls_key: Option<PathBuf>,
384 tls_client_ca: Option<PathBuf>,
385
386 #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
387 bindaddress: Option<Vec<String>>,
388 #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
389 ldapbindaddress: Option<Vec<String>>,
390
391 role: Option<ServerRole>,
392 log_level: Option<LogLevel>,
393 online_backup: Option<OnlineBackup>,
394
395 http_client_address_info: Option<HttpAddressInfo>,
396 ldap_client_address_info: Option<LdapAddressInfo>,
397
398 adminbindpath: Option<String>,
399 thread_count: Option<usize>,
400 maximum_request_size_bytes: Option<usize>,
401 #[allow(dead_code)]
402 db_arc_size: Option<usize>,
403 #[serde(default)]
404 #[serde(rename = "replication")]
405 repl_config: Option<ReplicationConfiguration>,
406 otel_grpc_url: Option<String>,
407}
408
409#[derive(Debug, Clone)]
410pub struct IntegrationTestConfig {
411 pub admin_user: String,
412 pub admin_password: String,
413 pub idm_admin_user: String,
414 pub idm_admin_password: String,
415}
416
417#[derive(Debug, Clone)]
418pub struct IntegrationReplConfig {
419 }
425
426#[derive(Debug, Clone)]
428pub struct Configuration {
429 pub address: Vec<String>,
430 pub ldapbindaddress: Option<Vec<String>>,
431 pub adminbindpath: String,
432 pub threads: usize,
433 pub db_path: Option<PathBuf>,
435 pub db_fs_type: Option<FsType>,
436 pub db_arc_size: Option<usize>,
437 pub maximum_request: usize,
438
439 pub http_client_address_info: HttpAddressInfo,
440 pub ldap_client_address_info: LdapAddressInfo,
441
442 pub tls_config: Option<TlsConfiguration>,
443 pub integration_test_config: Option<Box<IntegrationTestConfig>>,
444 pub online_backup: Option<OnlineBackup>,
445 pub domain: String,
446 pub origin: Url,
447 pub role: ServerRole,
448 pub output_mode: ConsoleOutputMode,
449 pub log_level: LogLevel,
450 pub repl_config: Option<ReplicationConfiguration>,
452 pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
454 pub otel_grpc_url: Option<String>,
455}
456
457impl Configuration {
458 pub fn build() -> ConfigurationBuilder {
459 ConfigurationBuilder {
460 bindaddress: None,
461 ldapbindaddress: None,
462 adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
464 threads: std::thread::available_parallelism()
465 .map(|t| t.get())
466 .unwrap_or_else(|_e| {
467 eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
468 4
469 }),
470 db_path: None,
471 db_fs_type: None,
472 db_arc_size: None,
473 maximum_request: 256 * 1024, http_client_address_info: HttpAddressInfo::default(),
475 ldap_client_address_info: LdapAddressInfo::default(),
476 tls_key: None,
477 tls_chain: None,
478 tls_client_ca: None,
479 online_backup: None,
480 domain: None,
481 origin: None,
482 output_mode: ConsoleOutputMode::default(),
483 log_level: None,
484 role: None,
485 repl_config: None,
486 otel_grpc_url: None,
487 }
488 }
489
490 pub fn new_for_test() -> Self {
491 #[allow(clippy::expect_used)]
492 Configuration {
493 address: vec![DEFAULT_SERVER_ADDRESS.to_string()],
494 ldapbindaddress: None,
495 adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
496 threads: 1,
497 db_path: None,
498 db_fs_type: None,
499 db_arc_size: None,
500 maximum_request: 256 * 1024, http_client_address_info: HttpAddressInfo::default(),
502 ldap_client_address_info: LdapAddressInfo::default(),
503 tls_config: None,
504 integration_test_config: None,
505 online_backup: None,
506 domain: "idm.example.com".to_string(),
507 origin: Url::from_str("https://idm.example.com")
508 .expect("Failed to parse built-in string as URL"),
509 output_mode: ConsoleOutputMode::default(),
510 log_level: LogLevel::default(),
511 role: ServerRole::WriteReplica,
512 repl_config: None,
513 integration_repl_config: None,
514 otel_grpc_url: None,
515 }
516 }
517}
518
519impl fmt::Display for Configuration {
520 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521 for a in &self.address {
522 write!(f, "address: {a}, ")?;
523 }
524 write!(f, "domain: {}, ", self.domain)?;
525 match &self.ldapbindaddress {
526 Some(las) => {
527 for la in las {
528 write!(f, "ldap address: {la}, ")?;
529 }
530 }
531 None => write!(f, "ldap address: disabled, ")?,
532 };
533 write!(f, "origin: {} ", self.origin)?;
534 write!(f, "admin bind path: {}, ", self.adminbindpath)?;
535 write!(f, "thread count: {}, ", self.threads)?;
536 write!(
537 f,
538 "dbpath: {}, ",
539 self.db_path
540 .as_ref()
541 .map(|p| p.to_string_lossy().to_string())
542 .unwrap_or("MEMORY".to_string())
543 )?;
544 match self.db_arc_size {
545 Some(v) => write!(f, "arcsize: {v}, "),
546 None => write!(f, "arcsize: AUTO, "),
547 }?;
548 write!(f, "max request size: {}b, ", self.maximum_request)?;
549 write!(
550 f,
551 "http client address info: {}, ",
552 self.http_client_address_info
553 )?;
554 write!(
555 f,
556 "ldap client address info: {}, ",
557 self.ldap_client_address_info
558 )?;
559
560 write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
561 match &self.online_backup {
562 Some(bck) => write!(
563 f,
564 "online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
565 bck.enabled,
566 bck.schedule,
567 bck.versions,
568 bck.path
569 .as_ref()
570 .map(|p| p.to_string_lossy().to_string())
571 .unwrap_or("<unset>".to_string())
572 ),
573 None => write!(f, "online_backup: disabled, "),
574 }?;
575 write!(
576 f,
577 "integration mode: {}, ",
578 self.integration_test_config.is_some()
579 )?;
580 write!(f, "console output format: {:?} ", self.output_mode)?;
581 write!(f, "log_level: {}", self.log_level)?;
582 write!(f, "role: {}, ", self.role)?;
583 match &self.repl_config {
584 Some(repl) => {
585 write!(f, "replication: enabled")?;
586 write!(f, "repl_origin: {} ", repl.origin)?;
587 write!(f, "repl_address: {} ", repl.bindaddress)?;
588 write!(
589 f,
590 "integration repl config mode: {}, ",
591 self.integration_repl_config.is_some()
592 )?;
593 }
594 None => {
595 write!(f, "replication: disabled, ")?;
596 }
597 }
598 write!(f, "otel_grpc_url: {:?}", self.otel_grpc_url)?;
599 Ok(())
600 }
601}
602
603#[derive(Debug, Clone)]
605pub struct ConfigurationBuilder {
606 bindaddress: Option<Vec<String>>,
607 ldapbindaddress: Option<Vec<String>>,
608 adminbindpath: String,
609 threads: usize,
610 db_path: Option<PathBuf>,
611 db_fs_type: Option<FsType>,
612 db_arc_size: Option<usize>,
613 maximum_request: usize,
614 http_client_address_info: HttpAddressInfo,
615 ldap_client_address_info: LdapAddressInfo,
616 tls_key: Option<PathBuf>,
617 tls_chain: Option<PathBuf>,
618 tls_client_ca: Option<PathBuf>,
619 online_backup: Option<OnlineBackup>,
620 domain: Option<String>,
621 origin: Option<Url>,
622 role: Option<ServerRole>,
623 output_mode: ConsoleOutputMode,
624 log_level: Option<LogLevel>,
625 repl_config: Option<ReplicationConfiguration>,
626 otel_grpc_url: Option<String>,
627}
628
629impl ConfigurationBuilder {
630 #![allow(clippy::needless_pass_by_value)]
631 pub fn add_cli_config(mut self, cli_config: &kanidm_proto::cli::KanidmdCli) -> Self {
632 self.output_mode = cli_config.output_mode;
634
635 if let Some(log_level) = &cli_config.log_level {
636 self.log_level = Some(*log_level);
637 }
638
639 if let Some(otel_grpc_url) = &cli_config.otel_grpc_url {
640 self.otel_grpc_url = Some(otel_grpc_url.clone());
641 }
642
643 if let Some(domain) = &cli_config.domain {
645 self.domain = Some(domain.clone());
646 }
647
648 if let Some(origin) = &cli_config.origin {
649 self.origin = Some(origin.clone());
650 }
651
652 if let Some(role) = &cli_config.role {
653 self.role = Some(*role);
654 }
655
656 if cli_config.bindaddress.is_some() {
659 self.bindaddress = cli_config
660 .bindaddress
661 .clone()
662 .map(|s| s.split(',').map(|s| s.to_string()).collect())
663 }
664
665 if cli_config.ldapbindaddress.is_some() {
666 self.ldapbindaddress = cli_config
667 .ldapbindaddress
668 .clone()
669 .map(|s| s.split(',').map(|s| s.to_string()).collect())
670 }
671
672 if let Some(repl_origin) = cli_config.replication_origin.clone() {
675 if let Some(repl) = &mut self.repl_config {
676 repl.origin = repl_origin
677 } else {
678 self.repl_config = Some(ReplicationConfiguration {
679 origin: repl_origin,
680 ..Default::default()
681 });
682 }
683 }
684
685 if let Some(replication_bindaddress) = &cli_config.replication_bindaddress {
686 if let Some(repl_config) = &mut self.repl_config {
687 repl_config.bindaddress = *replication_bindaddress;
688 } else {
689 self.repl_config = Some(ReplicationConfiguration {
690 bindaddress: *replication_bindaddress,
691 ..Default::default()
692 });
693 }
694 }
695
696 if let Some(task_poll_interval) = cli_config.replication_task_poll_interval {
697 if let Some(repl) = &mut self.repl_config {
698 repl.task_poll_interval = Some(task_poll_interval);
699 } else {
700 self.repl_config = Some(ReplicationConfiguration {
701 task_poll_interval: Some(task_poll_interval),
702 ..Default::default()
703 });
704 }
705 }
706
707 if let Some(tls_key) = &cli_config.tls_key {
710 self.tls_key = Some(tls_key.clone());
711 }
712
713 if let Some(tls_chain) = &cli_config.tls_chain {
714 self.tls_chain = Some(tls_chain.clone());
715 }
716
717 if let Some(tls_client_ca) = &cli_config.tls_client_ca {
718 self.tls_client_ca = Some(tls_client_ca.clone());
719 }
720
721 if let Some(adminbindpath) = &cli_config.admin_bind_path {
723 self.adminbindpath = adminbindpath.clone();
724 }
725
726 if let Some(db_path) = &cli_config.db_path {
727 self.db_path = Some(db_path.clone());
728 }
729
730 if let Some(db_fs_type) = &cli_config.db_fs_type {
731 self.db_fs_type = Some(*db_fs_type);
732 }
733
734 if cli_config.db_arc_size.is_some() {
735 self.db_arc_size = cli_config.db_arc_size;
736 }
737
738 if let Some(online_backup_path) = &cli_config.online_backup_path {
740 if let Some(backup) = &mut self.online_backup {
741 backup.path = Some(online_backup_path.clone());
742 } else {
743 self.online_backup = Some(OnlineBackup {
744 path: Some(online_backup_path.clone()),
745 ..Default::default()
746 });
747 }
748 }
749
750 if let Some(online_backup_schedule) = &cli_config.online_backup_schedule {
751 if let Some(backup) = &mut self.online_backup {
752 backup.schedule = online_backup_schedule.clone();
753 } else {
754 self.online_backup = Some(OnlineBackup {
755 schedule: online_backup_schedule.clone(),
756 ..Default::default()
757 });
758 }
759 }
760
761 if let Some(online_backup_versions) = &cli_config.online_backup_versions {
762 if let Some(backup) = &mut self.online_backup {
763 backup.versions = *online_backup_versions;
764 } else {
765 self.online_backup = Some(OnlineBackup {
766 versions: *online_backup_versions,
767 ..Default::default()
768 });
769 }
770 }
771
772 if let Some(true) = cli_config.trust_all_x_forwarded_for {
774 self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
775 }
776
777 self
778 }
779
780 pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
781 let Some(toml_config) = toml_config else {
783 return self;
784 };
785
786 match toml_config {
787 ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
788 self.add_v2_config(values)
789 }
790 ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
791 }
792 }
793
794 fn add_legacy_config(mut self, config: ServerConfig) -> Self {
795 if config.domain.is_some() {
796 self.domain = config.domain;
797 }
798
799 if config.origin.is_some() {
800 self.origin = config.origin;
801 }
802
803 if config.db_path.is_some() {
804 self.db_path = config.db_path;
805 }
806
807 if config.db_fs_type.is_some() {
808 self.db_fs_type = config.db_fs_type;
809 }
810
811 if config.tls_key.is_some() {
812 self.tls_key = config.tls_key;
813 }
814
815 if config.tls_chain.is_some() {
816 self.tls_chain = config.tls_chain;
817 }
818
819 if config.tls_client_ca.is_some() {
820 self.tls_client_ca = config.tls_client_ca;
821 }
822
823 if config.bindaddress.is_some() {
824 self.bindaddress = config.bindaddress.map(|a| vec![a]);
825 }
826
827 if config.ldapbindaddress.is_some() {
828 self.ldapbindaddress = config.ldapbindaddress.map(|a| vec![a]);
829 }
830
831 if let Some(adminbindpath) = config.adminbindpath {
832 self.adminbindpath = adminbindpath;
833 }
834
835 if config.role.is_some() {
836 self.role = config.role;
837 }
838
839 if config.log_level.is_some() {
840 self.log_level = config.log_level;
841 }
842
843 if let Some(threads) = config.thread_count {
844 self.threads = threads;
845 }
846
847 if let Some(maximum) = config.maximum_request_size_bytes {
848 self.maximum_request = maximum;
849 }
850
851 if config.db_arc_size.is_some() {
852 self.db_arc_size = config.db_arc_size;
853 }
854
855 if config.trust_x_forward_for == Some(true) {
856 self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
857 }
858
859 if config.online_backup.is_some() {
860 self.online_backup = config.online_backup;
861 }
862
863 if config.repl_config.is_some() {
864 self.repl_config = config.repl_config;
865 }
866
867 if config.otel_grpc_url.is_some() {
868 self.otel_grpc_url = config.otel_grpc_url;
869 }
870
871 self
872 }
873
874 fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
875 if config.domain.is_some() {
876 self.domain = config.domain;
877 }
878
879 if config.origin.is_some() {
880 self.origin = config.origin;
881 }
882
883 if config.db_path.is_some() {
884 self.db_path = config.db_path;
885 }
886
887 if config.db_fs_type.is_some() {
888 self.db_fs_type = config.db_fs_type;
889 }
890
891 if config.tls_key.is_some() {
892 self.tls_key = config.tls_key;
893 }
894
895 if config.tls_chain.is_some() {
896 self.tls_chain = config.tls_chain;
897 }
898
899 if config.tls_client_ca.is_some() {
900 self.tls_client_ca = config.tls_client_ca;
901 }
902
903 if config.bindaddress.is_some() {
904 self.bindaddress = config.bindaddress;
905 }
906
907 if config.ldapbindaddress.is_some() {
908 self.ldapbindaddress = config.ldapbindaddress;
909 }
910
911 if let Some(adminbindpath) = config.adminbindpath {
912 self.adminbindpath = adminbindpath;
913 }
914
915 if config.role.is_some() {
916 self.role = config.role;
917 }
918
919 if config.log_level.is_some() {
920 self.log_level = config.log_level;
921 }
922
923 if let Some(threads) = config.thread_count {
924 self.threads = threads;
925 }
926
927 if let Some(maximum) = config.maximum_request_size_bytes {
928 self.maximum_request = maximum;
929 }
930
931 if config.db_arc_size.is_some() {
932 self.db_arc_size = config.db_arc_size;
933 }
934
935 if let Some(http_client_address_info) = config.http_client_address_info {
936 self.http_client_address_info = http_client_address_info
937 }
938
939 if let Some(ldap_client_address_info) = config.ldap_client_address_info {
940 self.ldap_client_address_info = ldap_client_address_info
941 }
942
943 if config.online_backup.is_some() {
944 self.online_backup = config.online_backup;
945 }
946
947 if config.repl_config.is_some() {
948 self.repl_config = config.repl_config;
949 }
950
951 if config.otel_grpc_url.is_some() {
952 self.otel_grpc_url = config.otel_grpc_url;
953 }
954
955 self
956 }
957
958 pub fn is_server_mode(mut self, is_server: bool) -> Self {
960 if is_server {
961 self.threads = 1;
962 }
963 self
964 }
965
966 pub fn finish(self) -> Option<Configuration> {
967 let ConfigurationBuilder {
968 bindaddress,
969 ldapbindaddress,
970 adminbindpath,
971 threads,
972 db_path,
973 db_fs_type,
974 db_arc_size,
975 maximum_request,
976 http_client_address_info,
977 ldap_client_address_info,
978 tls_key,
979 tls_chain,
980 tls_client_ca,
981 mut online_backup,
982 domain,
983 origin,
984 role,
985 output_mode,
986 log_level,
987 repl_config,
988 otel_grpc_url,
989 } = self;
990
991 let tls_config = match (tls_key, tls_chain, tls_client_ca) {
992 (Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
993 chain,
994 key,
995 client_ca,
996 }),
997 _ => {
998 eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
999 return None;
1000 }
1001 };
1002
1003 let domain = domain.or_else(|| {
1004 eprintln!("ERROR: domain was not set.");
1005 None
1006 })?;
1007
1008 let origin = origin.or_else(|| {
1009 eprintln!("ERROR: origin was not set.");
1010 None
1011 })?;
1012
1013 if let Some(online_backup_ref) = online_backup.as_mut() {
1014 if online_backup_ref.path.is_none() {
1015 if let Some(db_path) = db_path.as_ref() {
1016 if let Some(db_parent_path) = db_path.parent() {
1017 online_backup_ref.path = Some(db_parent_path.to_path_buf());
1018 } else {
1019 eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
1020 return None;
1021 }
1022 } else {
1023 eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
1024 return None;
1025 }
1026 }
1027 };
1028
1029 let address = bindaddress.unwrap_or(vec![DEFAULT_SERVER_ADDRESS.to_string()]);
1031 let role = role.unwrap_or(ServerRole::WriteReplica);
1032 let log_level = log_level.unwrap_or_default();
1033
1034 Some(Configuration {
1035 address,
1036 ldapbindaddress,
1037 adminbindpath,
1038 threads,
1039 db_path,
1040 db_fs_type,
1041 db_arc_size,
1042 maximum_request,
1043 http_client_address_info,
1044 ldap_client_address_info,
1045 tls_config,
1046 online_backup,
1047 domain,
1048 origin,
1049 role,
1050 output_mode,
1051 log_level,
1052 repl_config,
1053 otel_grpc_url,
1054 integration_repl_config: None,
1055 integration_test_config: None,
1056 })
1057 }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
1063 use std::net::{Ipv4Addr, Ipv6Addr};
1064
1065 #[test]
1066 fn assert_cidr_parsing_behaviour() {
1067 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.1\"").unwrap();
1069 let expect_ip_cidr = IpCidr::from(Ipv4Addr::new(127, 0, 0, 1));
1070 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1071
1072 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.0/8\"").unwrap();
1073 let expect_ip_cidr = IpCidr::from(Ipv4Cidr::new(Ipv4Addr::new(127, 0, 0, 0), 8).unwrap());
1074 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1075
1076 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::1\"").unwrap();
1078 let expect_ip_cidr = IpCidr::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0001));
1079 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1080
1081 let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::/64\"").unwrap();
1082 let expect_ip_cidr = IpCidr::from(
1083 Ipv6Cidr::new(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 64).unwrap(),
1084 );
1085 assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1086 }
1087}