kanidmd_core/
config.rs

1//! The server configuration as processed from the startup wrapper. This controls a number of
2//! variables that determine how our backends, query server, and frontends are configured.
3//!
4//! These components should be "per server". Any "per domain" config should be in the system
5//! or domain entries that are able to be replicated.
6
7use 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 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)]
33// #[serde(tag = "version")]
34pub enum Version {
35    #[serde(rename = "2")]
36    V2,
37
38    #[default]
39    Legacy,
40}
41
42// Allowed as the large enum is only short lived at startup to the true config
43#[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    /// The destination folder for your backups, defaults to the db_path dir if not set
56    pub path: Option<PathBuf>,
57    /// The schedule to run online backups (see <https://crontab.guru/>), defaults to @daily
58    ///
59    /// Examples:
60    ///
61    /// - every day at 22:00 UTC (default): `"00 22 * * *"`
62    /// - every 6th hours (four times a day) at 3 minutes past the hour, :
63    ///   `"03 */6 * * *"`
64    ///
65    /// We also support non standard cron syntax, with the following format:
66    ///
67    /// `<sec>  <min>   <hour>   <day of month>   <month>   <day of week>   <year>`
68    ///
69    /// eg:
70    /// - `1 2 3 5 12 * 2023` would only back up once on the 5th of December 2023 at 03:02:01am.
71    /// - `3 2 1 * * Mon *` backs up every Monday at 03:02:01am.
72    ///
73    /// (it's very similar to the standard cron syntax, it just allows to specify the seconds at the beginning and the year at the end)
74    pub schedule: String,
75    #[serde(default = "default_online_backup_versions")]
76    /// How many past backup versions to keep, defaults to 7
77    pub versions: usize,
78    /// Enabled by default
79    #[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, // This makes it revert to the kanidm_db path
90            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    // IMPORTANT: This is undocumented, and only exists for backwards compat
190    // with config v1 which has a boolean toggle for this option.
191    #[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/// This is the Server Configuration as read from `server.toml` or environment variables.
253///
254/// Fields noted as "REQUIRED" are required for the server to start, even if they show as optional due to how file parsing works.
255///
256/// If you want to set these as environment variables, prefix them with `KANIDM_` and they will be picked up. This does not include replication peer config.
257///
258/// NOTE: not all flags or values from the internal [Configuration] object are exposed via this structure
259/// to prevent certain settings being set (e.g. integration test modes)
260#[derive(Debug, Deserialize, Default)]
261#[serde(deny_unknown_fields)]
262pub struct ServerConfig {
263    /// *REQUIRED* - Kanidm Domain, eg `kanidm.example.com`.
264    domain: Option<String>,
265    /// *REQUIRED* - The user-facing HTTPS URL for this server, eg <https://idm.example.com>
266    origin: Option<Url>,
267    /// File path of the database file
268    db_path: Option<PathBuf>,
269    /// The filesystem type, either "zfs" or "generic". Defaults to "generic" if unset. I you change this, run a database vacuum.
270    db_fs_type: Option<kanidm_proto::internal::FsType>,
271
272    ///  *REQUIRED* - The file path to the TLS Certificate Chain
273    tls_chain: Option<PathBuf>,
274    ///  *REQUIRED* - The file path to the TLS Private Key
275    tls_key: Option<PathBuf>,
276
277    /// The directory path of the client ca and crl dir.
278    tls_client_ca: Option<PathBuf>,
279
280    /// The listener address for the HTTPS server.
281    ///
282    /// eg. `[::]:8443` or `127.0.0.1:8443`. Defaults to [kanidm_proto::constants::DEFAULT_SERVER_ADDRESS]
283    bindaddress: Option<String>,
284    /// The listener address for the LDAP server.
285    ///
286    /// eg. `[::]:3636` or `127.0.0.1:3636`.
287    ///
288    /// If unset, the LDAP server will be disabled.
289    ldapbindaddress: Option<String>,
290    /// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica]
291    role: Option<ServerRole>,
292    /// The log level, one of info, debug, trace. Defaults to "info" if not set.
293    log_level: Option<LogLevel>,
294
295    /// Backup Configuration, see [OnlineBackup] for details on sub-keys.
296    online_backup: Option<OnlineBackup>,
297
298    /// Trust the X-Forwarded-For header for client IP address. Defaults to false if unset.
299    trust_x_forward_for: Option<bool>,
300
301    /// The path to the "admin" socket, used for local communication when performing certain server control tasks. Default is set on build, based on the system target.
302    adminbindpath: Option<String>,
303
304    /// The maximum amount of threads the server will use for the async worker pool. Defaults
305    /// to std::threads::available_parallelism.
306    thread_count: Option<usize>,
307
308    /// Maximum Request Size in bytes
309    maximum_request_size_bytes: Option<usize>,
310
311    /// Don't touch this unless you know what you're doing!
312    #[allow(dead_code)]
313    db_arc_size: Option<usize>,
314    #[serde(default)]
315    #[serde(rename = "replication")]
316    /// Replication configuration, this is a development feature and not yet ready for production use.
317    repl_config: Option<ReplicationConfiguration>,
318    /// An optional OpenTelemetry collector (GRPC) url to send trace and log data to, eg `http://localhost:4317`. If not set, disables the feature.
319    otel_grpc_url: Option<String>,
320}
321
322impl ServerConfigUntagged {
323    /// loads the configuration file from the path specified, then overlays fields from environment variables starting with `KANIDM_``
324    pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
325        // see if we can load it from the config file you asked for
326        let mut f: File = File::open(config_path.as_ref()).inspect_err(|e| {
327            eprintln!("Unable to open config file [{e:?}] 🥺");
328            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
329            eprintln!("{diag}");
330        })?;
331
332        let mut contents = String::new();
333
334        f.read_to_string(&mut contents).inspect_err(|e| {
335            eprintln!("unable to read contents {e:?}");
336            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
337            eprintln!("{diag}");
338        })?;
339
340        // First, can we detect the config version?
341        let config_version = toml::from_str::<VersionDetection>(contents.as_str())
342            .map(|vd| vd.version)
343            .map_err(|err| {
344                eprintln!(
345                    "Unable to parse config version from '{:?}': {:?}",
346                    config_path.as_ref(),
347                    err
348                );
349                std::io::Error::new(std::io::ErrorKind::InvalidData, err)
350            })?;
351
352        match config_version {
353            Version::V2 => toml::from_str::<ServerConfigV2>(contents.as_str())
354                .map(|values| ServerConfigUntagged::Version(ServerConfigVersion::V2 { values })),
355            Version::Legacy => {
356                toml::from_str::<ServerConfig>(contents.as_str()).map(ServerConfigUntagged::Legacy)
357            }
358        }
359        .map_err(|err| {
360            eprintln!(
361                "Unable to parse config from '{:?}': {:?}",
362                config_path.as_ref(),
363                err
364            );
365            std::io::Error::new(std::io::ErrorKind::InvalidData, err)
366        })
367    }
368}
369
370#[serde_as]
371#[derive(Debug, Deserialize, Default)]
372#[serde(deny_unknown_fields)]
373pub struct ServerConfigV2 {
374    #[allow(dead_code)]
375    version: String,
376    domain: Option<String>,
377    origin: Option<Url>,
378    db_path: Option<PathBuf>,
379    db_fs_type: Option<kanidm_proto::internal::FsType>,
380    tls_chain: Option<PathBuf>,
381    tls_key: Option<PathBuf>,
382    tls_client_ca: Option<PathBuf>,
383
384    migration_path: 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    // We can bake in a private key for mTLS here.
420    // pub private_key: PKey
421
422    // We might need some condition variables / timers to force replication
423    // events? Or a channel to submit with oneshot responses.
424}
425
426/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
427#[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    // db type later
434    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 migration_path: Option<PathBuf>,
440
441    pub http_client_address_info: HttpAddressInfo,
442    pub ldap_client_address_info: LdapAddressInfo,
443
444    pub tls_config: Option<TlsConfiguration>,
445    pub integration_test_config: Option<Box<IntegrationTestConfig>>,
446    pub online_backup: Option<OnlineBackup>,
447    pub domain: String,
448    pub origin: Url,
449    pub role: ServerRole,
450    pub log_level: LogLevel,
451    /// Replication settings.
452    pub repl_config: Option<ReplicationConfiguration>,
453    /// This allows internally setting some unsafe options for replication.
454    pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
455    pub otel_grpc_url: Option<String>,
456}
457
458impl Configuration {
459    pub fn build() -> ConfigurationBuilder {
460        ConfigurationBuilder {
461            bindaddress: None,
462            ldapbindaddress: None,
463            // set by build profiles
464            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
465            threads: std::thread::available_parallelism()
466                .map(|t| t.get())
467                .unwrap_or_else(|_e| {
468                    eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
469                    4
470                }),
471            db_path: None,
472            db_fs_type: None,
473            db_arc_size: None,
474            migration_path: None,
475            maximum_request: 256 * 1024, // 256k
476            http_client_address_info: HttpAddressInfo::default(),
477            ldap_client_address_info: LdapAddressInfo::default(),
478            tls_key: None,
479            tls_chain: None,
480            tls_client_ca: None,
481            online_backup: None,
482            domain: None,
483            origin: None,
484            log_level: None,
485            role: None,
486            repl_config: None,
487            otel_grpc_url: None,
488        }
489    }
490
491    pub fn new_for_test() -> Self {
492        #[allow(clippy::expect_used)]
493        Configuration {
494            address: vec![DEFAULT_SERVER_ADDRESS.to_string()],
495            ldapbindaddress: None,
496            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
497            threads: 1,
498            db_path: None,
499            db_fs_type: None,
500            db_arc_size: None,
501            migration_path: None,
502            maximum_request: 256 * 1024, // 256k
503            http_client_address_info: HttpAddressInfo::default(),
504            ldap_client_address_info: LdapAddressInfo::default(),
505            tls_config: None,
506            integration_test_config: None,
507            online_backup: None,
508            domain: "idm.example.com".to_string(),
509            origin: Url::from_str("https://idm.example.com")
510                .expect("Failed to parse built-in string as URL"),
511            log_level: LogLevel::default(),
512            role: ServerRole::WriteReplica,
513            repl_config: None,
514            integration_repl_config: None,
515            otel_grpc_url: None,
516        }
517    }
518}
519
520impl fmt::Display for Configuration {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        for a in &self.address {
523            write!(f, "address: {a}, ")?;
524        }
525        write!(f, "domain: {}, ", self.domain)?;
526        match &self.ldapbindaddress {
527            Some(las) => {
528                for la in las {
529                    write!(f, "ldap address: {la}, ")?;
530                }
531            }
532            None => write!(f, "ldap address: disabled, ")?,
533        };
534        write!(f, "origin: {} ", self.origin)?;
535        write!(f, "admin bind path: {}, ", self.adminbindpath)?;
536        write!(f, "thread count: {}, ", self.threads)?;
537        write!(
538            f,
539            "dbpath: {}, ",
540            self.db_path
541                .as_ref()
542                .map(|p| p.to_string_lossy().to_string())
543                .unwrap_or("MEMORY".to_string())
544        )?;
545        match self.db_arc_size {
546            Some(v) => write!(f, "arcsize: {v}, "),
547            None => write!(f, "arcsize: AUTO, "),
548        }?;
549        write!(f, "max request size: {}b, ", self.maximum_request)?;
550        write!(
551            f,
552            "http client address info: {}, ",
553            self.http_client_address_info
554        )?;
555        write!(
556            f,
557            "ldap client address info: {}, ",
558            self.ldap_client_address_info
559        )?;
560
561        write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
562        match &self.online_backup {
563            Some(bck) => write!(
564                f,
565                "online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
566                bck.enabled,
567                bck.schedule,
568                bck.versions,
569                bck.path
570                    .as_ref()
571                    .map(|p| p.to_string_lossy().to_string())
572                    .unwrap_or("<unset>".to_string())
573            ),
574            None => write!(f, "online_backup: disabled, "),
575        }?;
576        write!(
577            f,
578            "integration mode: {}, ",
579            self.integration_test_config.is_some()
580        )?;
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/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
604#[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    migration_path: Option<PathBuf>,
614    maximum_request: usize,
615    http_client_address_info: HttpAddressInfo,
616    ldap_client_address_info: LdapAddressInfo,
617    tls_key: Option<PathBuf>,
618    tls_chain: Option<PathBuf>,
619    tls_client_ca: Option<PathBuf>,
620    online_backup: Option<OnlineBackup>,
621    domain: Option<String>,
622    origin: Option<Url>,
623    role: Option<ServerRole>,
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        // logging
633        if let Some(log_level) = &cli_config.log_level {
634            self.log_level = Some(*log_level);
635        }
636
637        if let Some(otel_grpc_url) = &cli_config.otel_grpc_url {
638            self.otel_grpc_url = Some(otel_grpc_url.clone());
639        }
640
641        // core domainy things
642        if let Some(domain) = &cli_config.domain {
643            self.domain = Some(domain.clone());
644        }
645
646        if let Some(origin) = &cli_config.origin {
647            self.origin = Some(origin.clone());
648        }
649
650        if let Some(role) = &cli_config.role {
651            self.role = Some(*role);
652        }
653
654        // networking
655
656        if cli_config.bindaddress.is_some() {
657            self.bindaddress = cli_config
658                .bindaddress
659                .clone()
660                .map(|s| s.split(',').map(|s| s.to_string()).collect())
661        }
662
663        if cli_config.ldapbindaddress.is_some() {
664            self.ldapbindaddress = cli_config
665                .ldapbindaddress
666                .clone()
667                .map(|s| s.split(',').map(|s| s.to_string()).collect())
668        }
669
670        // replication
671
672        if let Some(repl_origin) = cli_config.replication_origin.clone() {
673            if let Some(repl) = &mut self.repl_config {
674                repl.origin = repl_origin
675            } else {
676                self.repl_config = Some(ReplicationConfiguration {
677                    origin: repl_origin,
678                    ..Default::default()
679                });
680            }
681        }
682
683        if let Some(replication_bindaddress) = &cli_config.replication_bindaddress {
684            if let Some(repl_config) = &mut self.repl_config {
685                repl_config.bindaddress = *replication_bindaddress;
686            } else {
687                self.repl_config = Some(ReplicationConfiguration {
688                    bindaddress: *replication_bindaddress,
689                    ..Default::default()
690                });
691            }
692        }
693
694        if let Some(task_poll_interval) = cli_config.replication_task_poll_interval {
695            if let Some(repl) = &mut self.repl_config {
696                repl.task_poll_interval = Some(task_poll_interval);
697            } else {
698                self.repl_config = Some(ReplicationConfiguration {
699                    task_poll_interval: Some(task_poll_interval),
700                    ..Default::default()
701                });
702            }
703        }
704
705        // tls
706
707        if let Some(tls_key) = &cli_config.tls_key {
708            self.tls_key = Some(tls_key.clone());
709        }
710
711        if let Some(tls_chain) = &cli_config.tls_chain {
712            self.tls_chain = Some(tls_chain.clone());
713        }
714
715        if let Some(tls_client_ca) = &cli_config.tls_client_ca {
716            self.tls_client_ca = Some(tls_client_ca.clone());
717        }
718
719        // filesystem things
720        if let Some(adminbindpath) = &cli_config.admin_bind_path {
721            self.adminbindpath = adminbindpath.clone();
722        }
723
724        if let Some(db_path) = &cli_config.db_path {
725            self.db_path = Some(db_path.clone());
726        }
727
728        if let Some(db_fs_type) = &cli_config.db_fs_type {
729            self.db_fs_type = Some(*db_fs_type);
730        }
731
732        if cli_config.db_arc_size.is_some() {
733            self.db_arc_size = cli_config.db_arc_size;
734        }
735
736        // backup things
737        if let Some(online_backup_path) = &cli_config.online_backup_path {
738            if let Some(backup) = &mut self.online_backup {
739                backup.path = Some(online_backup_path.clone());
740            } else {
741                self.online_backup = Some(OnlineBackup {
742                    path: Some(online_backup_path.clone()),
743                    ..Default::default()
744                });
745            }
746        }
747
748        if let Some(online_backup_schedule) = &cli_config.online_backup_schedule {
749            if let Some(backup) = &mut self.online_backup {
750                backup.schedule = online_backup_schedule.clone();
751            } else {
752                self.online_backup = Some(OnlineBackup {
753                    schedule: online_backup_schedule.clone(),
754                    ..Default::default()
755                });
756            }
757        }
758
759        if let Some(online_backup_versions) = &cli_config.online_backup_versions {
760            if let Some(backup) = &mut self.online_backup {
761                backup.versions = *online_backup_versions;
762            } else {
763                self.online_backup = Some(OnlineBackup {
764                    versions: *online_backup_versions,
765                    ..Default::default()
766                });
767            }
768        }
769
770        // XFF handling
771        if let Some(true) = cli_config.trust_all_x_forwarded_for {
772            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
773        }
774
775        self
776    }
777
778    pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
779        // Can only proceed if the config is real
780        let Some(toml_config) = toml_config else {
781            return self;
782        };
783
784        match toml_config {
785            ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
786                self.add_v2_config(values)
787            }
788            ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
789        }
790    }
791
792    fn add_legacy_config(mut self, config: ServerConfig) -> Self {
793        if config.domain.is_some() {
794            self.domain = config.domain;
795        }
796
797        if config.origin.is_some() {
798            self.origin = config.origin;
799        }
800
801        if config.db_path.is_some() {
802            self.db_path = config.db_path;
803        }
804
805        if config.db_fs_type.is_some() {
806            self.db_fs_type = config.db_fs_type;
807        }
808
809        if config.tls_key.is_some() {
810            self.tls_key = config.tls_key;
811        }
812
813        if config.tls_chain.is_some() {
814            self.tls_chain = config.tls_chain;
815        }
816
817        if config.tls_client_ca.is_some() {
818            self.tls_client_ca = config.tls_client_ca;
819        }
820
821        if config.bindaddress.is_some() {
822            self.bindaddress = config.bindaddress.map(|a| vec![a]);
823        }
824
825        if config.ldapbindaddress.is_some() {
826            self.ldapbindaddress = config.ldapbindaddress.map(|a| vec![a]);
827        }
828
829        if let Some(adminbindpath) = config.adminbindpath {
830            self.adminbindpath = adminbindpath;
831        }
832
833        if config.role.is_some() {
834            self.role = config.role;
835        }
836
837        if config.log_level.is_some() {
838            self.log_level = config.log_level;
839        }
840
841        if let Some(threads) = config.thread_count {
842            self.threads = threads;
843        }
844
845        if let Some(maximum) = config.maximum_request_size_bytes {
846            self.maximum_request = maximum;
847        }
848
849        if config.db_arc_size.is_some() {
850            self.db_arc_size = config.db_arc_size;
851        }
852
853        if config.trust_x_forward_for == Some(true) {
854            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
855        }
856
857        if config.online_backup.is_some() {
858            self.online_backup = config.online_backup;
859        }
860
861        if config.repl_config.is_some() {
862            self.repl_config = config.repl_config;
863        }
864
865        if config.otel_grpc_url.is_some() {
866            self.otel_grpc_url = config.otel_grpc_url;
867        }
868
869        self
870    }
871
872    fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
873        if config.domain.is_some() {
874            self.domain = config.domain;
875        }
876
877        if config.origin.is_some() {
878            self.origin = config.origin;
879        }
880
881        if config.db_path.is_some() {
882            self.db_path = config.db_path;
883        }
884
885        if config.migration_path.is_some() {
886            self.migration_path = config.migration_path;
887        }
888
889        if config.db_fs_type.is_some() {
890            self.db_fs_type = config.db_fs_type;
891        }
892
893        if config.tls_key.is_some() {
894            self.tls_key = config.tls_key;
895        }
896
897        if config.tls_chain.is_some() {
898            self.tls_chain = config.tls_chain;
899        }
900
901        if config.tls_client_ca.is_some() {
902            self.tls_client_ca = config.tls_client_ca;
903        }
904
905        if config.bindaddress.is_some() {
906            self.bindaddress = config.bindaddress;
907        }
908
909        if config.ldapbindaddress.is_some() {
910            self.ldapbindaddress = config.ldapbindaddress;
911        }
912
913        if let Some(adminbindpath) = config.adminbindpath {
914            self.adminbindpath = adminbindpath;
915        }
916
917        if config.role.is_some() {
918            self.role = config.role;
919        }
920
921        if config.log_level.is_some() {
922            self.log_level = config.log_level;
923        }
924
925        if let Some(threads) = config.thread_count {
926            self.threads = threads;
927        }
928
929        if let Some(maximum) = config.maximum_request_size_bytes {
930            self.maximum_request = maximum;
931        }
932
933        if config.db_arc_size.is_some() {
934            self.db_arc_size = config.db_arc_size;
935        }
936
937        if let Some(http_client_address_info) = config.http_client_address_info {
938            self.http_client_address_info = http_client_address_info
939        }
940
941        if let Some(ldap_client_address_info) = config.ldap_client_address_info {
942            self.ldap_client_address_info = ldap_client_address_info
943        }
944
945        if config.online_backup.is_some() {
946            self.online_backup = config.online_backup;
947        }
948
949        if config.repl_config.is_some() {
950            self.repl_config = config.repl_config;
951        }
952
953        if config.otel_grpc_url.is_some() {
954            self.otel_grpc_url = config.otel_grpc_url;
955        }
956
957        self
958    }
959
960    // We always set threads to 1 unless it's the main server.
961    pub fn is_server_mode(mut self, is_server: bool) -> Self {
962        if is_server {
963            self.threads = 1;
964        }
965        self
966    }
967
968    pub fn finish(self) -> Option<Configuration> {
969        let ConfigurationBuilder {
970            bindaddress,
971            ldapbindaddress,
972            adminbindpath,
973            threads,
974            db_path,
975            db_fs_type,
976            db_arc_size,
977            migration_path,
978            maximum_request,
979            http_client_address_info,
980            ldap_client_address_info,
981            tls_key,
982            tls_chain,
983            tls_client_ca,
984            mut online_backup,
985            domain,
986            origin,
987            role,
988            log_level,
989            repl_config,
990            otel_grpc_url,
991        } = self;
992
993        let tls_config = match (tls_key, tls_chain, tls_client_ca) {
994            (Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
995                chain,
996                key,
997                client_ca,
998            }),
999            _ => {
1000                eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
1001                return None;
1002            }
1003        };
1004
1005        let domain = domain.or_else(|| {
1006            eprintln!("ERROR: domain was not set.");
1007            None
1008        })?;
1009
1010        let origin = origin.or_else(|| {
1011            eprintln!("ERROR: origin was not set.");
1012            None
1013        })?;
1014
1015        if let Some(online_backup_ref) = online_backup.as_mut() {
1016            if online_backup_ref.path.is_none() {
1017                if let Some(db_path) = db_path.as_ref() {
1018                    if let Some(db_parent_path) = db_path.parent() {
1019                        online_backup_ref.path = Some(db_parent_path.to_path_buf());
1020                    } else {
1021                        eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
1022                        return None;
1023                    }
1024                } else {
1025                    eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
1026                    return None;
1027                }
1028            }
1029        };
1030
1031        // Apply any defaults if needed
1032        let address = bindaddress.unwrap_or(vec![DEFAULT_SERVER_ADDRESS.to_string()]);
1033        let role = role.unwrap_or(ServerRole::WriteReplica);
1034        let log_level = log_level.unwrap_or_default();
1035
1036        Some(Configuration {
1037            address,
1038            ldapbindaddress,
1039            adminbindpath,
1040            threads,
1041            db_path,
1042            db_fs_type,
1043            db_arc_size,
1044            migration_path,
1045            maximum_request,
1046            http_client_address_info,
1047            ldap_client_address_info,
1048            tls_config,
1049            online_backup,
1050            domain,
1051            origin,
1052            role,
1053            log_level,
1054            repl_config,
1055            otel_grpc_url,
1056            integration_repl_config: None,
1057            integration_test_config: None,
1058        })
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
1065    use std::net::{Ipv4Addr, Ipv6Addr};
1066
1067    #[test]
1068    fn assert_cidr_parsing_behaviour() {
1069        // Assert that we can parse individual hosts, and ranges
1070        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.1\"").unwrap();
1071        let expect_ip_cidr = IpCidr::from(Ipv4Addr::new(127, 0, 0, 1));
1072        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1073
1074        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.0/8\"").unwrap();
1075        let expect_ip_cidr = IpCidr::from(Ipv4Cidr::new(Ipv4Addr::new(127, 0, 0, 0), 8).unwrap());
1076        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1077
1078        // Same for ipv6
1079        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::1\"").unwrap();
1080        let expect_ip_cidr = IpCidr::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0001));
1081        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1082
1083        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::/64\"").unwrap();
1084        let expect_ip_cidr = IpCidr::from(
1085            Ipv6Cidr::new(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 64).unwrap(),
1086        );
1087        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1088    }
1089}