fix(governance): 补 db_guard 守卫到 3 处 L2 破坏性操作入口 (#227)
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:
@@ -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});"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user