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