From 08c6e12a74bd6639557ef3682df96f99b8761a61 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 13 Apr 2026 12:51:27 +0200 Subject: [PATCH] Update config and dependencies for auth Refactor configuration loading to separate user credentials from server settings. Update dependencies to their latest compatible versions to leverage new features and security patches. Key changes include: - Moving user credentials (`api_key`, `web_password`, `role`) from `.env` to a dedicated `users.toml` file for better manageability and security. - Introducing an `AuthenticatedUser` struct and extractor for API key-based authentication. - Updating `axum` and related crates to leverage Rust edition 2024 improvements. - Adjusting configuration values and tests to reflect these changes. --- docs/projektplan.md | 71 ++++++++++++++++++--------- server/Cargo.lock | 88 +++++++-------------------------- server/Cargo.toml | 6 +-- server/src/auth.rs | 51 ++++++++++++++++++++ server/src/lib.rs | 1 + server/src/routes/debug.rs | 13 +++++ server/src/routes/mod.rs | 6 ++- server/tests/auth_test.rs | 99 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 237 insertions(+), 98 deletions(-) create mode 100644 server/src/auth.rs create mode 100644 server/src/routes/debug.rs create mode 100644 server/tests/auth_test.rs diff --git a/docs/projektplan.md b/docs/projektplan.md index 4acd406..e803e07 100644 --- a/docs/projektplan.md +++ b/docs/projektplan.md @@ -487,16 +487,28 @@ Freitext: [________________________] --- -### 4. Konfiguration (.env) +### 4. Konfiguration + +**System-Konfiguration (.env):** ``` +# Server +SERVER_PORT=3000 +DATA_PATH=/data +USERS_FILE=users.toml + +# Logging +LOG_LEVEL=info +LOG_PATH=/var/log/recorder/ +LOG_MAX_DAYS=90 + # Aufbewahrung RETENTION_AUDIO_DAYS=30 RETENTION_TRANSCRIPT_DAYS=30 RETENTION_DOCUMENT_DAYS=0 # 0 = permanent # faster-whisper -WHISPER_URL=http://localhost:8100 +WHISPER_URL=http://localhost:10300 WHISPER_TIMEOUT_SECONDS=120 # Ollama @@ -510,27 +522,28 @@ LLM_API_KEY=... LLM_MODEL=... LLM_TEMPERATURE=0 -# Server -SERVER_PORT=3000 -DATA_PATH=/data - -# Logging -LOG_LEVEL=info -LOG_PATH=/var/log/recorder/ -LOG_MAX_DAYS=90 - -# API Keys (ein Eintrag pro Arzt, für Watch-Authentifizierung) -API_KEY_DR_MUELLER=... -API_KEY_DR_SCHMIDT=... - -# Web Login (ein Eintrag pro Arzt, bcrypt-Hash) -WEB_PASSWORD_DR_MUELLER=$2b$12$... -WEB_PASSWORD_DR_SCHMIDT=$2b$12$... - # Session SESSION_TIMEOUT_HOURS=8 ``` +**User-Verwaltung (users.toml):** + +User-Daten (API-Keys, Passwörter, Rollen) werden separat in `users.toml` verwaltet statt in .env. So können neue User hinzugefügt werden, ohne die System-Konfiguration anzufassen. Das `role`-Feld ermöglicht neben `doctor` auch andere Rollen (z.B. `mta`, `admin`). + +```toml +[[user]] +slug = "dr_mueller" +api_key = "..." +web_password = "$2b$12$..." +role = "doctor" + +[[user]] +slug = "dr_schmidt" +api_key = "..." +web_password = "$2b$12$..." +role = "doctor" +``` + --- ### 5. Vorgefertigte Prompts (prompts.toml) @@ -656,7 +669,7 @@ Axum selbst benötigt keinen GPU-Zugriff — STT und Preprocessing laufen als ex ```toml [dependencies] -axum = "0.7" +axum = "0.8" # 0.8 wegen edition 2024 (native async fn in Traits) tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["json", "multipart"] } askama = "0.12" @@ -668,10 +681,10 @@ uuid = { version = "1", features = ["v4"] } serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" -tower-http = { version = "0.5", features = ["limit", "trace"] } +tower-http = { version = "0.6", features = ["limit", "trace"] } # passend zu axum 0.8 rand = "0.8" # Session-Token-Generierung (OsRng) bcrypt = "0.15" # Passwort-Hashing (Web-Login) -axum-extra = { version = "0.9", features = ["cookie"] } # Cookie-Handling +axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 ``` --- @@ -814,7 +827,7 @@ axum-extra = { version = "0.9", features = ["cookie"] } # Cookie-Handling | Ollama Modell | Gemma 3 4B — ggf. Llama 3.1 8B falls Qualität nicht reicht | | Ionos Modell | Noch zu evaluieren | | Watch Hardware | Pixel Watch 2 oder 3 (LTE empfohlen) | -| Authentifizierung Browser | Entschieden: Serverseitiges Session-Token (256-Bit, Cookie mit `Secure`/`HttpOnly`/`SameSite=Strict`), Ablauf 8 h. Arzt-Identität ausschließlich aus Session, nie aus URL (IDOR-Prävention). Login gegen Arzt-Passwort in `.env`. | +| Authentifizierung Browser | Entschieden: Serverseitiges Session-Token (256-Bit, Cookie mit `Secure`/`HttpOnly`/`SameSite=Strict`), Ablauf 8 h. User-Identität ausschließlich aus Session, nie aus URL (IDOR-Prävention). Login gegen User-Passwort in `users.toml`. | | Prompt-Qualität | Erfordert Testing mit echten Diktaten | | Transkript editierbar? | Read-only oder editierbar vor Abschluss — offen | | Sicheres Löschen | Reicht rm oder Overwrite nötig? — offen | @@ -831,3 +844,15 @@ axum-extra = { version = "0.9", features = ["cookie"] } # Cookie-Handling - Multi-Tenant / Cloud-Hosting - Echtzeit-Transkription - Automatische Patientenzuordnung + +--- + +## Abweichungen von der Originalplanung + +| Änderung | Original | Aktuell | Grund | +|---|---|---|---| +| Crate-Versionen | axum 0.7, tower-http 0.5, axum-extra 0.9 | axum 0.8, tower-http 0.6, axum-extra 0.12 | Rust edition 2024 erfordert native async fn in Traits; axum 0.7 nutzt `#[async_trait]`, was inkompatibel ist | +| User-Verwaltung | `API_KEY_*` / `WEB_PASSWORD_*` in .env | Separate `users.toml` mit role-Feld | Flexibler: neue User ohne .env-Änderung, Rollen-System (doctor, mta, admin) | +| faster-whisper Port | 8100 | 10300 | Tatsächlicher Port des laufenden Containers auf minerva | +| Terminologie | "Arzt/Ärzte" | "User" | Rollen-System: nicht nur Ärzte, auch MTAs und ggf. Admins | +| Erster Upload neue case_id | "Fall unbekannt → ACK gone" | Neuer Fall anlegen, ACK "received" | Sonst würde der allererste Upload einer neuen case_id immer abgelehnt. "gone" nur für gelöschte Fälle. | diff --git a/server/Cargo.lock b/server/Cargo.lock index 711ef31..c2fb33d 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -61,17 +61,6 @@ dependencies = [ "nom", ] -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -86,13 +75,13 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.7.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "async-trait", "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -105,8 +94,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -120,19 +108,17 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -141,26 +127,24 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.6" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" dependencies = [ "axum", "axum-core", "bytes", "cookie", - "fastrand", + "futures-core", "futures-util", "http", "http-body", "http-body-util", "mime", - "multer", "pin-project-lite", - "serde", - "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -349,7 +333,7 @@ dependencies = [ "tokio", "toml", "tower", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-appender", "tracing-subscriber", @@ -906,9 +890,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" @@ -949,23 +933,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - [[package]] name = "native-tls" version = "0.2.18" @@ -1256,7 +1223,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tower", - "tower-http 0.6.8", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -1502,12 +1469,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1773,23 +1734,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-http" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" -dependencies = [ - "bitflags", - "bytes", - "http", - "http-body", - "http-body-util", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower-http" version = "0.6.8" @@ -1801,11 +1745,13 @@ dependencies = [ "futures-util", "http", "http-body", + "http-body-util", "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index a89077b..f50e8ba 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -axum = "0.7" +axum = "0.8" tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["json", "multipart"] } askama = "0.12" @@ -16,10 +16,10 @@ uuid = { version = "1", features = ["v4"] } serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" -tower-http = { version = "0.5", features = ["limit", "trace"] } +tower-http = { version = "0.6", features = ["limit", "trace"] } rand = "0.8" bcrypt = "0.15" -axum-extra = { version = "0.9", features = ["cookie"] } +axum-extra = { version = "0.12", features = ["cookie"] } [dev-dependencies] tower = { version = "0.5", features = ["util"] } diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000..6d1eae3 --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use axum::extract::FromRequestParts; +use axum::http::request::Parts; + +use crate::config::Config; +use crate::error::AppError; + +/// Extracted from the X-API-Key header on every authenticated request. +pub struct AuthenticatedUser { + pub slug: String, + pub role: String, + pub data_dir: PathBuf, +} + +impl FromRequestParts> for AuthenticatedUser { + type Rejection = AppError; + + async fn from_request_parts( + parts: &mut Parts, + state: &Arc, + ) -> Result { + let api_key = parts + .headers + .get("X-API-Key") + .and_then(|v| v.to_str().ok()) + .ok_or(AppError::Unauthorized)?; + + let slug = state + .api_keys + .get(api_key) + .ok_or(AppError::Unauthorized)?; + + let user = state + .users + .iter() + .find(|u| u.slug == *slug) + .ok_or(AppError::Unauthorized)?; + + let data_dir = state.data_path.join(slug); + tokio::fs::create_dir_all(data_dir.join("open")).await?; + tokio::fs::create_dir_all(data_dir.join("done")).await?; + + Ok(Self { + slug: slug.clone(), + role: user.role.clone(), + data_dir, + }) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index a68194b..0dcaec8 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod config; pub mod error; pub mod routes; diff --git a/server/src/routes/debug.rs b/server/src/routes/debug.rs new file mode 100644 index 0000000..f590723 --- /dev/null +++ b/server/src/routes/debug.rs @@ -0,0 +1,13 @@ +use axum::Json; +use serde_json::{json, Value}; + +use crate::auth::AuthenticatedUser; +use crate::error::AppError; + +pub async fn handle_whoami(user: AuthenticatedUser) -> Result, AppError> { + Ok(Json(json!({ + "slug": user.slug, + "role": user.role, + "data_dir": user.data_dir.to_string_lossy(), + }))) +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index ce121b9..d00ee15 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,11 +1,15 @@ +mod debug; mod health; use std::sync::Arc; +use axum::routing::get; use axum::Router; use crate::config::Config; pub fn api_router() -> Router> { - Router::new().route("/api/health", axum::routing::get(health::handle_health)) + Router::new() + .route("/api/health", get(health::handle_health)) + .route("/api/debug/whoami", get(debug::handle_whoami)) } diff --git a/server/tests/auth_test.rs b/server/tests/auth_test.rs new file mode 100644 index 0000000..ea4ae6a --- /dev/null +++ b/server/tests/auth_test.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use tower::util::ServiceExt; + +use doctate_server::config::{Config, User}; + +fn test_config() -> Arc { + Arc::new(Config { + server_port: 3000, + data_path: "/tmp/doctate-test".into(), + log_level: "info".into(), + log_path: "/tmp/doctate-test/logs".into(), + log_max_days: 90, + users: vec![User { + slug: "dr_test".into(), + api_key: "test-key-123".into(), + web_password: "unused".into(), + role: "doctor".into(), + }], + api_keys: HashMap::from([("test-key-123".into(), "dr_test".into())]), + retention_audio_days: 30, + retention_transcript_days: 30, + retention_document_days: 0, + whisper_url: "http://localhost:10300".into(), + whisper_timeout_seconds: 120, + ollama_url: "http://localhost:11434".into(), + ollama_model: "gemma3:4b".into(), + ollama_keep_alive: 0, + llm_url: String::new(), + llm_api_key: String::new(), + llm_model: String::new(), + llm_temperature: 0.0, + session_timeout_hours: 8, + }) +} + +#[tokio::test] +async fn valid_api_key_returns_user() { + let app = doctate_server::create_router(test_config()); + + let response = app + .oneshot( + Request::builder() + .uri("/api/debug/whoami") + .header("X-API-Key", "test-key-123") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(json["slug"], "dr_test"); + assert_eq!(json["role"], "doctor"); +} + +#[tokio::test] +async fn invalid_api_key_returns_401() { + let app = doctate_server::create_router(test_config()); + + let response = app + .oneshot( + Request::builder() + .uri("/api/debug/whoami") + .header("X-API-Key", "wrong-key") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn missing_api_key_returns_401() { + let app = doctate_server::create_router(test_config()); + + let response = app + .oneshot( + Request::builder() + .uri("/api/debug/whoami") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +}