Stage 1: Config-Layer (TOML laden/speichern, env-Fallback, Secret-Masking) + Tests
CI / test (push) Successful in 11m36s
CI / test (push) Successful in 11m36s
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
/// Laufzeit-Konfiguration des DT-MCP.
|
||||
///
|
||||
/// Bewusst serde-(de)serialisierbar, damit sie spaeter ueber die Admin-UI
|
||||
/// (Stage 3) editiert und auf Platte persistiert werden kann. Aufloesung:
|
||||
/// explizite Datei (`DTRACK_CONFIG`) hat Vorrang, sonst Umgebungsvariablen.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DtrackConfig {
|
||||
/// Basis-URL der DT-Instanz, z.B. https://dtrack.firma.intern
|
||||
pub url: String,
|
||||
/// API-Key eines read-only Teams.
|
||||
pub api_key: String,
|
||||
/// TLS-Zertpruefung deaktivieren (Notbehelf bei internem CA-Cert).
|
||||
#[serde(default)]
|
||||
pub insecure: bool,
|
||||
}
|
||||
|
||||
impl DtrackConfig {
|
||||
/// Liest die Config aus einer TOML-Datei.
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let raw = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Config nicht lesbar: {}", path.display()))?;
|
||||
toml::from_str(&raw).context("Config-TOML ungueltig")
|
||||
}
|
||||
|
||||
/// Schreibt die Config als TOML.
|
||||
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let raw = toml::to_string_pretty(self).context("Config nicht serialisierbar")?;
|
||||
std::fs::write(path, raw)
|
||||
.with_context(|| format!("Config nicht schreibbar: {}", path.display()))
|
||||
}
|
||||
|
||||
/// Baut die Config aus Umgebungsvariablen (`DTRACK_URL` / `DTRACK_API_KEY`
|
||||
/// / `DTRACK_INSECURE`). Das ist der Pfad fuers stdio-Binary auf dem Laptop.
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let url = std::env::var("DTRACK_URL").context("DTRACK_URL nicht gesetzt")?;
|
||||
let api_key = std::env::var("DTRACK_API_KEY").context("DTRACK_API_KEY nicht gesetzt")?;
|
||||
let insecure = std::env::var("DTRACK_INSECURE")
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
Ok(Self { url, api_key, insecure })
|
||||
}
|
||||
|
||||
/// Aufloesung mit Vorrang: explizite Datei (`DTRACK_CONFIG`) -> sonst env.
|
||||
pub fn resolve() -> Result<Self> {
|
||||
match std::env::var("DTRACK_CONFIG") {
|
||||
Ok(path) => Self::load(path),
|
||||
Err(_) => Self::from_env(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Maskierte Darstellung des API-Keys fuer Logs/UI.
|
||||
pub fn masked_api_key(&self) -> String {
|
||||
mask_secret(&self.api_key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Maskiert ein Secret: alles ausser den letzten 4 Zeichen wird zu Punkten.
|
||||
/// Reveal passiert spaeter explizit in der UI, nie by default.
|
||||
pub fn mask_secret(secret: &str) -> String {
|
||||
let n = secret.chars().count();
|
||||
if n <= 4 {
|
||||
return "\u{2022}".repeat(n);
|
||||
}
|
||||
let visible: String = secret.chars().skip(n - 4).collect();
|
||||
format!("{}{visible}", "\u{2022}".repeat(n - 4))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mask_hides_all_but_last_four() {
|
||||
let masked = mask_secret("odt_secret1234");
|
||||
assert!(masked.ends_with("1234"));
|
||||
assert_eq!(masked.chars().filter(|c| *c == '\u{2022}').count(), 10);
|
||||
assert_eq!(masked.chars().count(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_short_secret_fully() {
|
||||
let masked = mask_secret("abc");
|
||||
assert_eq!(masked.chars().count(), 3);
|
||||
assert!(masked.chars().all(|c| c == '\u{2022}'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_roundtrip() {
|
||||
let cfg = DtrackConfig {
|
||||
url: "https://dt.example".into(),
|
||||
api_key: "odt_abc1234".into(),
|
||||
insecure: true,
|
||||
};
|
||||
let raw = toml::to_string_pretty(&cfg).unwrap();
|
||||
let back = toml::from_str::<DtrackConfig>(&raw).unwrap();
|
||||
assert_eq!(back.url, cfg.url);
|
||||
assert_eq!(back.api_key, cfg.api_key);
|
||||
assert!(back.insecure);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user