mirror of
https://github.com/kanidm/kanidm.git
synced 2025-04-20 17:25:38 +02:00
This allows our server configuration to be versioned, in preparation for a change related to the proxy protocol additions.
1018 lines
34 KiB
Rust
1018 lines
34 KiB
Rust
//! The server configuration as processed from the startup wrapper. This controls a number of
|
|
//! variables that determine how our backends, query server, and frontends are configured.
|
|
//!
|
|
//! These components should be "per server". Any "per domain" config should be in the system
|
|
//! or domain entries that are able to be replicated.
|
|
|
|
use std::fmt::{self, Display};
|
|
use std::fs::File;
|
|
use std::io::Read;
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
|
|
use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
|
|
use kanidm_proto::internal::FsType;
|
|
use kanidm_proto::messages::ConsoleOutputMode;
|
|
|
|
use serde::Deserialize;
|
|
use sketching::LogLevel;
|
|
use url::Url;
|
|
|
|
use crate::repl::config::ReplicationConfiguration;
|
|
|
|
// Allowed as the large enum is only short lived at startup to the true config
|
|
#[allow(clippy::large_enum_variant)]
|
|
// These structures allow us to move to version tagging of the configuration structure.
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum ServerConfigUntagged {
|
|
Version(ServerConfigVersion),
|
|
Legacy(ServerConfig),
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(tag = "version")]
|
|
pub enum ServerConfigVersion {
|
|
#[serde(rename = "2")]
|
|
V2 {
|
|
#[serde(flatten)]
|
|
values: ServerConfigV2,
|
|
},
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct OnlineBackup {
|
|
/// The destination folder for your backups, defaults to the db_path dir if not set
|
|
pub path: Option<PathBuf>,
|
|
/// The schedule to run online backups (see <https://crontab.guru/>), defaults to @daily
|
|
///
|
|
/// Examples:
|
|
///
|
|
/// - every day at 22:00 UTC (default): `"00 22 * * *"`
|
|
/// - every 6th hours (four times a day) at 3 minutes past the hour, :
|
|
/// `"03 */6 * * *"`
|
|
///
|
|
/// We also support non standard cron syntax, with the following format:
|
|
///
|
|
/// `<sec> <min> <hour> <day of month> <month> <day of week> <year>`
|
|
///
|
|
/// eg:
|
|
/// - `1 2 3 5 12 * 2023` would only back up once on the 5th of December 2023 at 03:02:01am.
|
|
/// - `3 2 1 * * Mon *` backs up every Monday at 03:02:01am.
|
|
///
|
|
/// (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)
|
|
pub schedule: String,
|
|
#[serde(default = "default_online_backup_versions")]
|
|
/// How many past backup versions to keep, defaults to 7
|
|
pub versions: usize,
|
|
/// Enabled by default
|
|
#[serde(default = "default_online_backup_enabled")]
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl Default for OnlineBackup {
|
|
fn default() -> Self {
|
|
OnlineBackup {
|
|
path: None, // This makes it revert to the kanidm_db path
|
|
schedule: default_online_backup_schedule(),
|
|
versions: default_online_backup_versions(),
|
|
enabled: default_online_backup_enabled(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_online_backup_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_online_backup_schedule() -> String {
|
|
"@daily".to_string()
|
|
}
|
|
|
|
fn default_online_backup_versions() -> usize {
|
|
7
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct TlsConfiguration {
|
|
pub chain: PathBuf,
|
|
pub key: PathBuf,
|
|
pub client_ca: Option<PathBuf>,
|
|
}
|
|
|
|
/// This is the Server Configuration as read from `server.toml` or environment variables.
|
|
///
|
|
/// Fields noted as "REQUIRED" are required for the server to start, even if they show as optional due to how file parsing works.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// NOTE: not all flags or values from the internal [Configuration] object are exposed via this structure
|
|
/// to prevent certain settings being set (e.g. integration test modes)
|
|
#[derive(Debug, Deserialize, Default)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ServerConfig {
|
|
/// *REQUIRED* - Kanidm Domain, eg `kanidm.example.com`.
|
|
domain: Option<String>,
|
|
/// *REQUIRED* - The user-facing HTTPS URL for this server, eg <https://idm.example.com>
|
|
// TODO -this should be URL
|
|
origin: Option<String>,
|
|
/// File path of the database file
|
|
db_path: Option<PathBuf>,
|
|
/// The filesystem type, either "zfs" or "generic". Defaults to "generic" if unset. I you change this, run a database vacuum.
|
|
db_fs_type: Option<kanidm_proto::internal::FsType>,
|
|
|
|
/// *REQUIRED* - The file path to the TLS Certificate Chain
|
|
tls_chain: Option<PathBuf>,
|
|
/// *REQUIRED* - The file path to the TLS Private Key
|
|
tls_key: Option<PathBuf>,
|
|
|
|
/// The directory path of the client ca and crl dir.
|
|
tls_client_ca: Option<PathBuf>,
|
|
|
|
/// The listener address for the HTTPS server.
|
|
///
|
|
/// eg. `[::]:8443` or `127.0.0.1:8443`. Defaults to [kanidm_proto::constants::DEFAULT_SERVER_ADDRESS]
|
|
bindaddress: Option<String>,
|
|
/// The listener address for the LDAP server.
|
|
///
|
|
/// eg. `[::]:3636` or `127.0.0.1:3636`.
|
|
///
|
|
/// If unset, the LDAP server will be disabled.
|
|
ldapbindaddress: Option<String>,
|
|
/// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica]
|
|
role: Option<ServerRole>,
|
|
/// The log level, one of info, debug, trace. Defaults to "info" if not set.
|
|
log_level: Option<LogLevel>,
|
|
|
|
/// Backup Configuration, see [OnlineBackup] for details on sub-keys.
|
|
online_backup: Option<OnlineBackup>,
|
|
|
|
/// Trust the X-Forwarded-For header for client IP address. Defaults to false if unset.
|
|
trust_x_forward_for: Option<bool>,
|
|
|
|
/// 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.
|
|
adminbindpath: Option<String>,
|
|
|
|
/// The maximum amount of threads the server will use for the async worker pool. Defaults
|
|
/// to std::threads::available_parallelism.
|
|
thread_count: Option<usize>,
|
|
|
|
/// Maximum Request Size in bytes
|
|
maximum_request_size_bytes: Option<usize>,
|
|
|
|
/// Don't touch this unless you know what you're doing!
|
|
#[allow(dead_code)]
|
|
db_arc_size: Option<usize>,
|
|
#[serde(default)]
|
|
#[serde(rename = "replication")]
|
|
/// Replication configuration, this is a development feature and not yet ready for production use.
|
|
repl_config: Option<ReplicationConfiguration>,
|
|
/// An optional OpenTelemetry collector (GRPC) url to send trace and log data to, eg `http://localhost:4317`. If not set, disables the feature.
|
|
otel_grpc_url: Option<String>,
|
|
}
|
|
|
|
impl ServerConfigUntagged {
|
|
/// loads the configuration file from the path specified, then overlays fields from environment variables starting with `KANIDM_``
|
|
pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
|
|
// see if we can load it from the config file you asked for
|
|
eprintln!("📜 Using config file: {:?}", config_path.as_ref());
|
|
let mut f: File = File::open(config_path.as_ref()).inspect_err(|e| {
|
|
eprintln!("Unable to open config file [{:?}] 🥺", e);
|
|
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
|
|
eprintln!("{}", diag);
|
|
})?;
|
|
|
|
let mut contents = String::new();
|
|
|
|
f.read_to_string(&mut contents).inspect_err(|e| {
|
|
eprintln!("unable to read contents {:?}", e);
|
|
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
|
|
eprintln!("{}", diag);
|
|
})?;
|
|
|
|
// if we *can* load the config we'll set config to that.
|
|
toml::from_str::<ServerConfigUntagged>(contents.as_str()).map_err(|err| {
|
|
eprintln!(
|
|
"Unable to parse config from '{:?}': {:?}",
|
|
config_path.as_ref(),
|
|
err
|
|
);
|
|
std::io::Error::new(std::io::ErrorKind::InvalidData, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Default)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ServerConfigV2 {
|
|
domain: Option<String>,
|
|
origin: Option<String>,
|
|
db_path: Option<PathBuf>,
|
|
db_fs_type: Option<kanidm_proto::internal::FsType>,
|
|
tls_chain: Option<PathBuf>,
|
|
tls_key: Option<PathBuf>,
|
|
tls_client_ca: Option<PathBuf>,
|
|
bindaddress: Option<String>,
|
|
ldapbindaddress: Option<String>,
|
|
role: Option<ServerRole>,
|
|
log_level: Option<LogLevel>,
|
|
online_backup: Option<OnlineBackup>,
|
|
trust_x_forward_for: Option<bool>,
|
|
adminbindpath: Option<String>,
|
|
thread_count: Option<usize>,
|
|
maximum_request_size_bytes: Option<usize>,
|
|
#[allow(dead_code)]
|
|
db_arc_size: Option<usize>,
|
|
#[serde(default)]
|
|
#[serde(rename = "replication")]
|
|
repl_config: Option<ReplicationConfiguration>,
|
|
otel_grpc_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct CliConfig {
|
|
pub output_mode: Option<ConsoleOutputMode>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct EnvironmentConfig {
|
|
domain: Option<String>,
|
|
origin: Option<String>,
|
|
db_path: Option<PathBuf>,
|
|
tls_chain: Option<PathBuf>,
|
|
tls_key: Option<PathBuf>,
|
|
tls_client_ca: Option<PathBuf>,
|
|
bindaddress: Option<String>,
|
|
ldapbindaddress: Option<String>,
|
|
role: Option<ServerRole>,
|
|
log_level: Option<LogLevel>,
|
|
online_backup: Option<OnlineBackup>,
|
|
trust_x_forward_for: Option<bool>,
|
|
db_fs_type: Option<kanidm_proto::internal::FsType>,
|
|
adminbindpath: Option<String>,
|
|
db_arc_size: Option<usize>,
|
|
repl_config: Option<ReplicationConfiguration>,
|
|
otel_grpc_url: Option<String>,
|
|
}
|
|
|
|
impl EnvironmentConfig {
|
|
/// Updates the ServerConfig from environment variables starting with `KANIDM_`
|
|
pub fn new() -> Result<Self, String> {
|
|
let mut env_config = Self::default();
|
|
|
|
for (key, value) in std::env::vars() {
|
|
let Some(key) = key.strip_prefix("KANIDM_") else {
|
|
continue;
|
|
};
|
|
|
|
let ignorable_build_fields = [
|
|
"CPU_FLAGS",
|
|
"DEFAULT_CONFIG_PATH",
|
|
"DEFAULT_UNIX_SHELL_PATH",
|
|
"HTMX_UI_PKG_PATH",
|
|
"PKG_VERSION",
|
|
"PKG_VERSION_HASH",
|
|
"PRE_RELEASE",
|
|
"PROFILE_NAME",
|
|
];
|
|
|
|
if ignorable_build_fields.contains(&key) {
|
|
#[cfg(any(debug_assertions, test))]
|
|
eprintln!("-- Ignoring build-time env var KANIDM_{key}");
|
|
continue;
|
|
}
|
|
|
|
match key {
|
|
"DOMAIN" => {
|
|
env_config.domain = Some(value.to_string());
|
|
}
|
|
"ORIGIN" => {
|
|
env_config.origin = Some(value.to_string());
|
|
}
|
|
"DB_PATH" => {
|
|
env_config.db_path = Some(PathBuf::from(value.to_string()));
|
|
}
|
|
"TLS_CHAIN" => {
|
|
env_config.tls_chain = Some(PathBuf::from(value.to_string()));
|
|
}
|
|
"TLS_KEY" => {
|
|
env_config.tls_key = Some(PathBuf::from(value.to_string()));
|
|
}
|
|
"TLS_CLIENT_CA" => {
|
|
env_config.tls_client_ca = Some(PathBuf::from(value.to_string()));
|
|
}
|
|
"BINDADDRESS" => {
|
|
env_config.bindaddress = Some(value.to_string());
|
|
}
|
|
"LDAPBINDADDRESS" => {
|
|
env_config.ldapbindaddress = Some(value.to_string());
|
|
}
|
|
"ROLE" => {
|
|
env_config.role = Some(ServerRole::from_str(&value).map_err(|err| {
|
|
format!("Failed to parse KANIDM_ROLE as ServerRole: {}", err)
|
|
})?);
|
|
}
|
|
"LOG_LEVEL" => {
|
|
env_config.log_level = LogLevel::from_str(&value)
|
|
.map_err(|err| {
|
|
format!("Failed to parse KANIDM_LOG_LEVEL as LogLevel: {}", err)
|
|
})
|
|
.ok();
|
|
}
|
|
"ONLINE_BACKUP_PATH" => {
|
|
if let Some(backup) = &mut env_config.online_backup {
|
|
backup.path = Some(PathBuf::from(value.to_string()));
|
|
} else {
|
|
env_config.online_backup = Some(OnlineBackup {
|
|
path: Some(PathBuf::from(value.to_string())),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
"ONLINE_BACKUP_SCHEDULE" => {
|
|
if let Some(backup) = &mut env_config.online_backup {
|
|
backup.schedule = value.to_string();
|
|
} else {
|
|
env_config.online_backup = Some(OnlineBackup {
|
|
schedule: value.to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
"ONLINE_BACKUP_VERSIONS" => {
|
|
let versions = value.parse().map_err(|_| {
|
|
"Failed to parse KANIDM_ONLINE_BACKUP_VERSIONS as usize".to_string()
|
|
})?;
|
|
if let Some(backup) = &mut env_config.online_backup {
|
|
backup.versions = versions;
|
|
} else {
|
|
env_config.online_backup = Some(OnlineBackup {
|
|
versions,
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
"TRUST_X_FORWARD_FOR" => {
|
|
env_config.trust_x_forward_for = value
|
|
.parse()
|
|
.map_err(|_| {
|
|
"Failed to parse KANIDM_TRUST_X_FORWARD_FOR as bool".to_string()
|
|
})
|
|
.ok();
|
|
}
|
|
"DB_FS_TYPE" => {
|
|
env_config.db_fs_type = FsType::try_from(value.as_str())
|
|
.map_err(|_| {
|
|
"Failed to parse KANIDM_DB_FS_TYPE env var to valid value!".to_string()
|
|
})
|
|
.ok();
|
|
}
|
|
"DB_ARC_SIZE" => {
|
|
env_config.db_arc_size = value
|
|
.parse()
|
|
.map_err(|_| "Failed to parse KANIDM_DB_ARC_SIZE as value".to_string())
|
|
.ok();
|
|
}
|
|
"ADMIN_BIND_PATH" => {
|
|
env_config.adminbindpath = Some(value.to_string());
|
|
}
|
|
"REPLICATION_ORIGIN" => {
|
|
let repl_origin = Url::parse(value.as_str()).map_err(|err| {
|
|
format!("Failed to parse KANIDM_REPLICATION_ORIGIN as URL: {}", err)
|
|
})?;
|
|
if let Some(repl) = &mut env_config.repl_config {
|
|
repl.origin = repl_origin
|
|
} else {
|
|
env_config.repl_config = Some(ReplicationConfiguration {
|
|
origin: repl_origin,
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
"REPLICATION_BINDADDRESS" => {
|
|
let repl_bind_address = value
|
|
.parse()
|
|
.map_err(|_| "Failed to parse replication bind address".to_string())?;
|
|
if let Some(repl) = &mut env_config.repl_config {
|
|
repl.bindaddress = repl_bind_address;
|
|
} else {
|
|
env_config.repl_config = Some(ReplicationConfiguration {
|
|
bindaddress: repl_bind_address,
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
"REPLICATION_TASK_POLL_INTERVAL" => {
|
|
let poll_interval = value
|
|
.parse()
|
|
.map_err(|_| {
|
|
"Failed to parse replication task poll interval as u64".to_string()
|
|
})
|
|
.ok();
|
|
if let Some(repl) = &mut env_config.repl_config {
|
|
repl.task_poll_interval = poll_interval;
|
|
} else {
|
|
env_config.repl_config = Some(ReplicationConfiguration {
|
|
task_poll_interval: poll_interval,
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
"OTEL_GRPC_URL" => {
|
|
env_config.otel_grpc_url = Some(value.to_string());
|
|
}
|
|
|
|
_ => eprintln!("Ignoring env var KANIDM_{key}"),
|
|
}
|
|
}
|
|
|
|
Ok(env_config)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone, Copy, Default, Eq, PartialEq)]
|
|
pub enum ServerRole {
|
|
#[default]
|
|
WriteReplica,
|
|
WriteReplicaNoUI,
|
|
ReadOnlyReplica,
|
|
}
|
|
|
|
impl Display for ServerRole {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
ServerRole::WriteReplica => f.write_str("write replica"),
|
|
ServerRole::WriteReplicaNoUI => f.write_str("write replica (no ui)"),
|
|
ServerRole::ReadOnlyReplica => f.write_str("read only replica"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for ServerRole {
|
|
type Err = &'static str;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"write_replica" => Ok(ServerRole::WriteReplica),
|
|
"write_replica_no_ui" => Ok(ServerRole::WriteReplicaNoUI),
|
|
"read_only_replica" => Ok(ServerRole::ReadOnlyReplica),
|
|
_ => Err("Must be one of write_replica, write_replica_no_ui, read_only_replica"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct IntegrationTestConfig {
|
|
pub admin_user: String,
|
|
pub admin_password: String,
|
|
pub idm_admin_user: String,
|
|
pub idm_admin_password: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct IntegrationReplConfig {
|
|
// We can bake in a private key for mTLS here.
|
|
// pub private_key: PKey
|
|
|
|
// We might need some condition variables / timers to force replication
|
|
// events? Or a channel to submit with oneshot responses.
|
|
}
|
|
|
|
/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Configuration {
|
|
pub address: String,
|
|
pub ldapbindaddress: Option<String>,
|
|
pub adminbindpath: String,
|
|
pub threads: usize,
|
|
// db type later
|
|
pub db_path: Option<PathBuf>,
|
|
pub db_fs_type: Option<FsType>,
|
|
pub db_arc_size: Option<usize>,
|
|
pub maximum_request: usize,
|
|
pub trust_x_forward_for: bool,
|
|
pub tls_config: Option<TlsConfiguration>,
|
|
pub integration_test_config: Option<Box<IntegrationTestConfig>>,
|
|
pub online_backup: Option<OnlineBackup>,
|
|
pub domain: String,
|
|
pub origin: String,
|
|
pub role: ServerRole,
|
|
pub output_mode: ConsoleOutputMode,
|
|
pub log_level: LogLevel,
|
|
/// Replication settings.
|
|
pub repl_config: Option<ReplicationConfiguration>,
|
|
/// This allows internally setting some unsafe options for replication.
|
|
pub integration_repl_config: Option<Box<IntegrationReplConfig>>,
|
|
pub otel_grpc_url: Option<String>,
|
|
}
|
|
|
|
impl Configuration {
|
|
pub fn build() -> ConfigurationBuilder {
|
|
ConfigurationBuilder {
|
|
bindaddress: None,
|
|
ldapbindaddress: None,
|
|
adminbindpath: None,
|
|
threads: std::thread::available_parallelism()
|
|
.map(|t| t.get())
|
|
.unwrap_or_else(|_e| {
|
|
eprintln!("WARNING: Unable to read number of available CPUs, defaulting to 4");
|
|
4
|
|
}),
|
|
db_path: None,
|
|
db_fs_type: None,
|
|
db_arc_size: None,
|
|
maximum_request: 256 * 1024, // 256k
|
|
trust_x_forward_for: None,
|
|
tls_key: None,
|
|
tls_chain: None,
|
|
tls_client_ca: None,
|
|
online_backup: None,
|
|
domain: None,
|
|
origin: None,
|
|
output_mode: None,
|
|
log_level: None,
|
|
role: None,
|
|
repl_config: None,
|
|
otel_grpc_url: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_for_test() -> Self {
|
|
Configuration {
|
|
address: DEFAULT_SERVER_ADDRESS.to_string(),
|
|
ldapbindaddress: None,
|
|
adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
|
|
threads: 1,
|
|
db_path: None,
|
|
db_fs_type: None,
|
|
db_arc_size: None,
|
|
maximum_request: 256 * 1024, // 256k
|
|
trust_x_forward_for: false,
|
|
tls_config: None,
|
|
integration_test_config: None,
|
|
online_backup: None,
|
|
domain: "idm.example.com".to_string(),
|
|
origin: "https://idm.example.com".to_string(),
|
|
output_mode: ConsoleOutputMode::default(),
|
|
log_level: LogLevel::default(),
|
|
role: ServerRole::WriteReplica,
|
|
repl_config: None,
|
|
integration_repl_config: None,
|
|
otel_grpc_url: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Configuration {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "address: {}, ", self.address)?;
|
|
write!(f, "domain: {}, ", self.domain)?;
|
|
match &self.ldapbindaddress {
|
|
Some(la) => write!(f, "ldap address: {}, ", la),
|
|
None => write!(f, "ldap address: disabled, "),
|
|
}?;
|
|
write!(f, "origin: {} ", self.origin)?;
|
|
write!(f, "admin bind path: {}, ", self.adminbindpath)?;
|
|
write!(f, "thread count: {}, ", self.threads)?;
|
|
write!(
|
|
f,
|
|
"dbpath: {}, ",
|
|
self.db_path
|
|
.as_ref()
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
.unwrap_or("MEMORY".to_string())
|
|
)?;
|
|
match self.db_arc_size {
|
|
Some(v) => write!(f, "arcsize: {}, ", v),
|
|
None => write!(f, "arcsize: AUTO, "),
|
|
}?;
|
|
write!(f, "max request size: {}b, ", self.maximum_request)?;
|
|
write!(f, "trust X-Forwarded-For: {}, ", self.trust_x_forward_for)?;
|
|
write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
|
|
match &self.online_backup {
|
|
Some(bck) => write!(
|
|
f,
|
|
"online_backup: enabled: {} - schedule: {} versions: {} path: {}, ",
|
|
bck.enabled,
|
|
bck.schedule,
|
|
bck.versions,
|
|
bck.path
|
|
.as_ref()
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
.unwrap_or("<unset>".to_string())
|
|
),
|
|
None => write!(f, "online_backup: disabled, "),
|
|
}?;
|
|
write!(
|
|
f,
|
|
"integration mode: {}, ",
|
|
self.integration_test_config.is_some()
|
|
)?;
|
|
write!(f, "console output format: {:?} ", self.output_mode)?;
|
|
write!(f, "log_level: {}", self.log_level)?;
|
|
write!(f, "role: {}, ", self.role)?;
|
|
match &self.repl_config {
|
|
Some(repl) => {
|
|
write!(f, "replication: enabled")?;
|
|
write!(f, "repl_origin: {} ", repl.origin)?;
|
|
write!(f, "repl_address: {} ", repl.bindaddress)?;
|
|
write!(
|
|
f,
|
|
"integration repl config mode: {}, ",
|
|
self.integration_repl_config.is_some()
|
|
)?;
|
|
}
|
|
None => {
|
|
write!(f, "replication: disabled, ")?;
|
|
}
|
|
}
|
|
write!(f, "otel_grpc_url: {:?}", self.otel_grpc_url)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The internal configuration of the server. User-facing configuration is in [ServerConfig], as the configuration file is parsed by that object.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ConfigurationBuilder {
|
|
bindaddress: Option<String>,
|
|
ldapbindaddress: Option<String>,
|
|
adminbindpath: Option<String>,
|
|
threads: usize,
|
|
db_path: Option<PathBuf>,
|
|
db_fs_type: Option<FsType>,
|
|
db_arc_size: Option<usize>,
|
|
maximum_request: usize,
|
|
trust_x_forward_for: Option<bool>,
|
|
tls_key: Option<PathBuf>,
|
|
tls_chain: Option<PathBuf>,
|
|
tls_client_ca: Option<PathBuf>,
|
|
online_backup: Option<OnlineBackup>,
|
|
domain: Option<String>,
|
|
origin: Option<String>,
|
|
role: Option<ServerRole>,
|
|
output_mode: Option<ConsoleOutputMode>,
|
|
log_level: Option<LogLevel>,
|
|
repl_config: Option<ReplicationConfiguration>,
|
|
otel_grpc_url: Option<String>,
|
|
}
|
|
|
|
impl ConfigurationBuilder {
|
|
#![allow(clippy::needless_pass_by_value)]
|
|
pub fn add_cli_config(mut self, cli_config: CliConfig) -> Self {
|
|
if cli_config.output_mode.is_some() {
|
|
self.output_mode = cli_config.output_mode;
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
pub fn add_env_config(mut self, env_config: EnvironmentConfig) -> Self {
|
|
if env_config.bindaddress.is_some() {
|
|
self.bindaddress = env_config.bindaddress;
|
|
}
|
|
|
|
if env_config.ldapbindaddress.is_some() {
|
|
self.ldapbindaddress = env_config.ldapbindaddress;
|
|
}
|
|
|
|
if env_config.adminbindpath.is_some() {
|
|
self.adminbindpath = env_config.adminbindpath;
|
|
}
|
|
|
|
if env_config.db_path.is_some() {
|
|
self.db_path = env_config.db_path;
|
|
}
|
|
|
|
if env_config.db_fs_type.is_some() {
|
|
self.db_fs_type = env_config.db_fs_type;
|
|
}
|
|
|
|
if env_config.db_arc_size.is_some() {
|
|
self.db_arc_size = env_config.db_arc_size;
|
|
}
|
|
|
|
if env_config.trust_x_forward_for.is_some() {
|
|
self.trust_x_forward_for = env_config.trust_x_forward_for;
|
|
}
|
|
|
|
if env_config.tls_key.is_some() {
|
|
self.tls_key = env_config.tls_key;
|
|
}
|
|
|
|
if env_config.tls_chain.is_some() {
|
|
self.tls_chain = env_config.tls_chain;
|
|
}
|
|
|
|
if env_config.tls_client_ca.is_some() {
|
|
self.tls_client_ca = env_config.tls_client_ca;
|
|
}
|
|
|
|
if env_config.online_backup.is_some() {
|
|
self.online_backup = env_config.online_backup;
|
|
}
|
|
|
|
if env_config.domain.is_some() {
|
|
self.domain = env_config.domain;
|
|
}
|
|
|
|
if env_config.origin.is_some() {
|
|
self.origin = env_config.origin;
|
|
}
|
|
|
|
if env_config.role.is_some() {
|
|
self.role = env_config.role;
|
|
}
|
|
|
|
if env_config.log_level.is_some() {
|
|
self.log_level = env_config.log_level;
|
|
}
|
|
|
|
if env_config.repl_config.is_some() {
|
|
self.repl_config = env_config.repl_config;
|
|
}
|
|
|
|
if env_config.otel_grpc_url.is_some() {
|
|
self.otel_grpc_url = env_config.otel_grpc_url;
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
pub fn add_opt_toml_config(self, toml_config: Option<ServerConfigUntagged>) -> Self {
|
|
// Can only proceed if the config is real
|
|
let Some(toml_config) = toml_config else {
|
|
return self;
|
|
};
|
|
|
|
match toml_config {
|
|
ServerConfigUntagged::Version(ServerConfigVersion::V2 { values }) => {
|
|
self.add_v2_config(values)
|
|
}
|
|
ServerConfigUntagged::Legacy(config) => self.add_legacy_config(config),
|
|
}
|
|
}
|
|
|
|
fn add_legacy_config(mut self, config: ServerConfig) -> Self {
|
|
if config.domain.is_some() {
|
|
self.domain = config.domain;
|
|
}
|
|
|
|
if config.origin.is_some() {
|
|
self.origin = config.origin;
|
|
}
|
|
|
|
if config.db_path.is_some() {
|
|
self.db_path = config.db_path;
|
|
}
|
|
|
|
if config.db_fs_type.is_some() {
|
|
self.db_fs_type = config.db_fs_type;
|
|
}
|
|
|
|
if config.tls_key.is_some() {
|
|
self.tls_key = config.tls_key;
|
|
}
|
|
|
|
if config.tls_chain.is_some() {
|
|
self.tls_chain = config.tls_chain;
|
|
}
|
|
|
|
if config.tls_client_ca.is_some() {
|
|
self.tls_client_ca = config.tls_client_ca;
|
|
}
|
|
|
|
if config.bindaddress.is_some() {
|
|
self.bindaddress = config.bindaddress;
|
|
}
|
|
|
|
if config.ldapbindaddress.is_some() {
|
|
self.ldapbindaddress = config.ldapbindaddress;
|
|
}
|
|
|
|
if config.adminbindpath.is_some() {
|
|
self.adminbindpath = config.adminbindpath;
|
|
}
|
|
|
|
if config.role.is_some() {
|
|
self.role = config.role;
|
|
}
|
|
|
|
if config.log_level.is_some() {
|
|
self.log_level = config.log_level;
|
|
}
|
|
|
|
if let Some(threads) = config.thread_count {
|
|
self.threads = threads;
|
|
}
|
|
|
|
if let Some(maximum) = config.maximum_request_size_bytes {
|
|
self.maximum_request = maximum;
|
|
}
|
|
|
|
if config.db_arc_size.is_some() {
|
|
self.db_arc_size = config.db_arc_size;
|
|
}
|
|
|
|
if config.trust_x_forward_for.is_some() {
|
|
self.trust_x_forward_for = config.trust_x_forward_for;
|
|
}
|
|
|
|
if config.online_backup.is_some() {
|
|
self.online_backup = config.online_backup;
|
|
}
|
|
|
|
if config.repl_config.is_some() {
|
|
self.repl_config = config.repl_config;
|
|
}
|
|
|
|
if config.otel_grpc_url.is_some() {
|
|
self.otel_grpc_url = config.otel_grpc_url;
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
fn add_v2_config(mut self, config: ServerConfigV2) -> Self {
|
|
if config.domain.is_some() {
|
|
self.domain = config.domain;
|
|
}
|
|
|
|
if config.origin.is_some() {
|
|
self.origin = config.origin;
|
|
}
|
|
|
|
if config.db_path.is_some() {
|
|
self.db_path = config.db_path;
|
|
}
|
|
|
|
if config.db_fs_type.is_some() {
|
|
self.db_fs_type = config.db_fs_type;
|
|
}
|
|
|
|
if config.tls_key.is_some() {
|
|
self.tls_key = config.tls_key;
|
|
}
|
|
|
|
if config.tls_chain.is_some() {
|
|
self.tls_chain = config.tls_chain;
|
|
}
|
|
|
|
if config.tls_client_ca.is_some() {
|
|
self.tls_client_ca = config.tls_client_ca;
|
|
}
|
|
|
|
if config.bindaddress.is_some() {
|
|
self.bindaddress = config.bindaddress;
|
|
}
|
|
|
|
if config.ldapbindaddress.is_some() {
|
|
self.ldapbindaddress = config.ldapbindaddress;
|
|
}
|
|
|
|
if config.adminbindpath.is_some() {
|
|
self.adminbindpath = config.adminbindpath;
|
|
}
|
|
|
|
if config.role.is_some() {
|
|
self.role = config.role;
|
|
}
|
|
|
|
if config.log_level.is_some() {
|
|
self.log_level = config.log_level;
|
|
}
|
|
|
|
if let Some(threads) = config.thread_count {
|
|
self.threads = threads;
|
|
}
|
|
|
|
if let Some(maximum) = config.maximum_request_size_bytes {
|
|
self.maximum_request = maximum;
|
|
}
|
|
|
|
if config.db_arc_size.is_some() {
|
|
self.db_arc_size = config.db_arc_size;
|
|
}
|
|
|
|
if config.trust_x_forward_for.is_some() {
|
|
self.trust_x_forward_for = config.trust_x_forward_for;
|
|
}
|
|
|
|
if config.online_backup.is_some() {
|
|
self.online_backup = config.online_backup;
|
|
}
|
|
|
|
if config.repl_config.is_some() {
|
|
self.repl_config = config.repl_config;
|
|
}
|
|
|
|
if config.otel_grpc_url.is_some() {
|
|
self.otel_grpc_url = config.otel_grpc_url;
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
// We always set threads to 1 unless it's the main server.
|
|
pub fn is_server_mode(mut self, is_server: bool) -> Self {
|
|
if is_server {
|
|
self.threads = 1;
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn finish(self) -> Option<Configuration> {
|
|
let ConfigurationBuilder {
|
|
bindaddress,
|
|
ldapbindaddress,
|
|
adminbindpath,
|
|
threads,
|
|
db_path,
|
|
db_fs_type,
|
|
db_arc_size,
|
|
maximum_request,
|
|
trust_x_forward_for,
|
|
tls_key,
|
|
tls_chain,
|
|
tls_client_ca,
|
|
mut online_backup,
|
|
domain,
|
|
origin,
|
|
role,
|
|
output_mode,
|
|
log_level,
|
|
repl_config,
|
|
otel_grpc_url,
|
|
} = self;
|
|
|
|
let tls_config = match (tls_key, tls_chain, tls_client_ca) {
|
|
(Some(key), Some(chain), client_ca) => Some(TlsConfiguration {
|
|
chain,
|
|
key,
|
|
client_ca,
|
|
}),
|
|
_ => {
|
|
eprintln!("ERROR: Tls Private Key and Certificate Chain are required.");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let domain = domain.or_else(|| {
|
|
eprintln!("ERROR: domain was not set.");
|
|
None
|
|
})?;
|
|
|
|
let origin = origin.or_else(|| {
|
|
eprintln!("ERROR: origin was not set.");
|
|
None
|
|
})?;
|
|
|
|
if let Some(online_backup_ref) = online_backup.as_mut() {
|
|
if online_backup_ref.path.is_none() {
|
|
if let Some(db_path) = db_path.as_ref() {
|
|
if let Some(db_parent_path) = db_path.parent() {
|
|
online_backup_ref.path = Some(db_parent_path.to_path_buf());
|
|
} else {
|
|
eprintln!("ERROR: when db_path has no parent, and can not be used for online backups.");
|
|
return None;
|
|
}
|
|
} else {
|
|
eprintln!("ERROR: when db_path is unset (in memory) then online backup paths must be declared.");
|
|
return None;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Apply any defaults if needed
|
|
let adminbindpath =
|
|
adminbindpath.unwrap_or(env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string());
|
|
let address = bindaddress.unwrap_or(DEFAULT_SERVER_ADDRESS.to_string());
|
|
let trust_x_forward_for = trust_x_forward_for.unwrap_or_default();
|
|
let output_mode = output_mode.unwrap_or_default();
|
|
let role = role.unwrap_or(ServerRole::WriteReplica);
|
|
let log_level = log_level.unwrap_or_default();
|
|
|
|
Some(Configuration {
|
|
address,
|
|
ldapbindaddress,
|
|
adminbindpath,
|
|
threads,
|
|
db_path,
|
|
db_fs_type,
|
|
db_arc_size,
|
|
maximum_request,
|
|
trust_x_forward_for,
|
|
tls_config,
|
|
online_backup,
|
|
domain,
|
|
origin,
|
|
role,
|
|
output_mode,
|
|
log_level,
|
|
repl_config,
|
|
otel_grpc_url,
|
|
integration_repl_config: None,
|
|
integration_test_config: None,
|
|
})
|
|
}
|
|
}
|