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