umbau 1.0
This commit is contained in:
@ -15,6 +15,8 @@ import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import asyncio
|
||||
|
||||
import aiosqlite
|
||||
|
||||
GENESIS_HASH = "0" * 64
|
||||
@ -25,6 +27,22 @@ def _entry_hash(prev_hash: str, ts: str, event_type: str, details_json: str) ->
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
# E1 (Umsetzungsauftrag_Sonnet5.md Teil E.1): Lesen von prev_hash und das
|
||||
# nachfolgende INSERT muessen atomar sein. Ohne Schutz koennen zwei
|
||||
# gleichzeitig laufende Aufrufe (z.B. zwei Sitzungen, die im selben Moment
|
||||
# starten/enden) denselben prev_hash lesen und beide darauf aufbauen -- die
|
||||
# Kette bricht dann dauerhaft, weil UPDATE/DELETE auf audit_log durch
|
||||
# DB-Trigger verboten sind (0001_initial.sql). Zwei Schutzschichten:
|
||||
# 1. Ein prozessweiter asyncio.Lock serialisiert alle Aufrufe innerhalb
|
||||
# dieses (einzigen) Event-Loops -- ausreichend, solange die Anwendung in
|
||||
# einem uvicorn-Worker mit einer globalen Verbindung laeuft (E.0/E.5).
|
||||
# 2. BEGIN IMMEDIATE/COMMIT als zweite, von der Prozesstopologie
|
||||
# unabhaengige Schutzschicht: falls doch einmal mehrere Verbindungen auf
|
||||
# dieselbe Datei schreiben, sorgt die IMMEDIATE-Schreibsperre dafuer,
|
||||
# dass SELECT und INSERT nicht auseinandergerissen werden.
|
||||
_write_lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def write_audit_event(
|
||||
conn: aiosqlite.Connection,
|
||||
*,
|
||||
@ -37,20 +55,33 @@ async def write_audit_event(
|
||||
|
||||
Muss innerhalb derselben Transaktion wie die fachliche Aktion laufen (oder
|
||||
zumindest unmittelbar danach), damit kein Ereignis unauditiert bleibt.
|
||||
|
||||
Lesen des prev_hash und das INSERT sind durch einen Lock plus eine
|
||||
BEGIN-IMMEDIATE-Transaktion atomar (siehe E1 in Umsetzungsauftrag_Sonnet5.md).
|
||||
"""
|
||||
cursor = await conn.execute("SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1")
|
||||
row = await cursor.fetchone()
|
||||
prev_hash = row[0] if row else GENESIS_HASH
|
||||
async with _write_lock:
|
||||
await conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
cursor = await conn.execute(
|
||||
"SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
prev_hash = row[0] if row else GENESIS_HASH
|
||||
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
details_json = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
||||
entry_hash = _entry_hash(prev_hash, ts, event_type, details_json)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
details_json = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
||||
entry_hash = _entry_hash(prev_hash, ts, event_type, details_json)
|
||||
|
||||
await conn.execute(
|
||||
"INSERT INTO audit_log (ts, user_id, client_ip, event_type, details_json, prev_hash, entry_hash) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(ts, user_id, client_ip, event_type, details_json, prev_hash, entry_hash),
|
||||
)
|
||||
await conn.execute(
|
||||
"INSERT INTO audit_log (ts, user_id, client_ip, event_type, details_json, prev_hash, entry_hash) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(ts, user_id, client_ip, event_type, details_json, prev_hash, entry_hash),
|
||||
)
|
||||
except BaseException:
|
||||
await conn.execute("ROLLBACK")
|
||||
raise
|
||||
else:
|
||||
await conn.execute("COMMIT")
|
||||
|
||||
|
||||
async def verify_chain(conn: aiosqlite.Connection) -> tuple[bool, int | None]:
|
||||
|
||||
Reference in New Issue
Block a user