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 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)]
34// #[serde(tag = "version")]
35pub enum Version {
36    #[serde(rename = "2")]
37    V2,
38
39    #[default]
40    Legacy,
41}
42
43// Allowed as the large enum is only short lived at startup to the true config
44#[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    /// The destination folder for your backups, defaults to the db_path dir if not set
57    pub path: Option<PathBuf>,
58    /// The schedule to run online backups (see <https://crontab.guru/>), defaults to @daily
59    ///
60    /// Examples:
61    ///
62    /// - every day at 22:00 UTC (default): `"00 22 * * *"`
63    /// - every 6th hours (four times a day) at 3 minutes past the hour, :
64    ///   `"03 */6 * * *"`
65    ///
66    /// We also support non standard cron syntax, with the following format:
67    ///
68    /// `<sec>  <min>   <hour>   <day of month>   <month>   <day of week>   <year>`
69    ///
70    /// eg:
71    /// - `1 2 3 5 12 * 2023` would only back up once on the 5th of December 2023 at 03:02:01am.
72    /// - `3 2 1 * * Mon *` backs up every Monday at 03:02:01am.
73    ///
74    /// (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)
75    pub schedule: String,
76    #[serde(default = "default_online_backup_versions")]
77    /// How many past backup versions to keep, defaults to 7
78    pub versions: usize,
79    /// Enabled by default
80    #[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, // This makes it revert to the kanidm_db path
91            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    // IMPORTANT: This is undocumented, and only exists for backwards compat
191    // with config v1 which has a boolean toggle for this option.
192    #[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/// This is the Server Configuration as read from `server.toml` or environment variables.
254///
255/// Fields noted as "REQUIRED" are required for the server to start, even if they show as optional due to how file parsing works.
256///
257/// 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.
258///
259/// NOTE: not all flags or values from the internal [Configuration] object are exposed via this structure
260/// to prevent certain settings being set (e.g. integration test modes)
261#[derive(Debug, Deserialize, Default)]
262#[serde(deny_unknown_fields)]
263pub struct ServerConfig {
264    /// *REQUIRED* - Kanidm Domain, eg `kanidm.example.com`.
265    domain: Option<String>,
266    /// *REQUIRED* - The user-facing HTTPS URL for this server, eg <https://idm.example.com>
267    origin: Option<Url>,
268    /// File path of the database file
269    db_path: Option<PathBuf>,
270    /// The filesystem type, either "zfs" or "generic". Defaults to "generic" if unset. I you change this, run a database vacuum.
271    db_fs_type: Option<kanidm_proto::internal::FsType>,
272
273    ///  *REQUIRED* - The file path to the TLS Certificate Chain
274    tls_chain: Option<PathBuf>,
275    ///  *REQUIRED* - The file path to the TLS Private Key
276    tls_key: Option<PathBuf>,
277
278    /// The directory path of the client ca and crl dir.
279    tls_client_ca: Option<PathBuf>,
280
281    /// The listener address for the HTTPS server.
282    ///
283    /// eg. `[::]:8443` or `127.0.0.1:8443`. Defaults to [kanidm_proto::constants::DEFAULT_SERVER_ADDRESS]
284    bindaddress: Option<String>,
285    /// The listener address for the LDAP server.
286    ///
287    /// eg. `[::]:3636` or `127.0.0.1:3636`.
288    ///
289    /// If unset, the LDAP server will be disabled.
290    ldapbindaddress: Option<String>,
291    /// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica]
292    role: Option<ServerRole>,
293    /// The log level, one of info, debug, trace. Defaults to "info" if not set.
294    log_level: Option<LogLevel>,
295
296    /// Backup Configuration, see [OnlineBackup] for details on sub-keys.
297    online_backup: Option<OnlineBackup>,
298
299    /// Trust the X-Forwarded-For header for client IP address. Defaults to false if unset.
300    trust_x_forward_for: Option<bool>,
301
302    /// 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.
303    adminbindpath: Option<String>,
304
305    /// The maximum amount of threads the server will use for the async worker pool. Defaults
306    /// to std::threads::available_parallelism.
307    thread_count: Option<usize>,
308
309    /// Maximum Request Size in bytes
310    maximum_request_size_bytes: Option<usize>,
311
312    /// Don't touch this unless you know what you're doing!
313    #[allow(dead_code)]
314    db_arc_size: Option<usize>,
315    #[serde(default)]
316    #[serde(rename = "replication")]
317    /// Replication configuration, this is a development feature and not yet ready for production use.
318    repl_config: Option<ReplicationConfiguration>,
319    /// An optional OpenTelemetry collector (GRPC) url to send trace and log data to, eg `http://localhost:4317`. If not set, disables the feature.
320    otel_grpc_url: Option<String>,
321}
322
323impl ServerConfigUntagged {
324    /// loads the configuration file from the path specified, then overlays fields from environment variables starting with `KANIDM_``
325    pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
326        // see if we can load it from the config file you asked for
327        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        // First, can we detect the config version?
343        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    // 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 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    /// Replication settings.
451    pub repl_config: Option<ReplicationConfiguration>,
452    /// This allows internally setting some unsafe options for replication.
453    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            // set by build profiles
463            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, // 256k
474            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, // 256k
501            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/// 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    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        // logging
633        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        // core domainy things
644        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        // networking
657
658        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        // replication
673
674        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        // tls
708
709        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        // filesystem things
722        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        // backup things
739        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        // XFF handling
773        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        // Can only proceed if the config is real
782        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    // We always set threads to 1 unless it's the main server.
959    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        // Apply any defaults if needed
1030        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        // Assert that we can parse individual hosts, and ranges
1068        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        // Same for ipv6
1077        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}