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:
1 parent
0db51f8145
commit
8006a6fcba
4 files changed
+467
-121
No files matched your search
+56
-17
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user