Audit-Log + Brute-Force-Schutz + unlimited_budget + User-Delete-Fix

- Schema-Migration: ON DELETE SET NULL fuer incidents.created_by, magic_links.user_id,
  network_analyses.created_by (behebt 500er beim User-Loeschen). Neue Spalte
  licenses.unlimited_budget. Neue Tabellen portal_audit_log, portal_login_attempts.
- Audit-Log: alle CREATE/UPDATE/DELETE auf Org/User/Lizenz/Quelle + Login-Events
  werden mit before/after-Diff in portal_audit_log geschrieben.
- Brute-Force-Schutz: 5 Fehlversuche pro IP+Username/15min -> 429 mit Retry-After.
- Token-Budget: expliziter Schalter unlimited_budget pro Lizenz. UI zeigt ehrlich
  >100%-Verbrauch (kein Math.min mehr) und ungebremste Anzeige bei unlimited.
- Neuer Audit-Log Tab mit Filter (Aktion/Ressource/Admin/Zeitraum) und Pagination.
Dieser Commit ist enthalten in:
claude-dev
2026-05-02 20:16:03 +00:00
Ursprung 0da66fb585
Commit 4dc372814d
15 geänderte Dateien mit 1215 neuen und 151 gelöschten Zeilen

Datei anzeigen

@@ -6,12 +6,13 @@ import logging
# Monitor-Source-Rules verfÃÆÃ¼gbar machen
sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor/src")
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from typing import Optional
from auth import get_current_admin
from database import db_dependency
from audit import log_action, get_client_ip, row_to_dict
import aiosqlite
sys.path.insert(0, os.path.join('/home/claude-dev/AegisSight-Monitor/src'))
@@ -65,6 +66,7 @@ async def list_global_sources(
@router.post("/global", status_code=201)
async def create_global_source(
data: GlobalSourceCreate,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
@@ -86,16 +88,24 @@ async def create_global_source(
VALUES (?, ?, ?, ?, ?, ?, ?, 'system', NULL)""",
(data.name, data.url, data.domain, data.source_type, data.category, data.status, data.notes),
)
src_id = cursor.lastrowid
await db.commit()
cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (cursor.lastrowid,))
return dict(await cursor.fetchone())
cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (src_id,))
new_src = dict(await cursor.fetchone())
await log_action(
db, admin, get_client_ip(request),
action="create", resource_type="source", resource_id=src_id,
after=new_src,
)
return new_src
@router.put("/global/{source_id}")
async def update_global_source(
source_id: int,
data: GlobalSourceUpdate,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
@@ -106,13 +116,14 @@ async def update_global_source(
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Grundquelle nicht gefunden")
before = dict(row)
updates = {}
for field, value in data.model_dump(exclude_none=True).items():
updates[field] = value
if not updates:
return dict(row)
return before
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [source_id]
@@ -120,24 +131,38 @@ async def update_global_source(
await db.commit()
cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,))
return dict(await cursor.fetchone())
after = dict(await cursor.fetchone())
await log_action(
db, admin, get_client_ip(request),
action="update", resource_type="source", resource_id=source_id,
before=before, after=after,
)
return after
@router.delete("/global/{source_id}", status_code=204)
async def delete_global_source(
source_id: int,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Grundquelle loeschen."""
cursor = await db.execute(
"SELECT id FROM sources WHERE id = ? AND tenant_id IS NULL", (source_id,)
"SELECT * FROM sources WHERE id = ? AND tenant_id IS NULL", (source_id,)
)
if not await cursor.fetchone():
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Grundquelle nicht gefunden")
before = dict(row)
await db.execute("DELETE FROM sources WHERE id = ?", (source_id,))
await db.commit()
await log_action(
db, admin, get_client_ip(request),
action="delete", resource_type="source", resource_id=source_id,
before=before,
)
@router.get("/tenant")
@@ -159,6 +184,7 @@ async def list_tenant_sources(
@router.post("/tenant/{source_id}/promote")
async def promote_to_global(
source_id: int,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
@@ -169,6 +195,7 @@ async def promote_to_global(
raise HTTPException(status_code=404, detail="Quelle nicht gefunden")
if row["tenant_id"] is None:
raise HTTPException(status_code=400, detail="Bereits eine Grundquelle")
before = dict(row)
if row["url"]:
cursor = await db.execute(
@@ -185,7 +212,13 @@ async def promote_to_global(
await db.commit()
cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,))
return dict(await cursor.fetchone())
after = dict(await cursor.fetchone())
await log_action(
db, admin, get_client_ip(request),
action="update", resource_type="source", resource_id=source_id,
before=before, after=after,
)
return after
@@ -267,6 +300,7 @@ async def discover_source_endpoint(
@router.post("/discover/add")
async def add_discovered_sources(
feeds: list[dict],
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
@@ -282,6 +316,7 @@ async def add_discovered_sources(
added = 0
skipped = 0
added_ids = []
for feed in feeds:
if not feed.get("url"):
continue
@@ -290,11 +325,12 @@ async def add_discovered_sources(
continue
domain = feed.get("domain", "")
await db.execute(
cur = await db.execute(
"""INSERT INTO sources (name, url, domain, source_type, category, status, added_by, tenant_id)
VALUES (?, ?, ?, 'rss_feed', ?, 'active', 'system', NULL)""",
(feed["name"], feed["url"], domain, feed.get("category", "sonstige")),
)
added_ids.append(cur.lastrowid)
existing_urls.add(feed["url"])
added += 1
@@ -315,6 +351,13 @@ async def add_discovered_sources(
added += 1
await db.commit()
if added_ids:
await log_action(
db, admin, get_client_ip(request),
action="create", resource_type="source",
after={"discovered_add": {"count": added, "ids": added_ids,
"domain": feeds[0].get("domain") if feeds else None}},
)
return {"added": added, "skipped": skipped}
@@ -394,6 +437,7 @@ class SuggestionAction(BaseModel):
async def update_suggestion(
suggestion_id: int,
action: SuggestionAction,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
@@ -478,6 +522,14 @@ async def update_suggestion(
(new_status, suggestion_id),
)
await db.commit()
await log_action(
db, admin, get_client_ip(request),
action="update", resource_type="source",
resource_id=suggestion.get("source_id"),
before={"suggestion_id": suggestion_id, "status": "pending"},
after={"suggestion_id": suggestion_id, "status": new_status,
"result_action": result_action},
)
return {"status": new_status, "action": result_action}