Skip to main content

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::constants::DEFAULT_SERVER_ADDRESS;
10use kanidm_proto::internal::FsType;
11use serde::Deserialize;
12use serde_with::{formats::PreferOne, serde_as, OneOrMany};
13use sketching::LogLevel;
14use std::fmt::{self, Display};
15use std::fs::File;
16use std::io::Read;
17use std::net::IpAddr;
18use std::path::{Path, PathBuf};
19use std::str::FromStr;
20use std::sync::Arc;
21use url::Url;
22
23use crate::repl::config::ReplicationConfiguration;
24
25#[derive(Debug, Deserialize)]
26struct VersionDetection {
27    #[serde(default)]
28    version: Version,
29}
30
31#[derive(Debug, Deserialize, Default)]
32// #[serde(tag = "version")]
33pub enum Version {
34    #[serde(rename = "2")]
35    V2,
36
37    #[default]
38    Legacy,
39}
40
41// Allowed as the large enum is only short lived at startup to the true config
42#[allow(clippy::large_enum_variant)]
43pub enum ServerConfigUntagged {
44    Version(ServerConfigVersion),
45    Legacy(ServerConfig),
46}
47
48pub enum ServerConfigVersion {
49    V2 { values: ServerConfigV2 },
50}
51
52#[derive(Deserialize, Debug, Clone)]
53pub struct OnlineBackup {
54    /// The destination folder for your backups, defaults to the db_path dir if not set
55    pub path: Option<PathBuf>,
56    /// The schedule to run online backups (see <https://crontab.guru/>), defaults to @daily
57    ///
58    /// Examples:
59    ///
60    /// - every day at 22:00 UTC (default): `"00 22 * * *"`
61    /// - every 6th hours (four times a day) at 3 minutes past the hour, :
62    ///   `"03 */6 * * *"`
63    ///
64    /// We also support non standard cron syntax, with the following format:
65    ///
66    /// `<sec>  <min>   <hour>   <day of month>   <month>   <day of week>   <year>`
67    ///
68    /// eg:
69    /// - `1 2 3 5 12 * 2023` would only back up once on the 5th of December 2023 at 03:02:01am.
70    /// - `3 2 1 * * Mon *` backs up every Monday at 03:02:01am.
71    ///
72    /// (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)
73    pub schedule: String,
74    #[serde(default = "default_online_backup_versions")]
75    /// How many past backup versions to keep, defaults to 7
76    pub versions: usize,
77    /// Enabled by default
78    #[serde(default = "default_online_backup_enabled")]
79    pub enabled: bool,
80
81    #[serde(default)]
82    pub compression: BackupCompression,
83}
84
85impl Default for OnlineBackup {
86    fn default() -> Self {
87        OnlineBackup {
88            path: None, // This makes it revert to the kanidm_db path
89            schedule: default_online_backup_schedule(),
90            versions: default_online_backup_versions(),
91            enabled: default_online_backup_enabled(),
92            compression: BackupCompression::default(),
93        }
94    }
95}
96
97fn default_online_backup_enabled() -> bool {
98    true
99}
100
101fn default_online_backup_schedule() -> String {
102    "@daily".to_string()
103}
104
105fn default_online_backup_versions() -> usize {
106    7
107}
108
109#[derive(Deserialize, Debug, Clone)]
110pub struct TlsConfiguration {
111    pub chain: PathBuf,
112    pub key: PathBuf,
113    pub client_ca: Option<PathBuf>,
114}
115
116#[derive(Debug, Default)]
117pub enum TcpAddressInfo {
118    #[default]
119    None,
120    ProxyV2(Vec<IpCidr>),
121    ProxyV1(Vec<IpCidr>),
122}
123
124#[derive(Deserialize, Debug, Clone, Default)]
125pub enum LdapAddressInfo {
126    #[default]
127    None,
128    #[serde(rename = "proxy-v2")]
129    ProxyV2(Vec<IpCidr>),
130    #[serde(rename = "proxy-v1")]
131    ProxyV1(Vec<IpCidr>),
132}
133
134impl LdapAddressInfo {
135    pub fn trusted_tcp_info(&self) -> Arc<TcpAddressInfo> {
136        Arc::new(match self {
137            LdapAddressInfo::None => TcpAddressInfo::None,
138            LdapAddressInfo::ProxyV2(trusted) => TcpAddressInfo::ProxyV2(trusted.clone()),
139            LdapAddressInfo::ProxyV1(trusted) => TcpAddressInfo::ProxyV1(trusted.clone()),
140        })
141    }
142}
143
144impl Display for LdapAddressInfo {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            Self::None => f.write_str("none"),
148            Self::ProxyV2(trusted) => {
149                f.write_str("proxy-v2 [ ")?;
150                for ip in trusted {
151                    write!(f, "{ip} ")?;
152                }
153                f.write_str("]")
154            }
155            Self::ProxyV1(trusted) => {
156                f.write_str("proxy-v1 [ ")?;
157                for ip in trusted {
158                    write!(f, "{ip} ")?;
159                }
160                f.write_str("]")
161            }
162        }
163    }
164}
165
166pub(crate) enum AddressSet {
167    NonContiguousIpSet(Vec<IpCidr>),
168    All,
169}
170
171impl AddressSet {
172    pub(crate) fn contains(&self, ip_addr: &IpAddr) -> bool {
173        match self {
174            Self::All => true,
175            Self::NonContiguousIpSet(range) => {
176                range.iter().any(|ip_cidr| ip_cidr.contains(ip_addr))
177            }
178        }
179    }
180}
181
182#[derive(Deserialize, Debug, Clone, Default)]
183pub enum HttpAddressInfo {
184    #[default]
185    None,
186    #[serde(rename = "x-forward-for")]
187    XForwardFor(Vec<IpCidr>),
188    // IMPORTANT: This is undocumented, and only exists for backwards compat
189    // with config v1 which has a boolean toggle for this option.
190    #[serde(rename = "x-forward-for-all-source-trusted")]
191    XForwardForAllSourcesTrusted,
192    #[serde(rename = "proxy-v2")]
193    ProxyV2(Vec<IpCidr>),
194    #[serde(rename = "proxy-v1")]
195    ProxyV1(Vec<IpCidr>),
196}
197
198impl HttpAddressInfo {
199    pub(crate) fn trusted_x_forward_for(&self) -> Option<AddressSet> {
200        match self {
201            Self::XForwardForAllSourcesTrusted => Some(AddressSet::All),
202            Self::XForwardFor(trusted) => Some(AddressSet::NonContiguousIpSet(trusted.clone())),
203            _ => None,
204        }
205    }
206
207    pub fn trusted_tcp_info(&self) -> Arc<TcpAddressInfo> {
208        Arc::new(match self {
209            Self::ProxyV2(trusted) => TcpAddressInfo::ProxyV2(trusted.clone()),
210            Self::ProxyV1(trusted) => TcpAddressInfo::ProxyV1(trusted.clone()),
211            Self::None | Self::XForwardFor(_) | Self::XForwardForAllSourcesTrusted => {
212                TcpAddressInfo::None
213            }
214        })
215    }
216}
217
218impl Display for HttpAddressInfo {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::None => f.write_str("none"),
222
223            Self::XForwardFor(trusted) => {
224                f.write_str("x-forward-for [ ")?;
225                for ip in trusted {
226                    write!(f, "{ip} ")?;
227                }
228                f.write_str("]")
229            }
230            Self::XForwardForAllSourcesTrusted => {
231                f.write_str("x-forward-for [ ALL SOURCES TRUSTED ]")
232            }
233            Self::ProxyV2(trusted) => {
234                f.write_str("proxy-v2 [ ")?;
235                for ip in trusted {
236                    write!(f, "{ip} ")?;
237                }
238                f.write_str("]")
239            }
240            Self::ProxyV1(trusted) => {
241                f.write_str("proxy-v1 [ ")?;
242                for ip in trusted {
243                    write!(f, "{ip} ")?;
244                }
245                f.write_str("]")
246            }
247        }
248    }
249}
250
251#[derive(Debug, Deserialize, Clone, Copy, Default, Eq, PartialEq)]
252pub enum ServerRole {
253    #[default]
254    WriteReplica,
255    WriteReplicaNoUI,
256    ReadOnlyReplica,
257}
258
259impl Display for ServerRole {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        match self {
262            ServerRole::WriteReplica => f.write_str("write replica"),
263            ServerRole::WriteReplicaNoUI => f.write_str("write replica (no ui)"),
264            ServerRole::ReadOnlyReplica => f.write_str("read only replica"),
265        }
266    }
267}
268
269impl FromStr for ServerRole {
270    type Err = &'static str;
271
272    fn from_str(s: &str) -> Result<Self, Self::Err> {
273        match s {
274            "write_replica" => Ok(ServerRole::WriteReplica),
275            "write_replica_no_ui" => Ok(ServerRole::WriteReplicaNoUI),
276            "read_only_replica" => Ok(ServerRole::ReadOnlyReplica),
277            _ => Err("Must be one of write_replica, write_replica_no_ui, read_only_replica"),
278        }
279    }
280}
281
282/// This is the Server Configuration as read from `server.toml` or environment variables.
283///
284/// Fields noted as "REQUIRED" are required for the server to start, even if they show as optional due to how file parsing works.
285///
286/// 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.
287///
288/// NOTE: not all flags or values from the internal [Configuration] object are exposed via this structure
289/// to prevent certain settings being set (e.g. integration test modes)
290#[derive(Debug, Deserialize, Default)]
291#[serde(deny_unknown_fields)]
292pub struct ServerConfig {
293    /// *REQUIRED* - Kanidm Domain, eg `kanidm.example.com`.
294    domain: Option<String>,
295    /// *REQUIRED* - The user-facing HTTPS URL for this server, eg <https://idm.example.com>
296    origin: Option<Url>,
297    /// File path of the database file
298    db_path: Option<PathBuf>,
299    /// The filesystem type, either "zfs" or "generic". Defaults to "generic" if unset. I you change this, run a database vacuum.
300    db_fs_type: Option<kanidm_proto::internal::FsType>,
301
302    ///  *REQUIRED* - The file path to the TLS Certificate Chain
303    tls_chain: Option<PathBuf>,
304    ///  *REQUIRED* - The file path to the TLS Private Key
305    tls_key: Option<PathBuf>,
306
307    /// The directory path of the client ca and crl dir.
308    tls_client_ca: Option<PathBuf>,
309
310    /// The listener address for the HTTPS server.
311    ///
312    /// eg. `[::]:8443` or `127.0.0.1:8443`. Defaults to [kanidm_proto::constants::DEFAULT_SERVER_ADDRESS]
313    bindaddress: Option<String>,
314    /// The listener address for the LDAP server.
315    ///
316    /// eg. `[::]:3636` or `127.0.0.1:3636`.
317    ///
318    /// If unset, the LDAP server will be disabled.
319    ldapbindaddress: Option<String>,
320    /// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica]
321    role: Option<ServerRole>,
322    /// The log level, one of info, debug, trace. Defaults to "info" if not set.
323    log_level: Option<LogLevel>,
324
325    /// Backup Configuration, see [OnlineBackup] for details on sub-keys.
326    online_backup: Option<OnlineBackup>,
327
328    /// Trust the X-Forwarded-For header for client IP address. Defaults to false if unset.
329    trust_x_forward_for: Option<bool>,
330
331    /// 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.
332    adminbindpath: Option<String>,
333
334    /// The maximum amount of threads the server will use for the async worker pool. Defaults
335    /// to std::threads::available_parallelism.
336    thread_count: Option<usize>,
337
338    /// Maximum Request Size in bytes
339    maximum_request_size_bytes: Option<usize>,
340
341    /// Don't touch this unless you know what you're doing!
342    #[allow(dead_code)]
343    db_arc_size: Option<usize>,
344    #[serde(default)]
345    #[serde(rename = "replication")]
346    /// Replication configuration, this is a development feature and not yet ready for production use.
347    repl_config: Option<ReplicationConfiguration>,
348    /// An optional OpenTelemetry collector (GRPC) url to send trace and log data to, eg `http://localhost:4317`. If not set, disables the feature.
349    otel_grpc_url: Option<String>,
350}
351
352impl ServerConfigUntagged {
353    /// loads the configuration file from the path specified, then overlays fields from environment variables starting with `KANIDM_``
354    pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
355        // see if we can load it from the config file you asked for
356        let mut f: File = File::open(config_path.as_ref()).inspect_err(|e| {
357            eprintln!("Unable to open config file [{e:?}] 🥺");
358            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
359            eprintln!("{diag}");
360        })?;
361
362        let mut contents = String::new();
363
364        f.read_to_string(&mut contents).inspect_err(|e| {
365            eprintln!("unable to read contents {e:?}");
366            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
367            eprintln!("{diag}");
368        })?;
369
370        // First, can we detect the config version?
371        let config_version = toml::from_str::<VersionDetection>(contents.as_str())
372            .map(|vd| vd.version)
373            .map_err(|err| {
374                eprintln!(
375                    "Unable to parse config version from '{:?}': {:?}",
376                    config_path.as_ref(),
377                    err
378                );
379                std::io::Error::new(std::io::ErrorKind::InvalidData, err)
380            })?;
381
382        match config_version {
383            Version::V2 => toml::from_str::<ServerConfigV2>(contents.as_str())
384                .map(|values| ServerConfigUntagged::Version(ServerConfigVersion::V2 { values })),
385            Version::Legacy => {
386                toml::from_str::<ServerConfig>(contents.as_str()).map(ServerConfigUntagged::Legacy)
387            }
388        }
389        .map_err(|err| {
390            eprintln!(
391                "Unable to parse config from '{:?}': {:?}",
392                config_path.as_ref(),
393                err
394            );
395            std::io::Error::new(std::io::ErrorKind::InvalidData, err)
396        })
397    }
398}
399
400#[serde_as]
401#[derive(Debug, Deserialize, Default)]
402#[serde(deny_unknown_fields)]
403pub struct ServerConfigV2 {
404    #[allow(dead_code)]
405    version: String,
406    domain: Option<String>,
407    origin: Option<Url>,
408    db_path: Option<PathBuf>,
409    db_fs_type: Option<kanidm_proto::internal::FsType>,
410    tls_chain: Option<PathBuf>,
411    tls_key: Option<PathBuf>,
412    tls_client_ca: Option<PathBuf>,
413
414    migration_path: Option<PathBuf>,
415
416    #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
417    bindaddress: Option<Vec<String>>,
418    #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
419    ldapbindaddress: Option<Vec<String>>,
420
421    role: Option<ServerRole>,
422    log_level: Option<LogLevel>,
423    online_backup: Option<OnlineBackup>,
424
425    http_client_address_info: Option<HttpAddressInfo>,
426    ldap_client_address_info: Option<LdapAddressInfo>,
427
428    adminbindpath: Option<String>,
429    thread_count: Option<usize>,
430    maximum_request_size_bytes: Option<usize>,
431    #[allow(dead_code)]
432    db_arc_size: Option<usize>,
433    #[serde(default)]
434    #[serde(rename = "replication")]
435    repl_config: Option<ReplicationConfiguration>,
436    otel_grpc_url: Option<String>,
437}
438
439#[derive(Debug, Clone)]
440pub struct IntegrationTestConfig {
441    pub admin_user: String,
442    pub admin_password: String,
443    pub idm_admin_user: String,
444    pub idm_admin_password: String,
445}
446
447#[derive(Debug, Clone)]
448pub struct IntegrationReplConfig {
449    // We can bake in a private key for mTLS here.
450    // pub private_key: PKey
451
452    // We might need some condition variables / timers to force replication
453    // events? Or a channel to submit with oneshot responses.
454}
455
456/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
457#[derive(Debug, Clone)]
458pub struct Configuration {
459    pub address: Vec<String>,
460    pub ldapbindaddress: Option<Vec<String>>,
461    pub adminbindpath: String,
462    pub threads: usize,
463    // db type later
464    pub db_path: Option<PathBuf>,
465    pub db_fs_type: Option<FsType>,
466    pub db_arc_size: Option<usize>,
467    pub maximum_request: usize,
468
469    pub migration_path: Option<PathBuf>,
470
471    pub http_client_address_info: HttpAddressInfo,
472    pub ldap_client_address_info: LdapAddressInfo,
473
474    pub tls_config: Option<TlsConfiguration>,
475    pub integration_test_config: Option<Box<IntegrationTestConfig>>,
476    pub online_backup: Option<OnlineBackup>,
477    pub domain: String,
478    pub origin: Url,
479    pub role: ServerRole,
480    pub log_level: LogLevel,
481    /// Replication settings.
482    pub repl_config: Option<ReplicationConfiguration>,
483    /// This allows internally setting some unsafe options for replication.
484    pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
485    pub otel_grpc_url: Option<String>,
486}
487
488impl Configuration {
489    pub fn build() -> ConfigurationBuilder {
490        ConfigurationBuilder {
491            bindaddress: None,
492            ldapbindaddress: None,
493            // set by build profiles
494            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
495            threads: std::thread::available_parallelism()
496                .map(|t| t.get())
497                .unwrap_or_else(|_e| {
498                    eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
499                    4
500                }),
501            db_path: None,
502            db_fs_type: None,
503            db_arc_size: None,
504            migration_path: None,
505            maximum_request: 256 * 1024, // 256k
506            http_client_address_info: HttpAddressInfo::default(),
507            ldap_client_address_info: LdapAddressInfo::default(),
508            tls_key: None,
509            tls_chain: None,
510            tls_client_ca: None,
511            online_backup: None,
512            domain: None,
513            origin: None,
514            log_level: None,
515            role: None,
516            repl_config: None,
517            otel_grpc_url: None,
518        }
519    }
520
521    pub fn new_for_test() -> Self {
522        #[allow(clippy::expect_used)]
523        Configuration {
524            address: vec![DEFAULT_SERVER_ADDRESS.to_string()],
525            ldapbindaddress: None,
526            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
527            threads: 1,
528            db_path: None,
529            db_fs_type: None,
530            db_arc_size: None,
531            migration_path: None,
532            maximum_request: 256 * 1024, // 256k
533            http_client_address_info: HttpAddressInfo::default(),
534            ldap_client_address_info: LdapAddressInfo::default(),
535            tls_config: None,
536            integration_test_config: None,
537            online_backup: None,
538            domain: "idm.example.com".to_string(),
539            origin: Url::from_str("https://idm.example.com")
540                .expect("Failed to parse built-in string as URL"),
541            log_level: LogLevel::default(),
542            role: ServerRole::WriteReplicaNoUI,
543            repl_config: None,
544            integration_repl_config: None,
545            otel_grpc_url: None,
546        }
547    }
548}
549
550impl fmt::Display for Configuration {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        for a in &self.address {
553            write!(f, "address: {a}, ")?;
554        }
555        write!(f, "domain: {}, ", self.domain)?;
556        match &self.ldapbindaddress {
557            Some(las) => {
558                for la in las {
559                    write!(f, "ldap address: {la}, ")?;
560                }
561            }
562            None => write!(f, "ldap address: disabled, ")?,
563        };
564        write!(f, "origin: {} ", self.origin)?;
565        write!(f, "admin bind path: {}, ", self.adminbindpath)?;
566        write!(f, "thread count: {}, ", self.threads)?;
567        write!(
568            f,
569            "dbpath: {}, ",
570            self.db_path
571                .as_ref()
572                .map(|p| p.to_string_lossy().to_string())
573                .unwrap_or("MEMORY".to_string())
574        )?;
575        match self.db_arc_size {
576            Some(v) => write!(f, "arcsize: {v}, "),
577            None => write!(f, "arcsize: AUTO, "),
578        }?;
579        write!(f, "max request size: {}b, ", self.maximum_request)?;
580        write!(
581            f,
582            "http client address info: {}, ",
583            self.http_client_address_info
584        )?;
585        write!(
586            f,
587            "ldap client address info: {}, ",
588            self.ldap_client_address_info
589        )?;
590
591        write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
592        match &self.online_backup {
593            Some(bck) => write!(
594                f,
595                "online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
596                bck.enabled,
597                bck.schedule,
598                bck.versions,
599                bck.path
600                    .as_ref()
601                    .map(|p| p.to_string_lossy().to_string())
602                    .unwrap_or("<unset>".to_string())
603            ),
604            None => write!(f, "online_backup: disabled, "),
605        }?;
606        write!(
607            f,
608            "integration mode: {}, ",
609            self.integration_test_config.is_some()
610        )?;
611        write!(f, "log_level: {}", self.log_level)?;
612        write!(f, "role: {}, ", self.role)?;
613        match &self.repl_config {
614            Some(repl) => {
615                write!(f, "replication: enabled")?;
616                write!(f, "repl_origin: {} ", repl.origin)?;
617                write!(f, "repl_address: {} ", repl.bindaddress)?;
618                write!(
619                    f,
620                    "integration repl config mode: {}, ",
621                    self.integration_repl_config.is_some()
622                )?;
623            }
624            None => {
625                write!(f, "replication: disabled, ")?;
626            }
627        }
628        write!(f, "otel_grpc_url: {:?}", self.otel_grpc_url)?;
629        Ok(())
630    }
631}
632
633/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
634#[derive(Debug, Clone)]
635pub struct ConfigurationBuilder {
636    bindaddress: Option<Vec<String>>,
637    ldapbindaddress: Option<Vec<String>>,
638    adminbindpath: String,
639    threads: usize,
640    db_path: Option<PathBuf>,
641    db_fs_type: Option<FsType>,
642    db_arc_size: Option<usize>,
643    migration_path: Option<PathBuf>,
644    maximum_request: usize,
645    http_client_address_info: HttpAddressInfo,
646    ldap_client_address_info: LdapAddressInfo,
647    tls_key: Option<PathBuf>,
648    tls_chain: Option<PathBuf>,
649    tls_client_ca: Option<PathBuf>,
650    online_backup: Option<OnlineBackup>,
651    domain: Option<String>,
652    origin: Option<Url>,
653    role: Option<ServerRole>,
654    log_level: Option<LogLevel>,
655    repl_config: Option<ReplicationConfiguration>,
656    otel_grpc_url: Option<String>,
657}
658
659impl ConfigurationBuilder {
660    #![allow(clippy::needless_pass_by_value)]
661    pub fn add_cli_config(mut self, cli_config: &kanidm_proto::cli::KanidmdCli) -> Self {
662        // logging
663        if let Some(log_level) = &cli_config.log_level {
664            self.log_level = Some(*log_level);
665        }
666
667        if let Some(otel_grpc_url) = &cli_config.otel_grpc_url {
668            self.otel_grpc_url = Some(otel_grpc_url.clone());
669        }
670
671        // core domainy things
672        if let Some(domain) = &cli_config.domain {
673            self.domain = Some(domain.clone());
674        }
675
676        if let Some(origin) = &cli_config.origin {
677            self.origin = Some(origin.clone());
678        }
679
680        // networking
681
682        if cli_config.bindaddress.is_some() {
683            self.bindaddress = cli_config
684                .bindaddress
685                .clone()
686                .map(|s| s.split(',').map(|s| s.to_string()).collect())
687        }
688
689        if cli_config.ldapbindaddress.is_some() {
690            self.ldapbindaddress = cli_config
691                .ldapbindaddress
692                .clone()
693                .map(|s| s.split(',').map(|s| s.to_string()).collect())
694        }
695
696        // replication
697
698        if let Some(repl_origin) = cli_config.replication_origin.clone() {
699            if let Some(repl) = &mut self.repl_config {
700                repl.origin = repl_origin
701            } else {
702                self.repl_config = Some(ReplicationConfiguration {
703                    origin: repl_origin,
704                    ..Default::default()
705                });
706            }
707        }
708
709        if let Some(replication_bindaddress) = &cli_config.replication_bindaddress {
710            if let Some(repl_config) = &mut self.repl_config {
711                repl_config.bindaddress = *replication_bindaddress;
712            } else {
713                self.repl_config = Some(ReplicationConfiguration {
714                    bindaddress: *replication_bindaddress,
715                    ..Default::default()
716                });
717            }
718        }
719
720        if let Some(task_poll_interval) = cli_config.replication_task_poll_interval {
721            if let Some(repl) = &mut self.repl_config {
722                repl.task_poll_interval = Some(task_poll_interval);
723            } else {
724                self.repl_config = Some(ReplicationConfiguration {
725                    task_poll_interval: Some(task_poll_interval),
726                    ..Default::default()
727                });
728            }
729        }
730
731        // tls
732
733        if let Some(tls_key) = &cli_config.tls_key {
734            self.tls_key = Some(tls_key.clone());
735        }
736
737        if let Some(tls_chain) = &cli_config.tls_chain {
738            self.tls_chain = Some(tls_chain.clone());
739        }
740
741        if let Some(tls_client_ca) = &cli_config.tls_client_ca {
742            self.tls_client_ca = Some(tls_client_ca.clone());
743        }
744
745        // filesystem things
746        if let Some(adminbindpath) = &cli_config.admin_bind_path {
747            self.adminbindpath = adminbindpath.clone();
748        }
749
750        if let Some(db_path) = &cli_config.db_path {
751            self.db_path = Some(db_path.clone());
752        }
753
754        if let Some(db_fs_type) = &cli_config.db_fs_type {
755            self.db_fs_type = Some(*db_fs_type);
756        }
757
758        if cli_config.db_arc_size.is_some() {
759            self.db_arc_size = cli_config.db_arc_size;
760        }
761
762        // backup things
763        if let Some(online_backup_path) = &cli_config.online_backup_path {
764            if let Some(backup) = &mut self.online_backup {
765                backup.path = Some(online_backup_path.clone());
766            } else {
767                self.online_backup = Some(OnlineBackup {
768                    path: Some(online_backup_path.clone()),
769                    ..Default::default()
770                });
771            }
772        }
773
774        if let Some(online_backup_schedule) = &cli_config.online_backup_schedule {
775            if let Some(backup) = &mut self.online_backup {
776                backup.schedule = online_backup_schedule.clone();
777            } else {
778                self.online_backup = Some(OnlineBackup {
779                    schedule: online_backup_schedule.clone(),
780                    ..Default::default()
781                });
782            }
783        }
784
785        if let Some(online_backup_versions) = &cli_config.online_backup_versions {
786            if let Some(backup) = &mut self.online_backup {
787                backup.versions = *online_backup_versions;
788            } else {
789                self.online_backup = Some(OnlineBackup {
790                    versions: *online_backup_versions,
791                    ..Default::default()
792                });
793            }
794        }
795
796        // XFF handling
797        if let Some(true) = cli_config.trust_all_x_forwarded_for {
798            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
799        }
800
801        self
802    }
803
804    pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
805        // Can only proceed if the config is real
806        let Some(toml_config) = toml_config else {
807            return self;
808        };
809
810        match toml_config {
811            ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
812                self.add_v2_config(values)
813            }
814            ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
815        }
816    }
817
818    fn add_legacy_config(mut self, config: ServerConfig) -> Self {
819        if config.domain.is_some() {
820            self.domain = config.domain;
821        }
822
823        if config.origin.is_some() {
824            self.origin = config.origin;
825        }
826
827        if config.db_path.is_some() {
828            self.db_path = config.db_path;
829        }
830
831        if config.db_fs_type.is_some() {
832            self.db_fs_type = config.db_fs_type;
833        }
834
835        if config.tls_key.is_some() {
836            self.tls_key = config.tls_key;
837        }
838
839        if config.tls_chain.is_some() {
840            self.tls_chain = config.tls_chain;
841        }
842
843        if config.tls_client_ca.is_some() {
844            self.tls_client_ca = config.tls_client_ca;
845        }
846
847        if config.bindaddress.is_some() {
848            self.bindaddress = config.bindaddress.map(|a| vec![a]);
849        }
850
851        if config.ldapbindaddress.is_some() {
852            self.ldapbindaddress = config.ldapbindaddress.map(|a| vec![a]);
853        }
854
855        if let Some(adminbindpath) = config.adminbindpath {
856            self.adminbindpath = adminbindpath;
857        }
858
859        if config.role.is_some() {
860            self.role = config.role;
861        }
862
863        if config.log_level.is_some() {
864            self.log_level = config.log_level;
865        }
866
867        if let Some(threads) = config.thread_count {
868            self.threads = threads;
869        }
870
871        if let Some(maximum) = config.maximum_request_size_bytes {
872            self.maximum_request = maximum;
873        }
874
875        if config.db_arc_size.is_some() {
876            self.db_arc_size = config.db_arc_size;
877        }
878
879        if config.trust_x_forward_for == Some(true) {
880            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
881        }
882
883        if config.online_backup.is_some() {
884            self.online_backup = config.online_backup;
885        }
886
887        if config.repl_config.is_some() {
888            self.repl_config = config.repl_config;
889        }
890
891        if config.otel_grpc_url.is_some() {
892            self.otel_grpc_url = config.otel_grpc_url;
893        }
894
895        self
896    }
897
898    fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
899        if config.domain.is_some() {
900            self.domain = config.domain;
901        }
902
903        if config.origin.is_some() {
904            self.origin = config.origin;
905        }
906
907        if config.db_path.is_some() {
908            self.db_path = config.db_path;
909        }
910
911        if config.migration_path.is_some() {
912            self.migration_path = config.migration_path;
913        }
914
915        if config.db_fs_type.is_some() {
916            self.db_fs_type = config.db_fs_type;
917        }
918
919        if config.tls_key.is_some() {
920            self.tls_key = config.tls_key;
921        }
922
923        if config.tls_chain.is_some() {
924            self.tls_chain = config.tls_chain;
925        }
926
927        if config.tls_client_ca.is_some() {
928            self.tls_client_ca = config.tls_client_ca;
929        }
930
931        if config.bindaddress.is_some() {
932            self.bindaddress = config.bindaddress;
933        }
934
935        if config.ldapbindaddress.is_some() {
936            self.ldapbindaddress = config.ldapbindaddress;
937        }
938
939        if let Some(adminbindpath) = config.adminbindpath {
940            self.adminbindpath = adminbindpath;
941        }
942
943        if config.role.is_some() {
944            self.role = config.role;
945        }
946
947        if config.log_level.is_some() {
948            self.log_level = config.log_level;
949        }
950
951        if let Some(threads) = config.thread_count {
952            self.threads = threads;
953        }
954
955        if let Some(maximum) = config.maximum_request_size_bytes {
956            self.maximum_request = maximum;
957        }
958
959        if config.db_arc_size.is_some() {
960            self.db_arc_size = config.db_arc_size;
961        }
962
963        if let Some(http_client_address_info) = config.http_client_address_info {
964            self.http_client_address_info = http_client_address_info
965        }
966
967        if let Some(ldap_client_address_info) = config.ldap_client_address_info {
968            self.ldap_client_address_info = ldap_client_address_info
969        }
970
971        if config.online_backup.is_some() {
972            self.online_backup = config.online_backup;
973        }
974
975        if config.repl_config.is_some() {
976            self.repl_config = config.repl_config;
977        }
978
979        if config.otel_grpc_url.is_some() {
980            self.otel_grpc_url = config.otel_grpc_url;
981        }
982
983        self
984    }
985
986    // We always set threads to 1 unless it's the main server.
987    pub fn is_server_mode(mut self, is_server: bool) -> Self {
988        if !is_server {
989            self.threads = 1;
990        }
991        self
992    }
993
994    pub fn finish(self) -> Option<Configuration> {
995        let ConfigurationBuilder {
996            bindaddress,
997            ldapbindaddress,
998            adminbindpath,
999            threads,
1000            db_path,
1001            db_fs_type,
1002            db_arc_size,
1003            migration_path,
1004            maximum_request,
1005            http_client_address_info,
1006            ldap_client_address_info,
1007            tls_key,
1008            tls_chain,
1009            tls_client_ca,
1010            mut online_backup,
1011            domain,
1012            origin,
1013            role,
1014            log_level,
1015            repl_config,
1016            otel_grpc_url,
1017        } = self;
1018
1019        let tls_config = match (tls_key, tls_chain, tls_client_ca) {
1020            (Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
1021                chain,
1022                key,
1023                client_ca,
1024            }),
1025            _ => {
1026                eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
1027                return None;
1028            }
1029        };
1030
1031        let domain = domain.or_else(|| {
1032            eprintln!("ERROR: domain was not set.");
1033            None
1034        })?;
1035
1036        let origin = origin.or_else(|| {
1037            eprintln!("ERROR: origin was not set.");
1038            None
1039        })?;
1040
1041        if let Some(online_backup_ref) = online_backup.as_mut() {
1042            if online_backup_ref.path.is_none() {
1043                if let Some(db_path) = db_path.as_ref() {
1044                    if let Some(db_parent_path) = db_path.parent() {
1045                        online_backup_ref.path = Some(db_parent_path.to_path_buf());
1046                    } else {
1047                        eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
1048                        return None;
1049                    }
1050                } else {
1051                    eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
1052                    return None;
1053                }
1054            }
1055        };
1056
1057        // Apply any defaults if needed
1058        let address = bindaddress.unwrap_or(vec![DEFAULT_SERVER_ADDRESS.to_string()]);
1059        let role = role.unwrap_or(ServerRole::WriteReplica);
1060        let log_level = log_level.unwrap_or_default();
1061
1062        Some(Configuration {
1063            address,
1064            ldapbindaddress,
1065            adminbindpath,
1066            threads,
1067            db_path,
1068            db_fs_type,
1069            db_arc_size,
1070            migration_path,
1071            maximum_request,
1072            http_client_address_info,
1073            ldap_client_address_info,
1074            tls_config,
1075            online_backup,
1076            domain,
1077            origin,
1078            role,
1079            log_level,
1080            repl_config,
1081            otel_grpc_url,
1082            integration_repl_config: None,
1083            integration_test_config: None,
1084        })
1085    }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
1091    use std::net::{Ipv4Addr, Ipv6Addr};
1092
1093    #[test]
1094    fn assert_cidr_parsing_behaviour() {
1095        // Assert that we can parse individual hosts, and ranges
1096        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.1\"").unwrap();
1097        let expect_ip_cidr = IpCidr::from(Ipv4Addr::new(127, 0, 0, 1));
1098        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1099
1100        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.0/8\"").unwrap();
1101        let expect_ip_cidr = IpCidr::from(Ipv4Cidr::new(Ipv4Addr::new(127, 0, 0, 0), 8).unwrap());
1102        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1103
1104        // Same for ipv6
1105        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::1\"").unwrap();
1106        let expect_ip_cidr = IpCidr::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0001));
1107        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1108
1109        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::/64\"").unwrap();
1110        let expect_ip_cidr = IpCidr::from(
1111            Ipv6Cidr::new(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 64).unwrap(),
1112        );
1113        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1114    }
1115}