fix(i-d): RBAC-Comprehensive 4 Failures behoben — http_exception_handler um dict-detail-Durchreichung erweitert (strukturierte Error-Codes AGENTS.md-konform, body[detail] = raw_detail dict statt stringify); 3 Contact-Payload-Feldnamen korrigiert (firstname/surname statt first_name/last_name in legacy-editor Tests); test_rbac_comprehensive 102/102 gruen
This commit is contained in:
@@ -92,6 +92,8 @@
|
|||||||
| E1-b | E1 Beweis: Dedizierter Test test_audit_middleware.py — POST auf /api/v1/saved-views (Route OHNE explizites log_audit) erzeugt Audit-Zeile mit source=middleware | ✅ Test grün; Regressionssmoke test_permissions+test_audit_middleware 23/23 grün; ruff clean; dabei log_audit-details-Schwäche entdeckt (details-Parameter wird nicht persistiert — nur changes) und Middleware entsprechend auf changes umgestellt | — |
|
| E1-b | E1 Beweis: Dedizierter Test test_audit_middleware.py — POST auf /api/v1/saved-views (Route OHNE explizites log_audit) erzeugt Audit-Zeile mit source=middleware | ✅ Test grün; Regressionssmoke test_permissions+test_audit_middleware 23/23 grün; ruff clean; dabei log_audit-details-Schwäche entdeckt (details-Parameter wird nicht persistiert — nur changes) und Middleware entsprechend auf changes umgestellt | — |
|
||||||
| E3-a | E3 Restore-Drill: Neues Skript scripts/restore_drill.sh — vollständiger lokaler Drill ohne Production-Zugriff: Migrations-DB+Seed → pg_dump → frische DB → Restore → Integritäts-Checks | ✅ DRILL_EXIT=0, alle 12 Checks bestanden: Tabellen-Parität 69=69, Alembic-Version-Parität 0142, RLS-Policies-Parität 57, tenant-scoped contacts-Parität, audit_log-Parität, RLS fail-closed mit restricted role (NOSUPERUSER NOBYPASSRLS sieht 0 Zeilen ohne Tenant), Policy-Rollen-Bindung an crm_api bewiesen; dabei 2 Test-Harness-Fallen behoben (Superuser bypassed RLS by design; uuidgen fehlt im Container) | — |
|
| E3-a | E3 Restore-Drill: Neues Skript scripts/restore_drill.sh — vollständiger lokaler Drill ohne Production-Zugriff: Migrations-DB+Seed → pg_dump → frische DB → Restore → Integritäts-Checks | ✅ DRILL_EXIT=0, alle 12 Checks bestanden: Tabellen-Parität 69=69, Alembic-Version-Parität 0142, RLS-Policies-Parität 57, tenant-scoped contacts-Parität, audit_log-Parität, RLS fail-closed mit restricted role (NOSUPERUSER NOBYPASSRLS sieht 0 Zeilen ohne Tenant), Policy-Rollen-Bindung an crm_api bewiesen; dabei 2 Test-Harness-Fallen behoben (Superuser bypassed RLS by design; uuidgen fehlt im Container) | — |
|
||||||
| E3-b | E3 CI-Integration: restore_drill.sh als automatisierbarer Drill (Exit-Codes 0/1, Cleanup via trap) für wöchentlichen Lauf | ✅ Skript ist idempotent (einzigartige DB-Namen pro Lauf via $$), räumt Temp-DBs selbst auf; Einbindung in CI/wöchentlichen Cron als Follow-up für Server-Admin dokumentiert | 81aea8c |
|
| E3-b | E3 CI-Integration: restore_drill.sh als automatisierbarer Drill (Exit-Codes 0/1, Cleanup via trap) für wöchentlichen Lauf | ✅ Skript ist idempotent (einzigartige DB-Namen pro Lauf via $$), räumt Temp-DBs selbst auf; Einbindung in CI/wöchentlichen Cron als Follow-up für Server-Admin dokumentiert | 81aea8c |
|
||||||
|
| E/I-D | Geister-Komponenten eliminiert + RBAC-Failures behoben: AIAssistant-Seite gebaut; 5 Ghost-Tabs entfernt; http_exception_handler um dict-detail-Durchreichung erweitert (strukturierte Error-Codes AGENTS.md-konform); 3 Contact-Payload-Feldnamen korrigiert | ✅ test_rbac_comprehensive **102/102 grün** (vorher 4 failed); tsc exit=0; Production-Build mit AIAssistant-Chunks; ruff clean ×6 Dateien | — |
|
||||||
|
|
||||||
| E6-a | E6 Secrets-Hygiene: docs/deploy-guide.md enthielt 7 echte Credentials im Klartext (Forgejo-Token, Coolify-Token, DB-Passwort, Redis-Passwort, SECRET_KEY, Admin-Passwort) — durch Git-Historie kompromittiert | ✅ Alle Werte entfernt und durch Secretstore-Referenzen ersetzt; Credential-Rotation-Anleitung mit konkreten Schritten für alle 7 Credentials ergänzt (Reihenfolge: SECRET_KEY zuletzt da Session-Invalidierung); Verifikation: 0 echte Credentials in der Datei; ⚠️ ROTATION MUSS VOM USER AUF SERVER-SEITE DURCHGEFÜHRT WERDEN | — |
|
| E6-a | E6 Secrets-Hygiene: docs/deploy-guide.md enthielt 7 echte Credentials im Klartext (Forgejo-Token, Coolify-Token, DB-Passwort, Redis-Passwort, SECRET_KEY, Admin-Passwort) — durch Git-Historie kompromittiert | ✅ Alle Werte entfernt und durch Secretstore-Referenzen ersetzt; Credential-Rotation-Anleitung mit konkreten Schritten für alle 7 Credentials ergänzt (Reihenfolge: SECRET_KEY zuletzt da Session-Invalidierung); Verifikation: 0 echte Credentials in der Datei; ⚠️ ROTATION MUSS VOM USER AUF SERVER-SEITE DURCHGEFÜHRT WERDEN | — |
|
||||||
| E2/E4/E5 | E2 E2E gegen Production-Build, E4 Monitoring-Reality-Check, E5 Performance-Baseline: Benötigen Server-/Deployment-Kontext (Coolify-Deploy, externes Alerting, Lasttest-Umgebung) | ⏳ Als Server-Admin-Follow-ups dokumentiert; lokale Vorbereitung (Playwright-Config mit BASE_URL, seed_perf_data.py, spike_e_benchmark.py) existiert bereits | — |
|
| E2/E4/E5 | E2 E2E gegen Production-Build, E4 Monitoring-Reality-Check, E5 Performance-Baseline: Benötigen Server-/Deployment-Kontext (Coolify-Deploy, externes Alerting, Lasttest-Umgebung) | ⏳ Als Server-Admin-Follow-ups dokumentiert; lokale Vorbereitung (Playwright-Config mit BASE_URL, seed_perf_data.py, spike_e_benchmark.py) existiert bereits | — |
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
from app.config import get_settings # noqa: E402
|
from app.config import get_settings # noqa: E402
|
||||||
from app.core.db import close_engine, get_engine # noqa: E402
|
from app.core.db import close_engine, get_engine # noqa: E402
|
||||||
from app.core.error_codes import ApiError, build_error_response # noqa: E402
|
from app.core.error_codes import ERROR_CODES, ApiError, build_error_response # noqa: E402
|
||||||
from app.core.middleware import ( # noqa: E402
|
from app.core.middleware import ( # noqa: E402
|
||||||
AuditMiddleware,
|
AuditMiddleware,
|
||||||
CSRFMiddleware,
|
CSRFMiddleware,
|
||||||
@@ -519,11 +519,26 @@ def create_app() -> FastAPI:
|
|||||||
504: "service_timeout",
|
504: "service_timeout",
|
||||||
}
|
}
|
||||||
code = status_to_code.get(exc.status_code, "internal_error" if exc.status_code >= 500 else "validation_error")
|
code = status_to_code.get(exc.status_code, "internal_error" if exc.status_code >= 500 else "validation_error")
|
||||||
body = build_error_response(
|
# Structured detail passthrough (AGENTS.md): when a route raises
|
||||||
code=code,
|
# HTTPException with a dict detail containing a machine-readable ``code``,
|
||||||
detail=str(exc.detail) if exc.detail else None,
|
# preserve the structured shape instead of stringifying it.
|
||||||
trace_id=trace_id,
|
raw_detail = exc.detail
|
||||||
)
|
if isinstance(raw_detail, dict):
|
||||||
|
inner_code = raw_detail.get("code", code)
|
||||||
|
body = build_error_response(
|
||||||
|
code=inner_code if inner_code in ERROR_CODES else code,
|
||||||
|
detail=raw_detail.get("detail") or str(raw_detail),
|
||||||
|
trace_id=trace_id,
|
||||||
|
)
|
||||||
|
# Preserve the full structured detail as a nested object so clients
|
||||||
|
# can read ``resp.json()["detail"]["code"]``.
|
||||||
|
body["detail"] = raw_detail
|
||||||
|
else:
|
||||||
|
body = build_error_response(
|
||||||
|
code=code,
|
||||||
|
detail=str(exc.detail) if exc.detail else None,
|
||||||
|
trace_id=trace_id,
|
||||||
|
)
|
||||||
resp = JSONResponse(status_code=exc.status_code, content=body)
|
resp = JSONResponse(status_code=exc.status_code, content=body)
|
||||||
if trace_id:
|
if trace_id:
|
||||||
resp.headers["X-Trace-Id"] = trace_id
|
resp.headers["X-Trace-Id"] = trace_id
|
||||||
|
|||||||
@@ -926,7 +926,7 @@ class TestRBACRouteGuard:
|
|||||||
csrf = await login_with_csrf(client, "viewer@tenanta.com")
|
csrf = await login_with_csrf(client, "viewer@tenanta.com")
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/api/v1/contacts",
|
"/api/v1/contacts",
|
||||||
json={"first_name": "Test", "last_name": "User", "type": "person"},
|
json={"firstname": "Test", "surname": "User", "type": "person"},
|
||||||
headers=csrf_headers(csrf),
|
headers=csrf_headers(csrf),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
@@ -1071,7 +1071,7 @@ class TestRBACRouteGuard:
|
|||||||
csrf = await login_with_csrf(client, "editor@tenanta.com")
|
csrf = await login_with_csrf(client, "editor@tenanta.com")
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/api/v1/contacts",
|
"/api/v1/contacts",
|
||||||
json={"first_name": "Editor", "last_name": "Contact", "type": "person"},
|
json={"firstname": "Editor", "surname": "Contact", "type": "person"},
|
||||||
headers=csrf_headers(csrf),
|
headers=csrf_headers(csrf),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
@@ -1085,7 +1085,7 @@ class TestRBACRouteGuard:
|
|||||||
csrf = await login_with_csrf(client, "viewer@tenanta.com")
|
csrf = await login_with_csrf(client, "viewer@tenanta.com")
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/api/v1/contacts",
|
"/api/v1/contacts",
|
||||||
json={"first_name": "Viewer", "last_name": "Contact", "type": "person"},
|
json={"firstname": "Viewer", "surname": "Contact", "type": "person"},
|
||||||
headers=csrf_headers(csrf),
|
headers=csrf_headers(csrf),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
|
|||||||
Reference in New Issue
Block a user