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-03 02:49:25 +00:00
1 parent 0db51f8145
commit 8006a6fcba
4 files changed
+467 -121

No files matched your search

+56 -17
View File
@@ -40,6 +40,55 @@ _ACTION_MIN_ROLE = {
"manage_project": "owner",
}
# audit_log 建表 SQL(AuthStore.__init__ 与 audit_on_conn 共用同一
# 份,绝不能各写一份后漂移)。PG 适配层把其中的
# INTEGER PRIMARY KEY AUTOINCREMENT 翻译成 identity 列,见
# cloud/db.py;SQLite 下原样执行。
_AUDIT_LOG_TABLE_SQL = (
"CREATE TABLE IF NOT EXISTS audit_log("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"user_id TEXT, "
"project_id TEXT, "
"action TEXT, "
"detail TEXT, "
"created_at REAL)"
)
def audit_on_conn(conn, user_id, project_id, action, detail="",
created_at=None):
"""在调用方给定的连接上写一对审计记录(单次尝试)。
与 AuthStore.audit() 是同一对写:audit_log 行 + audit_ledger
链条目同 created_at(调用方不给时取当前时间),先建两张表
(幂等),再写两行。本函数不 commit、不 rollback、不重试:
事务边界属于调用方——调用方须在自己的事务里 commit,失败
时 rollback 整段业务写、链头唯一冲突时 rollback 后重试整段
(只重试本对写会丢掉调用方同事务里已写但未提交的业务行;
见 cloud/ledger.py append_entry 的警告)。AuthStore.audit()
即本函数 + “自有连接上 commit、唯一冲突重试 5 次”的包装。
存在理由:automations.run_due_automation 拿到的是调用方连接
(任务 INSERT 与产出记录都在该连接的事务里),不能像服务端
handler 那样另开 AuthStore(自有连接、自有事务)去写审计——
那样审计与任务不在同一事务,审计失败时任务已落盘,所谓
“整对回滚”只保审计对、不保业务对。本函数让自动化执行器
的审计行与任务行、产出记录行同事务提交、同事务回滚。
"""
if created_at is None:
created_at = time.time()
created_at = float(created_at)
conn.execute(_AUDIT_LOG_TABLE_SQL)
_ledger_ensure(conn)
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at)"
" VALUES(?, ?, ?, ?, ?)",
(user_id, project_id, action, detail, created_at),
)
_ledger_append(conn, user_id, project_id, action,
detail=detail, created_at=created_at)
class AuthStore:
"""基于 SQLite 的用户 / 项目成员 / 审计存储。"""
@@ -61,15 +110,7 @@ class AuthStore:
"role TEXT, "
"PRIMARY KEY(project_id, user_id))"
)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS audit_log("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"user_id TEXT, "
"project_id TEXT, "
"action TEXT, "
"detail TEXT, "
"created_at REAL)"
)
self.conn.execute(_AUDIT_LOG_TABLE_SQL)
# 防篡改账本(哈希链):audit_log 本身不动,链写进
# audit_ledger;建表失败即建库失败,不静默丢链。
_ledger_ensure(self.conn)
@@ -361,19 +402,17 @@ class AuthStore:
“audit_log 有行、链上无行”的静默缺口)。并发抢链头
导致唯一冲突时在事务边界重试整对写入,最多 5 次;
其它错误直接回滚抛错,不重试。
实现即 audit_on_conn() + 本连接上的 commit/重试包装,
对写本身与 #241 完全一致(同一函数、同一 created_at
语义),不是第二套审计写路径。
"""
now = time.time()
last_exc = None
for _attempt in range(5):
try:
self.conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at)"
" VALUES(?, ?, ?, ?, ?)",
(user_id, project_id, action, detail, now),
)
_ledger_append(self.conn, user_id, project_id, action,
detail=detail, created_at=now)
audit_on_conn(self.conn, user_id, project_id, action,
detail=detail, created_at=now)
self.conn.commit()
return
except Exception as exc: