audit: route automation audit rows through hash-chain pair (gap 4, option A)

Bypass closed: automations._audit_row (direct audit_log INSERT,
best-effort swallow) is deleted. run_due_automation now writes via
cloud.auth.audit_on_conn, the same audit_log + audit_ledger pair
AuthStore.audit() writes, sharing created_at. The audit pair runs
before record_automation_run's commit, so task row, run row and
audit pair commit or roll back together; chain-head IntegrityError
retries the whole block up to 5 times, other errors roll back and
raise (fail-closed). The claim stays committed, so a failed run is
retried next interval, never recorded as a failed run.
AuthStore.audit() now delegates to audit_on_conn; its pair, retry
count and fail-closed semantics are unchanged.
This commit is contained in:
host committed 2026-11-04 20:56:06 +00:00
1 parent d303dedf32
commit f83bd79525
4 files changed
+467 -121

No files matched your search

+61 -19
View File
@@ -5,13 +5,14 @@ import tempfile
import time
import unittest
from cloud.auth import audit_on_conn
from cloud.automations import (
_audit_row,
create_automation,
ensure_automations_tables,
list_automation_runs,
run_due_automation,
)
from cloud.ledger import GENESIS_HASH, compute_entry_hash
from cloud.server import init_db
@@ -49,25 +50,48 @@ def _set_quota(db_path, user_id, micros):
conn.close()
class AuditRowPgIdentityTestCase(unittest.TestCase):
class AuditPairPgIdentityTestCase(unittest.TestCase):
"""模拟 PG:id 是 BIGINT identity,显式插 TEXT uuid 会报错。
假连接只接受不带 id 的 INSERT;旧实现(带 TEXT id)在此
假连接上应失败、新实现应成功且审计行落盘。
假连接只接受不带 id 的 INSERT;旧 _audit_row(带 TEXT id)
在此假连接上应失败。接替它的 audit_on_conn 须同时做到:
audit_log 行不带 id、链条目与该行同 created_at、空链时
prev 为 GENESIS、entry_hash 按规范载荷重算一致;且本函数
不 commit(事务属于调用方,见 run_due_automation 文档)。
假连接类名须为 PgConnection:ledger.ensure_schema 靠类名
判定 PG 连接以跳过 SQLite 触发器,与真实 PG 适配层同款。
"""
class _PgLikeConn:
class _FakeCursor:
def __init__(self, row=None):
self._row = row
self.lastrowid = 1
def fetchone(self):
return self._row
class PgConnection:
def __init__(self):
self.rows = []
self.audit_rows = []
self.ledger_rows = []
self.committed = False
def execute(self, sql, params=()):
if sql.strip().upper().startswith("INSERT INTO AUDIT_LOG(ID"):
upper = sql.strip().upper()
if upper.startswith("INSERT INTO AUDIT_LOG(ID"):
raise ValueError("column \"id\" is of type bigint")
if sql.strip().upper().startswith("INSERT INTO AUDIT_LOG"):
self.rows.append(params)
return None
raise ValueError("unexpected sql in fake PG conn")
if upper.startswith("INSERT INTO AUDIT_LOG"):
self.audit_rows.append(params)
return AuditPairPgIdentityTestCase._FakeCursor()
if upper.startswith("SELECT ENTRY_HASH FROM AUDIT_LEDGER"):
return AuditPairPgIdentityTestCase._FakeCursor(None)
if upper.startswith("INSERT INTO AUDIT_LEDGER"):
self.ledger_rows.append(params)
return AuditPairPgIdentityTestCase._FakeCursor()
if upper.startswith("CREATE TABLE"):
return AuditPairPgIdentityTestCase._FakeCursor()
raise ValueError("unexpected sql in fake PG conn: %r" % sql)
def commit(self):
self.committed = True
@@ -75,15 +99,33 @@ class AuditRowPgIdentityTestCase(unittest.TestCase):
def rollback(self):
pass
def test_insert_without_id_succeeds_on_pg_like_conn(self):
conn = self._PgLikeConn()
_audit_row(conn, "p1", "automation_run_created",
"auto1 -> task1", 123.0)
self.assertTrue(conn.committed)
self.assertEqual(len(conn.rows), 1)
def test_pair_without_id_succeeds_on_pg_like_conn(self):
conn = self.PgConnection()
audit_on_conn(conn, "system", "p1", "automation_run_created",
"auto1 -> task1", created_at=123.0)
# 调用方拥有事务:本函数自己不 commit。
self.assertFalse(conn.committed)
self.assertEqual(len(conn.audit_rows), 1)
# 参数里没有 uuid:第一位是 system,第二位是 project_id。
self.assertEqual(conn.rows[0][0], "system")
self.assertEqual(conn.rows[0][1], "p1")
self.assertEqual(conn.audit_rows[0][0], "system")
self.assertEqual(conn.audit_rows[0][1], "p1")
self.assertEqual(conn.audit_rows[0][4], 123.0)
self.assertEqual(len(conn.ledger_rows), 1)
ledger = conn.ledger_rows[0]
# audit_ledger 列序:user_id, project_id, task_id, action,
# detail, created_at, prev_hash, entry_hash。
self.assertEqual(ledger[0], "system")
self.assertEqual(ledger[1], "p1")
self.assertIsNone(ledger[2])
self.assertEqual(ledger[3], "automation_run_created")
self.assertEqual(ledger[4], "auto1 -> task1")
self.assertEqual(ledger[5], 123.0)
self.assertEqual(ledger[6], GENESIS_HASH)
self.assertEqual(
ledger[7],
compute_entry_hash(GENESIS_HASH, "system", "p1", None,
"automation_run_created",
"auto1 -> task1", 123.0))
class RunDueAutomationTestCase(unittest.TestCase):