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    #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
385    bindaddress: Option<Vec<String>>,
386    #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
387    ldapbindaddress: Option<Vec<String>>,
388
389    role: Option<ServerRole>,
390    log_level: Option<LogLevel>,
391    online_backup: Option<OnlineBackup>,
392
393    http_client_address_info: Option<HttpAddressInfo>,
394    ldap_client_address_info: Option<LdapAddressInfo>,
395
396    adminbindpath: Option<String>,
397    thread_count: Option<usize>,
398    maximum_request_size_bytes: Option<usize>,
399    #[allow(dead_code)]
400    db_arc_size: Option<usize>,
401    #[serde(default)]
402    #[serde(rename = "replication")]
403    repl_config: Option<ReplicationConfiguration>,
404    otel_grpc_url: Option<String>,
405}
406
407#[derive(Debug, Clone)]
408pub struct IntegrationTestConfig {
409    pub admin_user: String,
410    pub admin_password: String,
411    pub idm_admin_user: String,
412    pub idm_admin_password: String,
413}
414
415#[derive(Debug, Clone)]
416pub struct IntegrationReplConfig {
417    // We can bake in a private key for mTLS here.
418    // pub private_key: PKey
419
420    // We might need some condition variables / timers to force replication
421    // events? Or a channel to submit with oneshot responses.
422}
423
424/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
425#[derive(Debug, Clone)]
426pub struct Configuration {
427    pub address: Vec<String>,
428    pub ldapbindaddress: Option<Vec<String>>,
429    pub adminbindpath: String,
430    pub threads: usize,
431    // db type later
432    pub db_path: Option<PathBuf>,
433    pub db_fs_type: Option<FsType>,
434    pub db_arc_size: Option<usize>,
435    pub maximum_request: usize,
436
437    pub http_client_address_info: HttpAddressInfo,
438    pub ldap_client_address_info: LdapAddressInfo,
439
440    pub tls_config: Option<TlsConfiguration>,
441    pub integration_test_config: Option<Box<IntegrationTestConfig>>,
442    pub online_backup: Option<OnlineBackup>,
443    pub domain: String,
444    pub origin: Url,
445    pub role: ServerRole,
446    pub log_level: LogLevel,
447    /// Replication settings.
448    pub repl_config: Option<ReplicationConfiguration>,
449    /// This allows internally setting some unsafe options for replication.
450    pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
451    pub otel_grpc_url: Option<String>,
452}
453
454impl Configuration {
455    pub fn build() -> ConfigurationBuilder {
456        ConfigurationBuilder {
457            bindaddress: None,
458            ldapbindaddress: None,
459            // set by build profiles
460            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
461            threads: std::thread::available_parallelism()
462                .map(|t| t.get())
463                .unwrap_or_else(|_e| {
464                    eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
465                    4
466                }),
467            db_path: None,
468            db_fs_type: None,
469            db_arc_size: None,
470            maximum_request: 256 * 1024, // 256k
471            http_client_address_info: HttpAddressInfo::default(),
472            ldap_client_address_info: LdapAddressInfo::default(),
473            tls_key: None,
474            tls_chain: None,
475            tls_client_ca: None,
476            online_backup: None,
477            domain: None,
478            origin: None,
479            log_level: None,
480            role: None,
481            repl_config: None,
482            otel_grpc_url: None,
483        }
484    }
485
486    pub fn new_for_test() -> Self {
487        #[allow(clippy::expect_used)]
488        Configuration {
489            address: vec![DEFAULT_SERVER_ADDRESS.to_string()],
490            ldapbindaddress: None,
491            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
492            threads: 1,
493            db_path: None,
494            db_fs_type: None,
495            db_arc_size: None,
496            maximum_request: 256 * 1024, // 256k
497            http_client_address_info: HttpAddressInfo::default(),
498            ldap_client_address_info: LdapAddressInfo::default(),
499            tls_config: None,
500            integration_test_config: None,
501            online_backup: None,
502            domain: "idm.example.com".to_string(),
503            origin: Url::from_str("https://idm.example.com")
504                .expect("Failed to parse built-in string as URL"),
505            log_level: LogLevel::default(),
506            role: ServerRole::WriteReplica,
507            repl_config: None,
508            integration_repl_config: None,
509            otel_grpc_url: None,
510        }
511    }
512}
513
514impl fmt::Display for Configuration {
515    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516        for a in &self.address {
517            write!(f, "address: {a}, ")?;
518        }
519        write!(f, "domain: {}, ", self.domain)?;
520        match &self.ldapbindaddress {
521            Some(las) => {
522                for la in las {
523                    write!(f, "ldap address: {la}, ")?;
524                }
525            }
526            None => write!(f, "ldap address: disabled, ")?,
527        };
528        write!(f, "origin: {} ", self.origin)?;
529        write!(f, "admin bind path: {}, ", self.adminbindpath)?;
530        write!(f, "thread count: {}, ", self.threads)?;
531        write!(
532            f,
533            "dbpath: {}, ",
534            self.db_path
535                .as_ref()
536                .map(|p| p.to_string_lossy().to_string())
537                .unwrap_or("MEMORY".to_string())
538        )?;
539        match self.db_arc_size {
540            Some(v) => write!(f, "arcsize: {v}, "),
541            None => write!(f, "arcsize: AUTO, "),
542        }?;
543        write!(f, "max request size: {}b, ", self.maximum_request)?;
544        write!(
545            f,
546            "http client address info: {}, ",
547            self.http_client_address_info
548        )?;
549        write!(
550            f,
551            "ldap client address info: {}, ",
552            self.ldap_client_address_info
553        )?;
554
555        write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
556        match &self.online_backup {
557            Some(bck) => write!(
558                f,
559                "online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
560                bck.enabled,
561                bck.schedule,
562                bck.versions,
563                bck.path
564                    .as_ref()
565                    .map(|p| p.to_string_lossy().to_string())
566                    .unwrap_or("<unset>".to_string())
567            ),
568            None => write!(f, "online_backup: disabled, "),
569        }?;
570        write!(
571            f,
572            "integration mode: {}, ",
573            self.integration_test_config.is_some()
574        )?;
575        write!(f, "log_level: {}", self.log_level)?;
576        write!(f, "role: {}, ", self.role)?;
577        match &self.repl_config {
578            Some(repl) => {
579                write!(f, "replication: enabled")?;
580                write!(f, "repl_origin: {} ", repl.origin)?;
581                write!(f, "repl_address: {} ", repl.bindaddress)?;
582                write!(
583                    f,
584                    "integration repl config mode: {}, ",
585                    self.integration_repl_config.is_some()
586                )?;
587            }
588            None => {
589                write!(f, "replication: disabled, ")?;
590            }
591        }
592        write!(f, "otel_grpc_url: {:?}", self.otel_grpc_url)?;
593        Ok(())
594    }
595}
596
597/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
598#[derive(Debug, Clone)]
599pub struct ConfigurationBuilder {
600    bindaddress: Option<Vec<String>>,
601    ldapbindaddress: Option<Vec<String>>,
602    adminbindpath: String,
603    threads: usize,
604    db_path: Option<PathBuf>,
605    db_fs_type: Option<FsType>,
606    db_arc_size: Option<usize>,
607    maximum_request: usize,
608    http_client_address_info: HttpAddressInfo,
609    ldap_client_address_info: LdapAddressInfo,
610    tls_key: Option<PathBuf>,
611    tls_chain: Option<PathBuf>,
612    tls_client_ca: Option<PathBuf>,
613    online_backup: Option<OnlineBackup>,
614    domain: Option<String>,
615    origin: Option<Url>,
616    role: Option<ServerRole>,
617    log_level: Option<LogLevel>,
618    repl_config: Option<ReplicationConfiguration>,
619    otel_grpc_url: Option<String>,
620}
621
622impl ConfigurationBuilder {
623    #![allow(clippy::needless_pass_by_value)]
624    pub fn add_cli_config(mut self, cli_config: &kanidm_proto::cli::KanidmdCli) -> Self {
625        // logging
626        if let Some(log_level) = &cli_config.log_level {
627            self.log_level = Some(*log_level);
628        }
629
630        if let Some(otel_grpc_url) = &cli_config.otel_grpc_url {
631            self.otel_grpc_url = Some(otel_grpc_url.clone());
632        }
633
634        // core domainy things
635        if let Some(domain) = &cli_config.domain {
636            self.domain = Some(domain.clone());
637        }
638
639        if let Some(origin) = &cli_config.origin {
640            self.origin = Some(origin.clone());
641        }
642
643        if let Some(role) = &cli_config.role {
644            self.role = Some(*role);
645        }
646
647        // networking
648
649        if cli_config.bindaddress.is_some() {
650            self.bindaddress = cli_config
651                .bindaddress
652                .clone()
653                .map(|s| s.split(',').map(|s| s.to_string()).collect())
654        }
655
656        if cli_config.ldapbindaddress.is_some() {
657            self.ldapbindaddress = cli_config
658                .ldapbindaddress
659                .clone()
660                .map(|s| s.split(',').map(|s| s.to_string()).collect())
661        }
662
663        // replication
664
665        if let Some(repl_origin) = cli_config.replication_origin.clone() {
666            if let Some(repl) = &mut self.repl_config {
667                repl.origin = repl_origin
668            } else {
669                self.repl_config = Some(ReplicationConfiguration {
670                    origin: repl_origin,
671                    ..Default::default()
672                });
673            }
674        }
675
676        if let Some(replication_bindaddress) = &cli_config.replication_bindaddress {
677            if let Some(repl_config) = &mut self.repl_config {
678                repl_config.bindaddress = *replication_bindaddress;
679            } else {
680                self.repl_config = Some(ReplicationConfiguration {
681                    bindaddress: *replication_bindaddress,
682                    ..Default::default()
683                });
684            }
685        }
686
687        if let Some(task_poll_interval) = cli_config.replication_task_poll_interval {
688            if let Some(repl) = &mut self.repl_config {
689                repl.task_poll_interval = Some(task_poll_interval);
690            } else {
691                self.repl_config = Some(ReplicationConfiguration {
692                    task_poll_interval: Some(task_poll_interval),
693                    ..Default::default()
694                });
695            }
696        }
697
698        // tls
699
700        if let Some(tls_key) = &cli_config.tls_key {
701            self.tls_key = Some(tls_key.clone());
702        }
703
704        if let Some(tls_chain) = &cli_config.tls_chain {
705            self.tls_chain = Some(tls_chain.clone());
706        }
707
708        if let Some(tls_client_ca) = &cli_config.tls_client_ca {
709            self.tls_client_ca = Some(tls_client_ca.clone());
710        }
711
712        // filesystem things
713        if let Some(adminbindpath) = &cli_config.admin_bind_path {
714            self.adminbindpath = adminbindpath.clone();
715        }
716
717        if let Some(db_path) = &cli_config.db_path {
718            self.db_path = Some(db_path.clone());
719        }
720
721        if let Some(db_fs_type) = &cli_config.db_fs_type {
722            self.db_fs_type = Some(*db_fs_type);
723        }
724
725        if cli_config.db_arc_size.is_some() {
726            self.db_arc_size = cli_config.db_arc_size;
727        }
728
729        // backup things
730        if let Some(online_backup_path) = &cli_config.online_backup_path {
731            if let Some(backup) = &mut self.online_backup {
732                backup.path = Some(online_backup_path.clone());
733            } else {
734                self.online_backup = Some(OnlineBackup {
735                    path: Some(online_backup_path.clone()),
736                    ..Default::default()
737                });
738            }
739        }
740
741        if let Some(online_backup_schedule) = &cli_config.online_backup_schedule {
742            if let Some(backup) = &mut self.online_backup {
743                backup.schedule = online_backup_schedule.clone();
744            } else {
745                self.online_backup = Some(OnlineBackup {
746                    schedule: online_backup_schedule.clone(),
747                    ..Default::default()
748                });
749            }
750        }
751
752        if let Some(online_backup_versions) = &cli_config.online_backup_versions {
753            if let Some(backup) = &mut self.online_backup {
754                backup.versions = *online_backup_versions;
755            } else {
756                self.online_backup = Some(OnlineBackup {
757                    versions: *online_backup_versions,
758                    ..Default::default()
759                });
760            }
761        }
762
763        // XFF handling
764        if let Some(true) = cli_config.trust_all_x_forwarded_for {
765            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
766        }
767
768        self
769    }
770
771    pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
772        // Can only proceed if the config is real
773        let Some(toml_config) = toml_config else {
774            return self;
775        };
776
777        match toml_config {
778            ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
779                self.add_v2_config(values)
780            }
781            ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
782        }
783    }
784
785    fn add_legacy_config(mut self, config: ServerConfig) -> Self {
786        if config.domain.is_some() {
787            self.domain = config.domain;
788        }
789
790        if config.origin.is_some() {
791            self.origin = config.origin;
792        }
793
794        if config.db_path.is_some() {
795            self.db_path = config.db_path;
796        }
797
798        if config.db_fs_type.is_some() {
799            self.db_fs_type = config.db_fs_type;
800        }
801
802        if config.tls_key.is_some() {
803            self.tls_key = config.tls_key;
804        }
805
806        if config.tls_chain.is_some() {
807            self.tls_chain = config.tls_chain;
808        }
809
810        if config.tls_client_ca.is_some() {
811            self.tls_client_ca = config.tls_client_ca;
812        }
813
814        if config.bindaddress.is_some() {
815            self.bindaddress = config.bindaddress.map(|a| vec![a]);
816        }
817
818        if config.ldapbindaddress.is_some() {
819            self.ldapbindaddress = config.ldapbindaddress.map(|a| vec![a]);
820        }
821
822        if let Some(adminbindpath) = config.adminbindpath {
823            self.adminbindpath = adminbindpath;
824        }
825
826        if config.role.is_some() {
827            self.role = config.role;
828        }
829
830        if config.log_level.is_some() {
831            self.log_level = config.log_level;
832        }
833
834        if let Some(threads) = config.thread_count {
835            self.threads = threads;
836        }
837
838        if let Some(maximum) = config.maximum_request_size_bytes {
839            self.maximum_request = maximum;
840        }
841
842        if config.db_arc_size.is_some() {
843            self.db_arc_size = config.db_arc_size;
844        }
845
846        if config.trust_x_forward_for == Some(true) {
847            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
848        }
849
850        if config.online_backup.is_some() {
851            self.online_backup = config.online_backup;
852        }
853
854        if config.repl_config.is_some() {
855            self.repl_config = config.repl_config;
856        }
857
858        if config.otel_grpc_url.is_some() {
859            self.otel_grpc_url = config.otel_grpc_url;
860        }
861
862        self
863    }
864
865    fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
866        if config.domain.is_some() {
867            self.domain = config.domain;
868        }
869
870        if config.origin.is_some() {
871            self.origin = config.origin;
872        }
873
874        if config.db_path.is_some() {
875            self.db_path = config.db_path;
876        }
877
878        if config.db_fs_type.is_some() {
879            self.db_fs_type = config.db_fs_type;
880        }
881
882        if config.tls_key.is_some() {
883            self.tls_key = config.tls_key;
884        }
885
886        if config.tls_chain.is_some() {
887            self.tls_chain = config.tls_chain;
888        }
889
890        if config.tls_client_ca.is_some() {
891            self.tls_client_ca = config.tls_client_ca;
892        }
893
894        if config.bindaddress.is_some() {
895            self.bindaddress = config.bindaddress;
896        }
897
898        if config.ldapbindaddress.is_some() {
899            self.ldapbindaddress = config.ldapbindaddress;
900        }
901
902        if let Some(adminbindpath) = config.adminbindpath {
903            self.adminbindpath = adminbindpath;
904        }
905
906        if config.role.is_some() {
907            self.role = config.role;
908        }
909
910        if config.log_level.is_some() {
911            self.log_level = config.log_level;
912        }
913
914        if let Some(threads) = config.thread_count {
915            self.threads = threads;
916        }
917
918        if let Some(maximum) = config.maximum_request_size_bytes {
919            self.maximum_request = maximum;
920        }
921
922        if config.db_arc_size.is_some() {
923            self.db_arc_size = config.db_arc_size;
924        }
925
926        if let Some(http_client_address_info) = config.http_client_address_info {
927            self.http_client_address_info = http_client_address_info
928        }
929
930        if let Some(ldap_client_address_info) = config.ldap_client_address_info {
931            self.ldap_client_address_info = ldap_client_address_info
932        }
933
934        if config.online_backup.is_some() {
935            self.online_backup = config.online_backup;
936        }
937
938        if config.repl_config.is_some() {
939            self.repl_config = config.repl_config;
940        }
941
942        if config.otel_grpc_url.is_some() {
943            self.otel_grpc_url = config.otel_grpc_url;
944        }
945
946        self
947    }
948
949    // We always set threads to 1 unless it's the main server.
950    pub fn is_server_mode(mut self, is_server: bool) -> Self {
951        if is_server {
952            self.threads = 1;
953        }
954        self
955    }
956
957    pub fn finish(self) -> Option<Configuration> {
958        let ConfigurationBuilder {
959            bindaddress,
960            ldapbindaddress,
961            adminbindpath,
962            threads,
963            db_path,
964            db_fs_type,
965            db_arc_size,
966            maximum_request,
967            http_client_address_info,
968            ldap_client_address_info,
969            tls_key,
970            tls_chain,
971            tls_client_ca,
972            mut online_backup,
973            domain,
974            origin,
975            role,
976            log_level,
977            repl_config,
978            otel_grpc_url,
979        } = self;
980
981        let tls_config = match (tls_key, tls_chain, tls_client_ca) {
982            (Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
983                chain,
984                key,
985                client_ca,
986            }),
987            _ => {
988                eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
989                return None;
990            }
991        };
992
993        let domain = domain.or_else(|| {
994            eprintln!("ERROR: domain was not set.");
995            None
996        })?;
997
998        let origin = origin.or_else(|| {
999            eprintln!("ERROR: origin was not set.");
1000            None
1001        })?;
1002
1003        if let Some(online_backup_ref) = online_backup.as_mut() {
1004            if online_backup_ref.path.is_none() {
1005                if let Some(db_path) = db_path.as_ref() {
1006                    if let Some(db_parent_path) = db_path.parent() {
1007                        online_backup_ref.path = Some(db_parent_path.to_path_buf());
1008                    } else {
1009                        eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
1010                        return None;
1011                    }
1012                } else {
1013                    eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
1014                    return None;
1015                }
1016            }
1017        };
1018
1019        // Apply any defaults if needed
1020        let address = bindaddress.unwrap_or(vec![DEFAULT_SERVER_ADDRESS.to_string()]);
1021        let role = role.unwrap_or(ServerRole::WriteReplica);
1022        let log_level = log_level.unwrap_or_default();
1023
1024        Some(Configuration {
1025            address,
1026            ldapbindaddress,
1027            adminbindpath,
1028            threads,
1029            db_path,
1030            db_fs_type,
1031            db_arc_size,
1032            maximum_request,
1033            http_client_address_info,
1034            ldap_client_address_info,
1035            tls_config,
1036            online_backup,
1037            domain,
1038            origin,
1039            role,
1040            log_level,
1041            repl_config,
1042            otel_grpc_url,
1043            integration_repl_config: None,
1044            integration_test_config: None,
1045        })
1046    }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
1052    use std::net::{Ipv4Addr, Ipv6Addr};
1053
1054    #[test]
1055    fn assert_cidr_parsing_behaviour() {
1056        // Assert that we can parse individual hosts, and ranges
1057        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.1\"").unwrap();
1058        let expect_ip_cidr = IpCidr::from(Ipv4Addr::new(127, 0, 0, 1));
1059        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1060
1061        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.0/8\"").unwrap();
1062        let expect_ip_cidr = IpCidr::from(Ipv4Cidr::new(Ipv4Addr::new(127, 0, 0, 0), 8).unwrap());
1063        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1064
1065        // Same for ipv6
1066        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::1\"").unwrap();
1067        let expect_ip_cidr = IpCidr::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0001));
1068        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1069
1070        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::/64\"").unwrap();
1071        let expect_ip_cidr = IpCidr::from(
1072            Ipv6Cidr::new(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 64).unwrap(),
1073        );
1074        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1075    }
1076}