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