| Zeit | Akteur | Methode | Endpoint | Status | Dry-run | Detail |
|---|---|---|---|---|---|---|
| Wird geladen … | ||||||
diff --git a/app.py b/app.py index 5171de7..bac1a05 100644 --- a/app.py +++ b/app.py @@ -9,7 +9,6 @@ from pydantic import BaseModel, Field from fastapi import FastAPI, Request, HTTPException, Depends, Query from fastapi.responses import JSONResponse, RedirectResponse, Response, HTMLResponse from contextlib import asynccontextmanager -from contextvars import ContextVar log = logging.getLogger("butler") VERSION = "2.3.5" @@ -47,7 +46,6 @@ _load_config() # --- Audit log --- _audit_log: list[dict] = [] -_audit_actor: ContextVar[str] = ContextVar("audit_actor", default="System/API") MAX_AUDIT = 500 AUDIT_DB_PATH = os.environ.get("AUDIT_DB_PATH", "/data/state/audit.sqlite3") @@ -71,12 +69,8 @@ def _init_audit_db() -> bool: method TEXT NOT NULL, status INTEGER NOT NULL, detail TEXT NOT NULL, - dry_run INTEGER NOT NULL, - actor TEXT NOT NULL DEFAULT 'Legacy/API' + dry_run INTEGER NOT NULL )""") - columns = {row[1] for row in db.execute("PRAGMA table_info(audit)")} - if "actor" not in columns: - db.execute("ALTER TABLE audit ADD COLUMN actor TEXT NOT NULL DEFAULT 'Legacy/API'") return True except (OSError, sqlite3.Error): return False @@ -89,7 +83,6 @@ def _audit(endpoint: str, method: str, status: int, detail: str = "", dry_run: b "status": status, "detail": _redact_audit_detail(detail), "dry_run": dry_run, - "actor": _audit_actor.get(), } _audit_log.append(entry) if len(_audit_log) > MAX_AUDIT: @@ -98,8 +91,8 @@ def _audit(endpoint: str, method: str, status: int, detail: str = "", dry_run: b if _init_audit_db(): with sqlite3.connect(AUDIT_DB_PATH) as db: db.execute( - "INSERT INTO audit (ts, endpoint, method, status, detail, dry_run, actor) VALUES (?, ?, ?, ?, ?, ?, ?)", - (entry["ts"], entry["endpoint"], entry["method"], entry["status"], entry["detail"], int(entry["dry_run"]), entry["actor"]), + "INSERT INTO audit (ts, endpoint, method, status, detail, dry_run) VALUES (?, ?, ?, ?, ?, ?)", + (entry["ts"], entry["endpoint"], entry["method"], entry["status"], entry["detail"], int(entry["dry_run"])), ) db.execute("DELETE FROM audit WHERE id NOT IN (SELECT id FROM audit ORDER BY id DESC LIMIT ?)", (MAX_AUDIT,)) except (OSError, sqlite3.Error): @@ -287,27 +280,6 @@ def _verify(request: Request): if not csrf or not secrets.compare_digest(csrf, session["csrf"]): raise HTTPException(403, "Invalid CSRF token") - -def _clean_audit_actor(value: str) -> str: - cleaned = re.sub(r"[^\w .@/\-]", "", str(value or ""))[:40].strip() - return cleaned or "KI/API" - - -@app.middleware("http") -async def audit_actor_context(request: Request, call_next): - if request.headers.get("authorization", "").startswith("Bearer "): - actor = _clean_audit_actor(request.headers.get("x-butler-actor", "KI/API")) - elif _ui_session(request): - actor = "Weboberfläche" - else: - actor = "System/Öffentlich" - token = _audit_actor.set(actor) - try: - return await call_next(request) - finally: - _audit_actor.reset(token) - - def _emby_network_identity(endpoint: str) -> dict: """Normalize an Emby endpoint without treating IPv6 privacy addresses as new households.""" raw = str(endpoint or "").strip() @@ -858,7 +830,7 @@ async def audit(_=Depends(_verify), limit: int = Query(50, le=MAX_AUDIT)): with sqlite3.connect(AUDIT_DB_PATH) as db: db.row_factory = sqlite3.Row rows = db.execute( - "SELECT ts, endpoint, method, status, detail, dry_run, actor FROM audit ORDER BY id DESC LIMIT ?", + "SELECT ts, endpoint, method, status, detail, dry_run FROM audit ORDER BY id DESC LIMIT ?", (limit,), ).fetchall() return [dict(row) | {"dry_run": bool(row["dry_run"])} for row in rows] diff --git a/tests/test_app.py b/tests/test_app.py index 5c6b2d7..a8d4645 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -266,39 +266,6 @@ def test_audit_persists_and_redacts_secrets(tmp_path, monkeypatch): assert entry["detail"].count("[REDACTED]") == 3 -def test_audit_records_ai_and_web_ui_actor(tmp_path, monkeypatch): - db = tmp_path / "audit.sqlite3" - monkeypatch.setattr(app, "AUDIT_DB_PATH", str(db)) - - async def empty_status(): - return {} - - monkeypatch.setattr(app, "_collect_service_status", empty_status) - app._ui_sessions.clear() - with TestClient(app.app) as client: - ai = client.get( - "/status", - headers={"Authorization": "Bearer test-token", "X-Butler-Actor": "Trulla"}, - ) - assert ai.status_code == 200 - - client.headers.pop("Authorization", None) - client.get("/ui/session") - web = client.get("/status") - assert web.status_code == 200 - - entries = client.get("/audit").json() - - assert [item["actor"] for item in entries[:2]] == ["Weboberfläche", "Trulla"] - - -def test_ui_audit_has_actor_column_and_filter(): - with TestClient(app.app) as client: - response = client.get("/ui") - assert '
Wer hat wann welche Butler-Funktion aufgerufen? Sensible Werte bleiben redigiert.
| Zeit | Akteur | Methode | Endpoint | Status | Dry-run | Detail |
|---|---|---|---|---|---|---|
| Wird geladen … | ||||||
Redigierte Butler-Aktionen aus SQLite.
| Zeit | Methode | Endpoint | Status | Dry-run | Detail |
|---|---|---|---|---|---|
| Wird geladen … | |||||