fix(governance): 补 db_guard 守卫到 3 处 L2 破坏性操作入口 (#227)
Some checks failed
backend-test / commit-message-lint (push) Has been cancelled
backend-test / backend-lint-test (push) Has been cancelled
backend-test / frontend-lint-test (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled

R-P1-11: 补齐 db_guard 运行时缺口

破坏性操作守卫补齐(AGENTS.md 绝对禁止第3条):
1. governance_execute_service._backup_rows 入口
   DROP TABLE IF EXISTS gov_bak_* (line 441)
2. governance_execute_service._exec_delete_dups 入口
   DELETE FROM {tbl} WHERE ctid IN (...) (line 477)
3. governance_execute_service._exec_fix_time 入口
   批量 UPDATE "{table}" SET "{col}" (line 667)
4. log_retention_service.purge_table 入口
   DELETE FROM {table} WHERE ctid IN (line 56)

实现:
- from app.core.db_guard import assert_safe_to_destroy
- 各函数入口(任何 execute 前)调 assert_safe_to_destroy(db, reason=...)
- reason 字符串含表名/操作类型便于追踪
- 测试库(_test 后缀)放行; 真实库无 allow_real_db 拦截

测试:
- test_test_db_guard.py 7 测试通过(守卫本身行为)
- governance 非DB测试 35 通过(无回归)
- L2 真实流程需DB, 待部署后生产验证
This commit is contained in:
2026-10-08 11:19:21 +08:00
parent 699c7bda08
commit ee804fa3d7
2 changed files with 11 additions and 0 deletions

View File

@@ -38,6 +38,7 @@ from typing import Any
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.db_guard import assert_safe_to_destroy # B1-2(#227): L2 破坏性操作守卫
from app.models.governance_finding import (
FINDING_STATUS_AUTO_CLOSED,
FINDING_STATUS_JUDGED,
@@ -438,6 +439,8 @@ def _backup_rows(db: Session, table: str, finding_id: int, pick_sql: str,
_assert_ident(table, "表名")
bak = f"{_BACKUP_PREFIX}_{table}_{finding_id}"
_assert_ident(bak, "备份表名")
# B1-2(#227): DROP 属破坏性操作, 必须先过 db_guard 守卫
assert_safe_to_destroy(db, reason=f"governance _backup_rows DROP gov_bak_{table}_{finding_id}")
db.execute(text(f'DROP TABLE IF EXISTS {_quote(bak)}'))
db.execute(text(f'CREATE TABLE {_quote(bak)} AS SELECT * FROM {_quote(table)} '
f'WHERE ctid IN (\n{pick_sql}\n)'), params or {})
@@ -449,6 +452,8 @@ def _backup_rows(db: Session, table: str, finding_id: int, pick_sql: str,
def _exec_delete_dups(db: Session, f: GovernanceFinding) -> dict:
"""L2 删重复行:判据同源 → dry-run 对数 → 备份 → 删除 → 前后对比。"""
# B1-2(#227): DELETE 属破坏性操作, 必须先过 db_guard 守卫
assert_safe_to_destroy(db, reason=f"governance _exec_delete_dups DELETE FROM {f.table_name}")
if f.detector != "unique_index_dup":
raise ExecuteError(
f"delete_dups 仅适用于 unique_index_dup 探测器(当前 {f.detector});"
@@ -599,6 +604,8 @@ def _exec_fix_time(db: Session, f: GovernanceFinding, *, params: dict | None) ->
③ **备份先行且行数必须相等**:备份行数 != 待改行数 → 中止(备份不完整不得改)。
表名/列名走 `_assert_ident` 白名单,值一律绑定参数(AGENTS.md 绝对禁止第 2 条)。
"""
# B1-2(#227): 批量 UPDATE 属破坏性操作, 必须先过 db_guard 守卫
assert_safe_to_destroy(db, reason=f"governance _exec_fix_time UPDATE {f.table_name}")
if f.detector != "impossible_date":
raise ExecuteError(
f"fix_time 仅适用于 impossible_date 探测器(当前 {f.detector});"

View File

@@ -21,6 +21,8 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.db_guard import assert_safe_to_destroy # B1-2(#227): 破坏性 DELETE 守卫
logger = logging.getLogger(__name__)
# 表名 → 保留天数。表名是代码内置白名单(不接受外部传入), 杜绝 SQL 注入。
@@ -48,6 +50,8 @@ def purge_table(db: Session, table: str, days: int,
用 ctid 子查询限定每批行集(PostgreSQL), 每批独立提交, 不持有长事务。
"""
# B1-2(#227): 批量 DELETE 属破坏性操作, 必须先过 db_guard 守卫
assert_safe_to_destroy(db, reason=f"log_retention purge_table DELETE {table}")
if table not in RETENTION_DAYS:
raise ValueError(f"不在保留期白名单内的表: {table}")
threshold = datetime.now(timezone.utc) - timedelta(days=days)