Stage 1: Config-Layer (TOML laden/speichern, env-Fallback, Secret-Masking) + Tests
CI / test (push) Successful in 11m36s

This commit is contained in:
2026-06-23 16:06:53 +00:00
parent dcfe9f30e0
commit c09aa78101
+106
View File
@@ -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);
}
}