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 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        eprintln!("📜 Using config file: {:?}", config_path.as_ref());
327        let mut f: File = File::open(config_path.as_ref()).inspect_err(|e| {
328            eprintln!("Unable to open config file [{e:?}] 🥺");
329            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
330            eprintln!("{diag}");
331        })?;
332
333        let mut contents = String::new();
334
335        f.read_to_string(&mut contents).inspect_err(|e| {
336            eprintln!("unable to read contents {e:?}");
337            let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
338            eprintln!("{diag}");
339        })?;
340
341        // First, can we detect the config version?
342        let config_version = toml::from_str::<VersionDetection>(contents.as_str())
343            .map(|vd| vd.version)
344            .map_err(|err| {
345                eprintln!(
346                    "Unable to parse config version from '{:?}': {:?}",
347                    config_path.as_ref(),
348                    err
349                );
350                std::io::Error::new(std::io::ErrorKind::InvalidData, err)
351            })?;
352
353        match config_version {
354            Version::V2 => toml::from_str::<ServerConfigV2>(contents.as_str())
355                .map(|values| ServerConfigUntagged::Version(ServerConfigVersion::V2 { values })),
356            Version::Legacy => {
357                toml::from_str::<ServerConfig>(contents.as_str()).map(ServerConfigUntagged::Legacy)
358            }
359        }
360        .map_err(|err| {
361            eprintln!(
362                "Unable to parse config from '{:?}': {:?}",
363                config_path.as_ref(),
364                err
365            );
366            std::io::Error::new(std::io::ErrorKind::InvalidData, err)
367        })
368    }
369}
370
371#[serde_as]
372#[derive(Debug, Deserialize, Default)]
373#[serde(deny_unknown_fields)]
374pub struct ServerConfigV2 {
375    #[allow(dead_code)]
376    version: String,
377    domain: Option<String>,
378    origin: Option<Url>,
379    db_path: Option<PathBuf>,
380    db_fs_type: Option<kanidm_proto::internal::FsType>,
381    tls_chain: Option<PathBuf>,
382    tls_key: Option<PathBuf>,
383    tls_client_ca: Option<PathBuf>,
384
385    #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
386    bindaddress: Option<Vec<String>>,
387    #[serde_as(as = "Option<OneOrMany<_, PreferOne>>")]
388    ldapbindaddress: Option<Vec<String>>,
389
390    role: Option<ServerRole>,
391    log_level: Option<LogLevel>,
392    online_backup: Option<OnlineBackup>,
393
394    http_client_address_info: Option<HttpAddressInfo>,
395    ldap_client_address_info: Option<LdapAddressInfo>,
396
397    adminbindpath: Option<String>,
398    thread_count: Option<usize>,
399    maximum_request_size_bytes: Option<usize>,
400    #[allow(dead_code)]
401    db_arc_size: Option<usize>,
402    #[serde(default)]
403    #[serde(rename = "replication")]
404    repl_config: Option<ReplicationConfiguration>,
405    otel_grpc_url: Option<String>,
406}
407
408#[derive(Default)]
409pub struct CliConfig {
410    pub output_mode: Option<ConsoleOutputMode>,
411}
412
413#[derive(Default)]
414pub struct EnvironmentConfig {
415    domain: Option<String>,
416    origin: Option<Url>,
417    db_path: Option<PathBuf>,
418    tls_chain: Option<PathBuf>,
419    tls_key: Option<PathBuf>,
420    tls_client_ca: Option<PathBuf>,
421    bindaddress: Option<String>,
422    ldapbindaddress: Option<String>,
423    role: Option<ServerRole>,
424    log_level: Option<LogLevel>,
425    online_backup: Option<OnlineBackup>,
426    trust_x_forward_for: Option<bool>,
427    db_fs_type: Option<kanidm_proto::internal::FsType>,
428    adminbindpath: Option<String>,
429    db_arc_size: Option<usize>,
430    repl_config: Option<ReplicationConfiguration>,
431    otel_grpc_url: Option<String>,
432}
433
434impl EnvironmentConfig {
435    /// Updates the ServerConfig from environment variables starting with `KANIDM_`
436    pub fn new() -> Result<Self, String> {
437        let mut env_config = Self::default();
438
439        for (key, value) in std::env::vars() {
440            let Some(key) = key.strip_prefix("KANIDM_") else {
441                continue;
442            };
443
444            let ignorable_build_fields = [
445                "CPU_FLAGS",
446                "DEFAULT_CONFIG_PATH",
447                "DEFAULT_UNIX_SHELL_PATH",
448                "HTMX_UI_PKG_PATH",
449                "PKG_VERSION",
450                "PKG_VERSION_HASH",
451                "PRE_RELEASE",
452                "PROFILE_NAME",
453            ];
454
455            if ignorable_build_fields.contains(&key) {
456                #[cfg(any(debug_assertions, test))]
457                eprintln!("-- Ignoring build-time env var KANIDM_{key}");
458                continue;
459            }
460
461            match key {
462                "DOMAIN" => {
463                    env_config.domain = Some(value.to_string());
464                }
465                "ORIGIN" => {
466                    let url = Url::parse(value.as_str())
467                        .map_err(|err| format!("Failed to parse KANIDM_ORIGIN as URL: {err}"))?;
468                    env_config.origin = Some(url);
469                }
470                "DB_PATH" => {
471                    env_config.db_path = Some(PathBuf::from(value.to_string()));
472                }
473                "TLS_CHAIN" => {
474                    env_config.tls_chain = Some(PathBuf::from(value.to_string()));
475                }
476                "TLS_KEY" => {
477                    env_config.tls_key = Some(PathBuf::from(value.to_string()));
478                }
479                "TLS_CLIENT_CA" => {
480                    env_config.tls_client_ca = Some(PathBuf::from(value.to_string()));
481                }
482                "BINDADDRESS" => {
483                    env_config.bindaddress = Some(value.to_string());
484                }
485                "LDAPBINDADDRESS" => {
486                    env_config.ldapbindaddress = Some(value.to_string());
487                }
488                "ROLE" => {
489                    env_config.role = Some(ServerRole::from_str(&value).map_err(|err| {
490                        format!("Failed to parse KANIDM_ROLE as ServerRole: {err}")
491                    })?);
492                }
493                "LOG_LEVEL" => {
494                    env_config.log_level = LogLevel::from_str(&value)
495                        .map_err(|err| {
496                            format!("Failed to parse KANIDM_LOG_LEVEL as LogLevel: {err}")
497                        })
498                        .ok();
499                }
500                "ONLINE_BACKUP_PATH" => {
501                    if let Some(backup) = &mut env_config.online_backup {
502                        backup.path = Some(PathBuf::from(value.to_string()));
503                    } else {
504                        env_config.online_backup = Some(OnlineBackup {
505                            path: Some(PathBuf::from(value.to_string())),
506                            ..Default::default()
507                        });
508                    }
509                }
510                "ONLINE_BACKUP_SCHEDULE" => {
511                    if let Some(backup) = &mut env_config.online_backup {
512                        backup.schedule = value.to_string();
513                    } else {
514                        env_config.online_backup = Some(OnlineBackup {
515                            schedule: value.to_string(),
516                            ..Default::default()
517                        });
518                    }
519                }
520                "ONLINE_BACKUP_VERSIONS" => {
521                    let versions = value.parse().map_err(|_| {
522                        "Failed to parse KANIDM_ONLINE_BACKUP_VERSIONS as usize".to_string()
523                    })?;
524                    if let Some(backup) = &mut env_config.online_backup {
525                        backup.versions = versions;
526                    } else {
527                        env_config.online_backup = Some(OnlineBackup {
528                            versions,
529                            ..Default::default()
530                        })
531                    }
532                }
533                "TRUST_X_FORWARD_FOR" => {
534                    env_config.trust_x_forward_for = value
535                        .parse()
536                        .map_err(|_| {
537                            "Failed to parse KANIDM_TRUST_X_FORWARD_FOR as bool".to_string()
538                        })
539                        .ok();
540                }
541                "DB_FS_TYPE" => {
542                    env_config.db_fs_type = FsType::try_from(value.as_str())
543                        .map_err(|_| {
544                            "Failed to parse KANIDM_DB_FS_TYPE env var to valid value!".to_string()
545                        })
546                        .ok();
547                }
548                "DB_ARC_SIZE" => {
549                    env_config.db_arc_size = value
550                        .parse()
551                        .map_err(|_| "Failed to parse KANIDM_DB_ARC_SIZE as value".to_string())
552                        .ok();
553                }
554                "ADMIN_BIND_PATH" => {
555                    env_config.adminbindpath = Some(value.to_string());
556                }
557                "REPLICATION_ORIGIN" => {
558                    let repl_origin = Url::parse(value.as_str()).map_err(|err| {
559                        format!("Failed to parse KANIDM_REPLICATION_ORIGIN as URL: {err}")
560                    })?;
561                    if let Some(repl) = &mut env_config.repl_config {
562                        repl.origin = repl_origin
563                    } else {
564                        env_config.repl_config = Some(ReplicationConfiguration {
565                            origin: repl_origin,
566                            ..Default::default()
567                        });
568                    }
569                }
570                "REPLICATION_BINDADDRESS" => {
571                    let repl_bind_address = value
572                        .parse()
573                        .map_err(|_| "Failed to parse replication bind address".to_string())?;
574                    if let Some(repl) = &mut env_config.repl_config {
575                        repl.bindaddress = repl_bind_address;
576                    } else {
577                        env_config.repl_config = Some(ReplicationConfiguration {
578                            bindaddress: repl_bind_address,
579                            ..Default::default()
580                        });
581                    }
582                }
583                "REPLICATION_TASK_POLL_INTERVAL" => {
584                    let poll_interval = value
585                        .parse()
586                        .map_err(|_| {
587                            "Failed to parse replication task poll interval as u64".to_string()
588                        })
589                        .ok();
590                    if let Some(repl) = &mut env_config.repl_config {
591                        repl.task_poll_interval = poll_interval;
592                    } else {
593                        env_config.repl_config = Some(ReplicationConfiguration {
594                            task_poll_interval: poll_interval,
595                            ..Default::default()
596                        });
597                    }
598                }
599                "OTEL_GRPC_URL" => {
600                    env_config.otel_grpc_url = Some(value.to_string());
601                }
602
603                _ => eprintln!("Ignoring env var KANIDM_{key}"),
604            }
605        }
606
607        Ok(env_config)
608    }
609}
610
611#[derive(Debug, Deserialize, Clone, Copy, Default, Eq, PartialEq)]
612pub enum ServerRole {
613    #[default]
614    WriteReplica,
615    WriteReplicaNoUI,
616    ReadOnlyReplica,
617}
618
619impl Display for ServerRole {
620    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
621        match self {
622            ServerRole::WriteReplica => f.write_str("write replica"),
623            ServerRole::WriteReplicaNoUI => f.write_str("write replica (no ui)"),
624            ServerRole::ReadOnlyReplica => f.write_str("read only replica"),
625        }
626    }
627}
628
629impl FromStr for ServerRole {
630    type Err = &'static str;
631
632    fn from_str(s: &str) -> Result<Self, Self::Err> {
633        match s {
634            "write_replica" => Ok(ServerRole::WriteReplica),
635            "write_replica_no_ui" => Ok(ServerRole::WriteReplicaNoUI),
636            "read_only_replica" => Ok(ServerRole::ReadOnlyReplica),
637            _ => Err("Must be one of write_replica, write_replica_no_ui, read_only_replica"),
638        }
639    }
640}
641
642#[derive(Debug, Clone)]
643pub struct IntegrationTestConfig {
644    pub admin_user: String,
645    pub admin_password: String,
646    pub idm_admin_user: String,
647    pub idm_admin_password: String,
648}
649
650#[derive(Debug, Clone)]
651pub struct IntegrationReplConfig {
652    // We can bake in a private key for mTLS here.
653    // pub private_key: PKey
654
655    // We might need some condition variables / timers to force replication
656    // events? Or a channel to submit with oneshot responses.
657}
658
659/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
660#[derive(Debug, Clone)]
661pub struct Configuration {
662    pub address: Vec<String>,
663    pub ldapbindaddress: Option<Vec<String>>,
664    pub adminbindpath: String,
665    pub threads: usize,
666    // db type later
667    pub db_path: Option<PathBuf>,
668    pub db_fs_type: Option<FsType>,
669    pub db_arc_size: Option<usize>,
670    pub maximum_request: usize,
671
672    pub http_client_address_info: HttpAddressInfo,
673    pub ldap_client_address_info: LdapAddressInfo,
674
675    pub tls_config: Option<TlsConfiguration>,
676    pub integration_test_config: Option<Box<IntegrationTestConfig>>,
677    pub online_backup: Option<OnlineBackup>,
678    pub domain: String,
679    pub origin: Url,
680    pub role: ServerRole,
681    pub output_mode: ConsoleOutputMode,
682    pub log_level: LogLevel,
683    /// Replication settings.
684    pub repl_config: Option<ReplicationConfiguration>,
685    /// This allows internally setting some unsafe options for replication.
686    pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
687    pub otel_grpc_url: Option<String>,
688}
689
690impl Configuration {
691    pub fn build() -> ConfigurationBuilder {
692        ConfigurationBuilder {
693            bindaddress: None,
694            ldapbindaddress: None,
695            adminbindpath: None,
696            threads: std::thread::available_parallelism()
697                .map(|t| t.get())
698                .unwrap_or_else(|_e| {
699                    eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
700                    4
701                }),
702            db_path: None,
703            db_fs_type: None,
704            db_arc_size: None,
705            maximum_request: 256 * 1024, // 256k
706            http_client_address_info: HttpAddressInfo::default(),
707            ldap_client_address_info: LdapAddressInfo::default(),
708            tls_key: None,
709            tls_chain: None,
710            tls_client_ca: None,
711            online_backup: None,
712            domain: None,
713            origin: None,
714            output_mode: None,
715            log_level: None,
716            role: None,
717            repl_config: None,
718            otel_grpc_url: None,
719        }
720    }
721
722    pub fn new_for_test() -> Self {
723        #[allow(clippy::expect_used)]
724        Configuration {
725            address: vec![DEFAULT_SERVER_ADDRESS.to_string()],
726            ldapbindaddress: None,
727            adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
728            threads: 1,
729            db_path: None,
730            db_fs_type: None,
731            db_arc_size: None,
732            maximum_request: 256 * 1024, // 256k
733            http_client_address_info: HttpAddressInfo::default(),
734            ldap_client_address_info: LdapAddressInfo::default(),
735            tls_config: None,
736            integration_test_config: None,
737            online_backup: None,
738            domain: "idm.example.com".to_string(),
739            origin: Url::from_str("https://idm.example.com")
740                .expect("Failed to parse built-in string as URL"),
741            output_mode: ConsoleOutputMode::default(),
742            log_level: LogLevel::default(),
743            role: ServerRole::WriteReplica,
744            repl_config: None,
745            integration_repl_config: None,
746            otel_grpc_url: None,
747        }
748    }
749}
750
751impl fmt::Display for Configuration {
752    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753        for a in &self.address {
754            write!(f, "address: {a}, ")?;
755        }
756        write!(f, "domain: {}, ", self.domain)?;
757        match &self.ldapbindaddress {
758            Some(las) => {
759                for la in las {
760                    write!(f, "ldap address: {la}, ")?;
761                }
762            }
763            None => write!(f, "ldap address: disabled, ")?,
764        };
765        write!(f, "origin: {} ", self.origin)?;
766        write!(f, "admin bind path: {}, ", self.adminbindpath)?;
767        write!(f, "thread count: {}, ", self.threads)?;
768        write!(
769            f,
770            "dbpath: {}, ",
771            self.db_path
772                .as_ref()
773                .map(|p| p.to_string_lossy().to_string())
774                .unwrap_or("MEMORY".to_string())
775        )?;
776        match self.db_arc_size {
777            Some(v) => write!(f, "arcsize: {v}, "),
778            None => write!(f, "arcsize: AUTO, "),
779        }?;
780        write!(f, "max request size: {}b, ", self.maximum_request)?;
781        write!(
782            f,
783            "http client address info: {}, ",
784            self.http_client_address_info
785        )?;
786        write!(
787            f,
788            "ldap client address info: {}, ",
789            self.ldap_client_address_info
790        )?;
791
792        write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
793        match &self.online_backup {
794            Some(bck) => write!(
795                f,
796                "online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
797                bck.enabled,
798                bck.schedule,
799                bck.versions,
800                bck.path
801                    .as_ref()
802                    .map(|p| p.to_string_lossy().to_string())
803                    .unwrap_or("<unset>".to_string())
804            ),
805            None => write!(f, "online_backup: disabled, "),
806        }?;
807        write!(
808            f,
809            "integration mode: {}, ",
810            self.integration_test_config.is_some()
811        )?;
812        write!(f, "console output format: {:?} ", self.output_mode)?;
813        write!(f, "log_level: {}", self.log_level)?;
814        write!(f, "role: {}, ", self.role)?;
815        match &self.repl_config {
816            Some(repl) => {
817                write!(f, "replication: enabled")?;
818                write!(f, "repl_origin: {} ", repl.origin)?;
819                write!(f, "repl_address: {} ", repl.bindaddress)?;
820                write!(
821                    f,
822                    "integration repl config mode: {}, ",
823                    self.integration_repl_config.is_some()
824                )?;
825            }
826            None => {
827                write!(f, "replication: disabled, ")?;
828            }
829        }
830        write!(f, "otel_grpc_url: {:?}", self.otel_grpc_url)?;
831        Ok(())
832    }
833}
834
835/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
836#[derive(Debug, Clone)]
837pub struct ConfigurationBuilder {
838    bindaddress: Option<Vec<String>>,
839    ldapbindaddress: Option<Vec<String>>,
840    adminbindpath: Option<String>,
841    threads: usize,
842    db_path: Option<PathBuf>,
843    db_fs_type: Option<FsType>,
844    db_arc_size: Option<usize>,
845    maximum_request: usize,
846    http_client_address_info: HttpAddressInfo,
847    ldap_client_address_info: LdapAddressInfo,
848    tls_key: Option<PathBuf>,
849    tls_chain: Option<PathBuf>,
850    tls_client_ca: Option<PathBuf>,
851    online_backup: Option<OnlineBackup>,
852    domain: Option<String>,
853    origin: Option<Url>,
854    role: Option<ServerRole>,
855    output_mode: Option<ConsoleOutputMode>,
856    log_level: Option<LogLevel>,
857    repl_config: Option<ReplicationConfiguration>,
858    otel_grpc_url: Option<String>,
859}
860
861impl ConfigurationBuilder {
862    #![allow(clippy::needless_pass_by_value)]
863    pub fn add_cli_config(mut self, cli_config: CliConfig) -> Self {
864        if cli_config.output_mode.is_some() {
865            self.output_mode = cli_config.output_mode;
866        }
867
868        self
869    }
870
871    pub fn add_env_config(mut self, env_config: EnvironmentConfig) -> Self {
872        if env_config.bindaddress.is_some() {
873            self.bindaddress = env_config.bindaddress.map(|a| vec![a]);
874        }
875
876        if env_config.ldapbindaddress.is_some() {
877            self.ldapbindaddress = env_config.ldapbindaddress.map(|a| vec![a]);
878        }
879
880        if env_config.adminbindpath.is_some() {
881            self.adminbindpath = env_config.adminbindpath;
882        }
883
884        if env_config.db_path.is_some() {
885            self.db_path = env_config.db_path;
886        }
887
888        if env_config.db_fs_type.is_some() {
889            self.db_fs_type = env_config.db_fs_type;
890        }
891
892        if env_config.db_arc_size.is_some() {
893            self.db_arc_size = env_config.db_arc_size;
894        }
895
896        if env_config.trust_x_forward_for == Some(true) {
897            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
898        }
899
900        if env_config.tls_key.is_some() {
901            self.tls_key = env_config.tls_key;
902        }
903
904        if env_config.tls_chain.is_some() {
905            self.tls_chain = env_config.tls_chain;
906        }
907
908        if env_config.tls_client_ca.is_some() {
909            self.tls_client_ca = env_config.tls_client_ca;
910        }
911
912        if env_config.online_backup.is_some() {
913            self.online_backup = env_config.online_backup;
914        }
915
916        if env_config.domain.is_some() {
917            self.domain = env_config.domain;
918        }
919
920        if env_config.origin.is_some() {
921            self.origin = env_config.origin;
922        }
923
924        if env_config.role.is_some() {
925            self.role = env_config.role;
926        }
927
928        if env_config.log_level.is_some() {
929            self.log_level = env_config.log_level;
930        }
931
932        if env_config.repl_config.is_some() {
933            self.repl_config = env_config.repl_config;
934        }
935
936        if env_config.otel_grpc_url.is_some() {
937            self.otel_grpc_url = env_config.otel_grpc_url;
938        }
939
940        self
941    }
942
943    pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
944        // Can only proceed if the config is real
945        let Some(toml_config) = toml_config else {
946            return self;
947        };
948
949        match toml_config {
950            ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
951                self.add_v2_config(values)
952            }
953            ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
954        }
955    }
956
957    fn add_legacy_config(mut self, config: ServerConfig) -> Self {
958        if config.domain.is_some() {
959            self.domain = config.domain;
960        }
961
962        if config.origin.is_some() {
963            self.origin = config.origin;
964        }
965
966        if config.db_path.is_some() {
967            self.db_path = config.db_path;
968        }
969
970        if config.db_fs_type.is_some() {
971            self.db_fs_type = config.db_fs_type;
972        }
973
974        if config.tls_key.is_some() {
975            self.tls_key = config.tls_key;
976        }
977
978        if config.tls_chain.is_some() {
979            self.tls_chain = config.tls_chain;
980        }
981
982        if config.tls_client_ca.is_some() {
983            self.tls_client_ca = config.tls_client_ca;
984        }
985
986        if config.bindaddress.is_some() {
987            self.bindaddress = config.bindaddress.map(|a| vec![a]);
988        }
989
990        if config.ldapbindaddress.is_some() {
991            self.ldapbindaddress = config.ldapbindaddress.map(|a| vec![a]);
992        }
993
994        if config.adminbindpath.is_some() {
995            self.adminbindpath = config.adminbindpath;
996        }
997
998        if config.role.is_some() {
999            self.role = config.role;
1000        }
1001
1002        if config.log_level.is_some() {
1003            self.log_level = config.log_level;
1004        }
1005
1006        if let Some(threads) = config.thread_count {
1007            self.threads = threads;
1008        }
1009
1010        if let Some(maximum) = config.maximum_request_size_bytes {
1011            self.maximum_request = maximum;
1012        }
1013
1014        if config.db_arc_size.is_some() {
1015            self.db_arc_size = config.db_arc_size;
1016        }
1017
1018        if config.trust_x_forward_for == Some(true) {
1019            self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
1020        }
1021
1022        if config.online_backup.is_some() {
1023            self.online_backup = config.online_backup;
1024        }
1025
1026        if config.repl_config.is_some() {
1027            self.repl_config = config.repl_config;
1028        }
1029
1030        if config.otel_grpc_url.is_some() {
1031            self.otel_grpc_url = config.otel_grpc_url;
1032        }
1033
1034        self
1035    }
1036
1037    fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
1038        if config.domain.is_some() {
1039            self.domain = config.domain;
1040        }
1041
1042        if config.origin.is_some() {
1043            self.origin = config.origin;
1044        }
1045
1046        if config.db_path.is_some() {
1047            self.db_path = config.db_path;
1048        }
1049
1050        if config.db_fs_type.is_some() {
1051            self.db_fs_type = config.db_fs_type;
1052        }
1053
1054        if config.tls_key.is_some() {
1055            self.tls_key = config.tls_key;
1056        }
1057
1058        if config.tls_chain.is_some() {
1059            self.tls_chain = config.tls_chain;
1060        }
1061
1062        if config.tls_client_ca.is_some() {
1063            self.tls_client_ca = config.tls_client_ca;
1064        }
1065
1066        if config.bindaddress.is_some() {
1067            self.bindaddress = config.bindaddress;
1068        }
1069
1070        if config.ldapbindaddress.is_some() {
1071            self.ldapbindaddress = config.ldapbindaddress;
1072        }
1073
1074        if config.adminbindpath.is_some() {
1075            self.adminbindpath = config.adminbindpath;
1076        }
1077
1078        if config.role.is_some() {
1079            self.role = config.role;
1080        }
1081
1082        if config.log_level.is_some() {
1083            self.log_level = config.log_level;
1084        }
1085
1086        if let Some(threads) = config.thread_count {
1087            self.threads = threads;
1088        }
1089
1090        if let Some(maximum) = config.maximum_request_size_bytes {
1091            self.maximum_request = maximum;
1092        }
1093
1094        if config.db_arc_size.is_some() {
1095            self.db_arc_size = config.db_arc_size;
1096        }
1097
1098        if let Some(http_client_address_info) = config.http_client_address_info {
1099            self.http_client_address_info = http_client_address_info
1100        }
1101
1102        if let Some(ldap_client_address_info) = config.ldap_client_address_info {
1103            self.ldap_client_address_info = ldap_client_address_info
1104        }
1105
1106        if config.online_backup.is_some() {
1107            self.online_backup = config.online_backup;
1108        }
1109
1110        if config.repl_config.is_some() {
1111            self.repl_config = config.repl_config;
1112        }
1113
1114        if config.otel_grpc_url.is_some() {
1115            self.otel_grpc_url = config.otel_grpc_url;
1116        }
1117
1118        self
1119    }
1120
1121    // We always set threads to 1 unless it's the main server.
1122    pub fn is_server_mode(mut self, is_server: bool) -> Self {
1123        if is_server {
1124            self.threads = 1;
1125        }
1126        self
1127    }
1128
1129    pub fn finish(self) -> Option<Configuration> {
1130        let ConfigurationBuilder {
1131            bindaddress,
1132            ldapbindaddress,
1133            adminbindpath,
1134            threads,
1135            db_path,
1136            db_fs_type,
1137            db_arc_size,
1138            maximum_request,
1139            http_client_address_info,
1140            ldap_client_address_info,
1141            tls_key,
1142            tls_chain,
1143            tls_client_ca,
1144            mut online_backup,
1145            domain,
1146            origin,
1147            role,
1148            output_mode,
1149            log_level,
1150            repl_config,
1151            otel_grpc_url,
1152        } = self;
1153
1154        let tls_config = match (tls_key, tls_chain, tls_client_ca) {
1155            (Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
1156                chain,
1157                key,
1158                client_ca,
1159            }),
1160            _ => {
1161                eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
1162                return None;
1163            }
1164        };
1165
1166        let domain = domain.or_else(|| {
1167            eprintln!("ERROR: domain was not set.");
1168            None
1169        })?;
1170
1171        let origin = origin.or_else(|| {
1172            eprintln!("ERROR: origin was not set.");
1173            None
1174        })?;
1175
1176        if let Some(online_backup_ref) = online_backup.as_mut() {
1177            if online_backup_ref.path.is_none() {
1178                if let Some(db_path) = db_path.as_ref() {
1179                    if let Some(db_parent_path) = db_path.parent() {
1180                        online_backup_ref.path = Some(db_parent_path.to_path_buf());
1181                    } else {
1182                        eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
1183                        return None;
1184                    }
1185                } else {
1186                    eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
1187                    return None;
1188                }
1189            }
1190        };
1191
1192        // Apply any defaults if needed
1193        let adminbindpath =
1194            adminbindpath.unwrap_or(env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string());
1195        let address = bindaddress.unwrap_or(vec![DEFAULT_SERVER_ADDRESS.to_string()]);
1196        let output_mode = output_mode.unwrap_or_default();
1197        let role = role.unwrap_or(ServerRole::WriteReplica);
1198        let log_level = log_level.unwrap_or_default();
1199
1200        Some(Configuration {
1201            address,
1202            ldapbindaddress,
1203            adminbindpath,
1204            threads,
1205            db_path,
1206            db_fs_type,
1207            db_arc_size,
1208            maximum_request,
1209            http_client_address_info,
1210            ldap_client_address_info,
1211            tls_config,
1212            online_backup,
1213            domain,
1214            origin,
1215            role,
1216            output_mode,
1217            log_level,
1218            repl_config,
1219            otel_grpc_url,
1220            integration_repl_config: None,
1221            integration_test_config: None,
1222        })
1223    }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228    use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
1229    use std::net::{Ipv4Addr, Ipv6Addr};
1230
1231    #[test]
1232    fn assert_cidr_parsing_behaviour() {
1233        // Assert that we can parse individual hosts, and ranges
1234        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.1\"").unwrap();
1235        let expect_ip_cidr = IpCidr::from(Ipv4Addr::new(127, 0, 0, 1));
1236        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1237
1238        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"127.0.0.0/8\"").unwrap();
1239        let expect_ip_cidr = IpCidr::from(Ipv4Cidr::new(Ipv4Addr::new(127, 0, 0, 0), 8).unwrap());
1240        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1241
1242        // Same for ipv6
1243        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::1\"").unwrap();
1244        let expect_ip_cidr = IpCidr::from(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0001));
1245        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1246
1247        let parsed_ip_cidr: IpCidr = serde_json::from_str("\"2001:0db8::/64\"").unwrap();
1248        let expect_ip_cidr = IpCidr::from(
1249            Ipv6Cidr::new(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 64).unwrap(),
1250        );
1251        assert_eq!(parsed_ip_cidr, expect_ip_cidr);
1252    }
1253}