Akteursbezogenes KI- und Butler-Aktivitätsprotokoll #44
1 changed files with 32 additions and 4 deletions
36
app.py
36
app.py
|
|
@ -9,6 +9,7 @@ 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"
|
||||
|
|
@ -46,6 +47,7 @@ _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")
|
||||
|
||||
|
|
@ -69,8 +71,12 @@ def _init_audit_db() -> bool:
|
|||
method TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
detail TEXT NOT NULL,
|
||||
dry_run INTEGER NOT NULL
|
||||
dry_run INTEGER NOT NULL,
|
||||
actor TEXT NOT NULL DEFAULT 'Legacy/API'
|
||||
)""")
|
||||
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
|
||||
|
|
@ -83,6 +89,7 @@ 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:
|
||||
|
|
@ -91,8 +98,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) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(entry["ts"], entry["endpoint"], entry["method"], entry["status"], entry["detail"], int(entry["dry_run"])),
|
||||
"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"]),
|
||||
)
|
||||
db.execute("DELETE FROM audit WHERE id NOT IN (SELECT id FROM audit ORDER BY id DESC LIMIT ?)", (MAX_AUDIT,))
|
||||
except (OSError, sqlite3.Error):
|
||||
|
|
@ -280,6 +287,27 @@ 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()
|
||||
|
|
@ -830,7 +858,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 FROM audit ORDER BY id DESC LIMIT ?",
|
||||
"SELECT ts, endpoint, method, status, detail, dry_run, actor FROM audit ORDER BY id DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [dict(row) | {"dry_run": bool(row["dry_run"])} for row in rows]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue