mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
packages/ak-common/config: init (#21256)
This commit is contained in:
committed by
GitHub
parent
ba82c97409
commit
3355669274
@@ -2,12 +2,14 @@
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
|
||||
199
Cargo.lock
generated
199
Cargo.lock
generated
@@ -76,6 +76,12 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -115,12 +121,20 @@ dependencies = [
|
||||
name = "authentik-common"
|
||||
version = "2026.5.0-rc1"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"axum-server",
|
||||
"config",
|
||||
"eyre",
|
||||
"glob",
|
||||
"nix",
|
||||
"notify",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -201,7 +215,7 @@ version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
@@ -215,6 +229,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@@ -367,6 +387,19 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.15.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"pathdiff",
|
||||
"serde_core",
|
||||
"winnow",
|
||||
"yaml-rust2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -477,6 +510,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -520,6 +559,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@@ -660,6 +708,15 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -907,6 +964,26 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -1008,6 +1085,26 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -1030,6 +1127,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
@@ -1092,6 +1195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -1102,7 +1206,7 @@ version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -1118,6 +1222,33 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.0"
|
||||
@@ -1174,6 +1305,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1347,7 +1484,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1459,6 +1596,19 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
@@ -1577,7 +1727,7 @@ version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -1773,7 +1923,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
@@ -1788,6 +1938,19 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -1949,7 +2112,7 @@ version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -2219,7 +2382,7 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
@@ -2520,6 +2683,15 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
@@ -2578,7 +2750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -2614,6 +2786,17 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
|
||||
dependencies = [
|
||||
"arraydeque",
|
||||
"encoding_rs",
|
||||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
||||
@@ -18,13 +18,20 @@ license-file = "LICENSE"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
arc-swap = "= 1.9.0"
|
||||
axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
|
||||
aws-lc-rs = { version = "= 1.16.2", features = ["fips"] }
|
||||
clap = { version = "= 4.6.0", features = ["derive", "env"] }
|
||||
colored = "= 3.1.1"
|
||||
config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [
|
||||
"yaml",
|
||||
"async",
|
||||
] }
|
||||
dotenvy = "= 0.15.7"
|
||||
eyre = "= 0.6.12"
|
||||
glob = "= 0.3.3"
|
||||
nix = { version = "= 0.31.2", features = ["signal"] }
|
||||
notify = "= 8.2.0"
|
||||
regex = "= 1.12.3"
|
||||
reqwest = { version = "= 0.13.2", features = [
|
||||
"form",
|
||||
@@ -48,6 +55,7 @@ serde_repr = "= 0.1.20"
|
||||
serde_with = { version = "= 3.18.0", default-features = false, features = [
|
||||
"base64",
|
||||
] }
|
||||
tempfile = "= 3.27.0"
|
||||
tokio = { version = "= 1.50.0", features = ["full", "tracing"] }
|
||||
tokio-util = { version = "= 0.7.18", features = ["full"] }
|
||||
tracing = "= 0.1.44"
|
||||
|
||||
@@ -47,6 +47,7 @@ listen:
|
||||
- "[::]:9300"
|
||||
debug: 0.0.0.0:9900
|
||||
debug_py: 0.0.0.0:9901
|
||||
debug_tokio: "[::]:6669"
|
||||
trusted_proxy_cidrs:
|
||||
- 127.0.0.0/8
|
||||
- 10.0.0.0/8
|
||||
@@ -73,6 +74,19 @@ log_level: info
|
||||
log:
|
||||
http_headers:
|
||||
- User-Agent
|
||||
rust_log:
|
||||
"console_subscriber": info
|
||||
"h2": info
|
||||
"hyper_util": warn
|
||||
"mio": info
|
||||
"notify": info
|
||||
"reqwest": info
|
||||
"runtime": info
|
||||
"rustls": info
|
||||
"sqlx": info
|
||||
"sqlx_postgres": info
|
||||
"tokio": info
|
||||
"tungstenite": info
|
||||
|
||||
sessions:
|
||||
unauthenticated_age: days=1
|
||||
|
||||
@@ -10,14 +10,22 @@ license-file.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
arc-swap.workspace = true
|
||||
axum-server.workspace = true
|
||||
config-rs.workspace = true
|
||||
eyre.workspace = true
|
||||
tokio.workspace = true
|
||||
glob.workspace = true
|
||||
notify.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
nix.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -293,8 +293,8 @@ impl Arbiter {
|
||||
pub enum Event {
|
||||
/// A signal has been received.
|
||||
Signal(SignalKind),
|
||||
#[cfg(test)]
|
||||
Noop,
|
||||
/// The configuration has been reloaded from sources.
|
||||
ConfigChanged,
|
||||
}
|
||||
|
||||
impl From<SignalKind> for Event {
|
||||
@@ -414,15 +414,15 @@ mod tests {
|
||||
let mut events_rx1 = arbiter.events_subscribe();
|
||||
let mut events_rx2 = arbiter.events_subscribe();
|
||||
|
||||
let _ = arbiter.send_event(Event::Noop);
|
||||
let _ = arbiter.send_event(Event::ConfigChanged);
|
||||
|
||||
assert_eq!(
|
||||
events_rx1.recv().await.expect("failed to receive event"),
|
||||
Event::Noop,
|
||||
Event::ConfigChanged,
|
||||
);
|
||||
assert_eq!(
|
||||
events_rx2.recv().await.expect("failed to receive event"),
|
||||
Event::Noop,
|
||||
Event::ConfigChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
429
packages/ak-common/src/config/mod.rs
Normal file
429
packages/ak-common/src/config/mod.rs
Normal file
@@ -0,0 +1,429 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, read_to_string},
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use eyre::Result;
|
||||
use notify::{RecommendedWatcher, Watcher as _};
|
||||
use serde_json::{Map, Value};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{error, info, warn};
|
||||
use url::Url;
|
||||
|
||||
pub mod schema;
|
||||
pub use schema::Config;
|
||||
|
||||
use crate::arbiter::{Arbiter, Event, Tasks};
|
||||
|
||||
static DEFAULT_CONFIG: &str = include_str!("../../../../authentik/lib/default.yml");
|
||||
static CONFIG_MANAGER: OnceLock<ConfigManager> = OnceLock::new();
|
||||
|
||||
/// List of paths from where to read YAML configuration.
|
||||
fn config_paths() -> Vec<PathBuf> {
|
||||
let mut config_paths = vec![
|
||||
PathBuf::from("/etc/authentik/config.yml"),
|
||||
PathBuf::from(""),
|
||||
];
|
||||
if let Ok(workspace) = env::var("WORKSPACE_DIR") {
|
||||
let _ = env::set_current_dir(workspace);
|
||||
}
|
||||
|
||||
if let Ok(paths) = glob::glob("/etc/authentik/config.d/*.yml") {
|
||||
config_paths.extend(paths.filter_map(Result::ok));
|
||||
}
|
||||
|
||||
let environment = env::var("AUTHENTIK_ENV").unwrap_or_else(|_| "local".to_owned());
|
||||
|
||||
let mut computed_paths = Vec::new();
|
||||
|
||||
for path in config_paths {
|
||||
if let Ok(metadata) = fs::metadata(&path) {
|
||||
if !metadata.is_dir() {
|
||||
computed_paths.push(path);
|
||||
}
|
||||
} else {
|
||||
let env_paths = vec![
|
||||
path.join(format!("{environment}.yml")),
|
||||
path.join(format!("{environment}.env.yml")),
|
||||
];
|
||||
for env_path in env_paths {
|
||||
if let Ok(metadata) = fs::metadata(&env_path)
|
||||
&& !metadata.is_dir()
|
||||
{
|
||||
computed_paths.push(env_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computed_paths
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load the configuration from files and environment into a [`Value`], allowing for extra
|
||||
/// processing later.
|
||||
fn load_raw(config_paths: &[PathBuf]) -> Result<Value> {
|
||||
let mut builder = config_rs::Config::builder().add_source(config_rs::File::from_str(
|
||||
DEFAULT_CONFIG,
|
||||
config_rs::FileFormat::Yaml,
|
||||
));
|
||||
for path in config_paths {
|
||||
builder = builder.add_source(
|
||||
config_rs::File::from(path.as_path()).format(config_rs::FileFormat::Yaml),
|
||||
);
|
||||
}
|
||||
builder = builder.add_source(
|
||||
config_rs::Environment::with_prefix("AUTHENTIK")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
);
|
||||
let config = builder.build()?;
|
||||
let raw = config.try_deserialize::<Value>()?;
|
||||
Ok(raw)
|
||||
}
|
||||
|
||||
/// Expand a value if it matches an env:// or file:// protocol.
|
||||
///
|
||||
/// If expanded from a file, returns the file path for it to be watched later.
|
||||
fn expand_value(value: &str) -> (String, Option<PathBuf>) {
|
||||
let value = value.trim();
|
||||
if let Ok(uri) = Url::parse(value) {
|
||||
let fallback = uri.query().unwrap_or("").to_owned();
|
||||
match uri.scheme() {
|
||||
"file" => {
|
||||
let path = uri.path();
|
||||
match read_to_string(path).map(|s| s.trim().to_owned()) {
|
||||
Ok(value) => return (value, Some(PathBuf::from(path))),
|
||||
Err(err) => {
|
||||
error!("failed to read config value from {path}: {err}");
|
||||
return (fallback, Some(PathBuf::from(path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
"env" => {
|
||||
if let Some(var) = uri.host_str() {
|
||||
if let Ok(value) = env::var(var) {
|
||||
return (value, None);
|
||||
}
|
||||
return (fallback, None);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(value.to_owned(), None)
|
||||
}
|
||||
|
||||
/// Expand the configuration for env:// and file:// values.
|
||||
///
|
||||
/// Returns the expanded configuration and a list of file paths for which to watch changes.
|
||||
fn expand(mut raw: Value) -> (Value, Vec<PathBuf>) {
|
||||
let mut file_paths = Vec::new();
|
||||
let value = match &mut raw {
|
||||
Value::String(s) => {
|
||||
let (v, path) = Self::expand_value(s);
|
||||
if let Some(path) = path {
|
||||
file_paths.push(path);
|
||||
}
|
||||
Value::String(v)
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
let mut res = Vec::with_capacity(arr.len());
|
||||
for v in arr {
|
||||
let (expanded, paths) = Self::expand(v.clone());
|
||||
file_paths.extend(paths);
|
||||
res.push(expanded);
|
||||
}
|
||||
Value::Array(res)
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut res = Map::with_capacity(map.len());
|
||||
for (k, v) in map {
|
||||
let (expanded, paths) = Self::expand(v.clone());
|
||||
file_paths.extend(paths);
|
||||
res.insert(k.clone(), expanded);
|
||||
}
|
||||
Value::Object(res)
|
||||
}
|
||||
_ => raw,
|
||||
};
|
||||
(value, file_paths)
|
||||
}
|
||||
|
||||
/// Load the configuration.
|
||||
fn load(config_paths: &[PathBuf]) -> Result<(Self, Vec<PathBuf>)> {
|
||||
let raw = Self::load_raw(config_paths)?;
|
||||
let (expanded, file_paths) = Self::expand(raw);
|
||||
let config: Self = serde_json::from_value(expanded)?;
|
||||
Ok((config, file_paths))
|
||||
}
|
||||
}
|
||||
|
||||
/// Manager of the config. Handles reloading when changed on disk.
|
||||
struct ConfigManager {
|
||||
config: ArcSwap<Config>,
|
||||
config_paths: Vec<PathBuf>,
|
||||
watch_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Initialize the configuration. It relies on a global [`OnceLock`] and must be called before
|
||||
/// other methods are called.
|
||||
pub fn init() -> Result<()> {
|
||||
info!("loading config");
|
||||
let config_paths = config_paths();
|
||||
init_with_paths(config_paths)?;
|
||||
info!("config loaded");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the configuration from a list of specific paths to read if from.
|
||||
fn init_with_paths(config_paths: Vec<PathBuf>) -> Result<()> {
|
||||
let (config, mut other_paths) = Config::load(&config_paths)?;
|
||||
let mut watch_paths = config_paths.clone();
|
||||
watch_paths.append(&mut other_paths);
|
||||
let manager = ConfigManager {
|
||||
config: ArcSwap::from_pointee(config),
|
||||
config_paths,
|
||||
watch_paths,
|
||||
};
|
||||
CONFIG_MANAGER.get_or_init(|| manager);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch for configuration changes, reload the configuration in memory and send events.
|
||||
///
|
||||
/// [`init`] must be called before this is used.
|
||||
async fn watch_config(arbiter: Arbiter) -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |res: notify::Result<notify::Event>| {
|
||||
if let Ok(event) = res
|
||||
&& let notify::EventKind::Modify(_) = &event.kind
|
||||
{
|
||||
let _ = tx.blocking_send(());
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)?;
|
||||
let watch_paths = &CONFIG_MANAGER
|
||||
.get()
|
||||
.expect("failed to get config, has it been initialized?")
|
||||
.watch_paths;
|
||||
for path in watch_paths {
|
||||
watcher.watch(path.as_ref(), notify::RecursiveMode::NonRecursive)?;
|
||||
}
|
||||
|
||||
let _ = arbiter.send_event(Event::ConfigChanged);
|
||||
info!("config file watcher started on paths: {:?}", watch_paths);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = rx.recv() => {
|
||||
info!("a configuration file changed, reloading config");
|
||||
if res.is_none() {
|
||||
break;
|
||||
}
|
||||
let manager = CONFIG_MANAGER.get().expect("failed to get config, has it been initialized?");
|
||||
match tokio::task::spawn_blocking(|| Config::load(&manager.config_paths)).await? {
|
||||
Ok((new_config, _)) => {
|
||||
info!("configuration reloaded");
|
||||
manager.config.store(Arc::new(new_config));
|
||||
if let Err(err) = arbiter.send_event(Event::ConfigChanged) {
|
||||
warn!("failed to notify of config change, aborting: {err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("failed to reload config, continuing with previous config: {err:?}");
|
||||
}
|
||||
}
|
||||
},
|
||||
() = arbiter.shutdown() => break,
|
||||
}
|
||||
}
|
||||
|
||||
info!("stopping config file watcher");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the configuration watcher.
|
||||
///
|
||||
/// [`init`] must be called before this is used.
|
||||
pub fn run(tasks: &mut Tasks) -> Result<()> {
|
||||
info!("starting config file watcher");
|
||||
let arbiter = tasks.arbiter();
|
||||
tasks
|
||||
.build_task()
|
||||
.name(&format!("{}::watch_config", module_path!()))
|
||||
.spawn(watch_config(arbiter))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the currently stored configuration.
|
||||
///
|
||||
/// [`init`] must be called before this is used.
|
||||
pub fn get() -> arc_swap::Guard<Arc<Config>> {
|
||||
let manager = CONFIG_MANAGER
|
||||
.get()
|
||||
.expect("failed to get config, has it been initialized?");
|
||||
manager.config.load()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{env, fs::File, io::Write as _, path::PathBuf};
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::arbiter::{Event, Tasks};
|
||||
|
||||
#[test]
|
||||
fn default_config() {
|
||||
let (config, _) = super::Config::load(&[]).expect("default config doesn't load");
|
||||
assert_eq!(config.secret_key, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_paths() {
|
||||
let temp_dir = tempdir().expect("failed to create temp dir");
|
||||
for f in &[
|
||||
"local.env.yml",
|
||||
"local.env.yaml",
|
||||
"test_config_paths.yml",
|
||||
"test_config_paths.env.yml",
|
||||
"test_config_paths.env.yaml",
|
||||
] {
|
||||
File::create(temp_dir.path().join(f)).expect("failed to create file");
|
||||
}
|
||||
#[expect(unsafe_code, reason = "testing")]
|
||||
// SAFETY: testing
|
||||
unsafe {
|
||||
env::set_var("WORKSPACE_DIR", temp_dir.path());
|
||||
env::set_var("AUTHENTIK_ENV", "test_config_paths");
|
||||
}
|
||||
|
||||
let paths = super::config_paths();
|
||||
|
||||
assert_eq!(
|
||||
&paths,
|
||||
&[
|
||||
PathBuf::from("test_config_paths.yml"),
|
||||
PathBuf::from("test_config_paths.env.yml"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand() {
|
||||
let temp_dir = tempdir().expect("failed to create temp dir");
|
||||
|
||||
let secret_key_path = temp_dir.path().join("secret_key");
|
||||
let mut secret_key_file = File::create(&secret_key_path).expect("failed to create file");
|
||||
write!(secret_key_file, "my_secret_key").expect("failed to write to file");
|
||||
|
||||
let config_file_path = temp_dir.path().join("config");
|
||||
let mut config_file = File::create(&config_file_path).expect("failed to create file");
|
||||
writeln!(
|
||||
config_file,
|
||||
"secret_key: file://{}\npostgresql:\n password: env://TEST_CONFIG_POSTGRES_PASS",
|
||||
secret_key_path.display()
|
||||
)
|
||||
.expect("failed to write to file");
|
||||
|
||||
#[expect(unsafe_code, reason = "testing")]
|
||||
// SAFETY: testing
|
||||
unsafe {
|
||||
env::set_var("TEST_CONFIG_POSTGRES_PASS", "my_postgres_pass");
|
||||
}
|
||||
|
||||
let (config, config_paths) =
|
||||
super::Config::load(&[config_file_path]).expect("failed to load config");
|
||||
|
||||
assert_eq!(config.secret_key, "my_secret_key");
|
||||
assert_eq!(config.postgresql.password, "my_postgres_pass");
|
||||
assert_eq!(config_paths, &[secret_key_path]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init() {
|
||||
super::init_with_paths(vec![]).expect("failed to init config");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher() {
|
||||
let temp_dir = tempdir().expect("failed to create temp dir");
|
||||
|
||||
let secret_key_path = temp_dir.path().join("secret_key");
|
||||
let mut secret_key_file = File::create(&secret_key_path).expect("failed to create file");
|
||||
write!(secret_key_file, "my_secret_key").expect("failed to write to file");
|
||||
drop(secret_key_file);
|
||||
|
||||
let config_file_path = temp_dir.path().join("config");
|
||||
let mut config_file = File::create(&config_file_path).expect("failed to create file");
|
||||
writeln!(
|
||||
config_file,
|
||||
"secret_key: file://{}\npostgresql:\n password: my_postgres_pass",
|
||||
secret_key_path.display()
|
||||
)
|
||||
.expect("failed to write to file");
|
||||
drop(config_file);
|
||||
|
||||
super::init_with_paths(vec![config_file_path.clone()]).expect("failed to init config");
|
||||
|
||||
let mut tasks = Tasks::new().expect("failed to create tasks");
|
||||
let arbiter = tasks.arbiter();
|
||||
let mut events_rx = arbiter.events_subscribe();
|
||||
|
||||
super::run(&mut tasks).expect("failed to start watcher");
|
||||
|
||||
assert_eq!(super::get().secret_key, "my_secret_key");
|
||||
assert_eq!(super::get().postgresql.password, "my_postgres_pass");
|
||||
|
||||
let _ = events_rx.recv().await;
|
||||
let mut secret_key_file = File::create(&secret_key_path).expect("failed to open file");
|
||||
write!(secret_key_file, "my_other_secret_key").expect("failed to write to file");
|
||||
drop(secret_key_file);
|
||||
|
||||
assert_eq!(
|
||||
events_rx.recv().await.expect("failed to receive event"),
|
||||
Event::ConfigChanged,
|
||||
);
|
||||
while !events_rx.is_empty() {
|
||||
assert_eq!(
|
||||
events_rx.recv().await.expect("failed to receive event"),
|
||||
Event::ConfigChanged,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(super::get().secret_key, "my_other_secret_key");
|
||||
assert_eq!(super::get().postgresql.password, "my_postgres_pass");
|
||||
|
||||
let mut config_file = File::create(&config_file_path).expect("failed to open file");
|
||||
writeln!(
|
||||
config_file,
|
||||
"secret_key: file://{}\npostgresql:\n password: my_new_postgres_pass",
|
||||
secret_key_path.display()
|
||||
)
|
||||
.expect("failed to write to file");
|
||||
drop(config_file);
|
||||
|
||||
assert_eq!(
|
||||
events_rx.recv().await.expect("failed to receive event"),
|
||||
Event::ConfigChanged,
|
||||
);
|
||||
while !events_rx.is_empty() {
|
||||
assert_eq!(
|
||||
events_rx.recv().await.expect("failed to receive event"),
|
||||
Event::ConfigChanged,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(super::get().secret_key, "my_other_secret_key");
|
||||
assert_eq!(super::get().postgresql.password, "my_new_postgres_pass");
|
||||
}
|
||||
}
|
||||
90
packages/ak-common/src/config/schema.rs
Normal file
90
packages/ak-common/src/config/schema.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub postgresql: PostgreSQLConfig,
|
||||
|
||||
pub listen: ListenConfig,
|
||||
|
||||
pub debug: bool,
|
||||
#[serde(default)]
|
||||
pub secret_key: String,
|
||||
|
||||
pub log_level: String,
|
||||
pub log: LogConfig,
|
||||
|
||||
pub error_reporting: ErrorReportingConfig,
|
||||
|
||||
pub compliance: ComplianceConfig,
|
||||
|
||||
pub web: WebConfig,
|
||||
|
||||
pub worker: WorkerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PostgreSQLConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub name: String,
|
||||
|
||||
pub sslmode: String,
|
||||
pub sslrootcert: Option<String>,
|
||||
pub sslcert: Option<String>,
|
||||
pub sslkey: Option<String>,
|
||||
|
||||
pub conn_max_age: Option<u64>,
|
||||
pub conn_health_checks: bool,
|
||||
|
||||
pub default_schema: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListenConfig {
|
||||
pub http: Vec<SocketAddr>,
|
||||
pub metrics: Vec<SocketAddr>,
|
||||
pub debug_tokio: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogConfig {
|
||||
pub http_headers: Vec<String>,
|
||||
pub rust_log: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ErrorReportingConfig {
|
||||
pub enabled: bool,
|
||||
pub sentry_dsn: Option<String>,
|
||||
pub environment: String,
|
||||
pub send_pii: bool,
|
||||
pub sample_rate: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComplianceConfig {
|
||||
pub fips: ComplianceFipsConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComplianceFipsConfig {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebConfig {
|
||||
pub path: String,
|
||||
pub timeout_http_read_header: String,
|
||||
pub timeout_http_read: String,
|
||||
pub timeout_http_write: String,
|
||||
pub timeout_http_idle: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkerConfig {
|
||||
pub processes: NonZeroUsize,
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod arbiter;
|
||||
pub use arbiter::{Arbiter, Event, Tasks};
|
||||
pub mod config;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user