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:
@@ -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}
|
||||
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren