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