--- name: audit-logging description: Use when 设计审计日志/审计跟踪(审计独立性、owner.audit角色、防自删)。 version: 1.0.0 --- # 审计日志设计(审计独立性 + 防自删) 为多租户/多用户系统设计全局审计日志模块。核心不是"记录日志",而是**审计独立性**: 审计员独立于被审计者(管理员),防止管理员篡改/删除自己的操作记录。 ## 核心原则 1. **审计独立性**:单独 `owner.audit` 角色,审计查看/备份/删除权限**仅** audit 角色, `owner.superuser` 也无权(superuser 不会自动放行审计路径)。审计员是独立角色,不是管理员。 2. **append-only**:审计日志只 INSERT 不 UPDATE,禁止修改已写记录。 3. **防自删**:删除操作留痕永不删除(见下)。 ## 数据表 ```sql CREATE TABLE IF NOT EXISTS sd_audit_logs ( id varchar(32) NOT NULL, user_id varchar(32), username varchar(100), action varchar(50) NOT NULL, -- 事件白名单 target varchar(200), detail text, -- 操作对象 + 详情 result varchar(10), client_ip varchar(64), created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_action (action), KEY idx_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## owner.audit 角色 + 判断 role 表 `id='owner.audit', orgtypeid='owner', name='audit'`(`owner.superuser` 同理,id 直接是 字符串 `'owner.superuser'`,不是随机 ID)。初始化幂等:`INSERT IGNORE INTO role (id,orgtypeid,name) VALUES ('owner.audit','owner','audit')`。 判断用户是否审计员查 **userrole 表**(users 表无 role 字段): ```sql SELECT 1 FROM userrole WHERE userid=${u}$ AND roleid='owner.audit' LIMIT 1 ``` ## 双保险权限(RBAC 层 + 应用层,缺一不可) 审计 API 要两层都卡,因为独立 app 的 RBAC 可能对**未注册路径放行**(实测新 .dspy 未注册 也能访问): 1. **RBAC 层**:load_path.py 加 `PATHS_AUDIT` 挂 owner.audit(不给 logined/superuser)。 set_role_perm.py 角色名用 `orgtypeid.name` 格式,即 `owner.audit`。 2. **应用层**:审计 .dspy 内部再查 userrole 校验 `is_audit_role()`,不依赖 RBAC 是否生效。 应用层校验是最终保障,即使 RBAC 放行或缓存未刷新也拦得住。 ## 防自删(删除留痕永不删) 删除操作先写 `audit_delete` 记录,且 DELETE 语句排除它: ```python async def delete_audit_logs(sor, user_id, username, before="", client_ip=""): # 排除 audit_delete 记录,保证删除留痕不被删 wsql = (" WHERE created_at<${b}$ AND action != 'audit_delete'" if before else " WHERE action != 'audit_delete'") await audit_log(sor, user_id, username, "audit_delete", target="sd_audit_logs", detail="删除 before=" + (before or "全部"), client_ip=client_ip) # 先留痕 recs = await sor.sqlExe("SELECT COUNT(*) AS c FROM sd_audit_logs" + wsql, ns) await sor.sqlExe("DELETE FROM sd_audit_logs" + wsql, ns) ``` audit_delete 记录累积是审计的代价,可接受(那正是"删除证据"本身)。 ## 审计事件清单(action 白名单) 用集合白名单校验 action,非白名单归 `unknown`: - 认证:login / login_fail / logout - 权限:role_change / perm_change / user_role_change - 工作环境:work_env_set / org_key_gen / remote_bwrap(远程主机变更是高风险) - 部署账号:account_create / account_remove / sandbox_run - 用户机构:user_create / user_disable / user_delete / org_change - 审计自身:audit_delete / audit_backup ## 审计写入的接入点 写入是"系统自动"(各模块关键操作时调用 `audit_log()`),不是用户触发。审计写入失败 不应阻断主流程(try/except 吞掉,仅 logger 记录)。关键接入点:RBAC 变更、工作环境/ 远程主机变更、账号生命周期、高危命令执行、登录成败。 ## Pitfalls - **client_ip 是可信的(勿误判为可伪造)**:ahserver `real_ip.py` 中间件 `split(',')[-1]` 取 X-Forwarded-For 最后一段;nginx 用 `$proxy_add_x_forwarded_for` **追加**真实 IP(非透传 `$http_x_forwarded_for`),且 `X-real-ip $remote_addr` 覆盖。故末段恒是 nginx 追加的真实客户端 IP,客户端伪造的 XFF 只出现在前段。**前提**:生产必须经 nginx(`$proxy_add_x_forwarded_for`);若直连应用端口(无 nginx)才可伪造。 - **information_schema 查询必须限定 `table_schema=DATABASE()`**:不限定会查到别的库的同名列 (如把 sage 库的 `need_audit` 列误当成当前库的,随后 SQL 报 Unknown column)。 - **审计独立性要实测**:撤销 audit 角色后,用 superuser 账号 curl 审计 API,确认返回拒绝—— 只靠 RBAC 注册不够,必须验证应用层 `is_audit_role` 真的拦住了 superuser。 - **审计写入与删除的次序**:删除留痕必须在 DELETE 之前写,且 DELETE 排除留痕本身, 否则"删全部"会把刚写的留痕一起删掉。