5.0 KiB
Raw Blame History

name description version
audit-logging Use when 设计审计日志/审计跟踪审计独立性、owner.audit角色、防自删 1.0.0

审计日志设计(审计独立性 + 防自删)

为多租户/多用户系统设计全局审计日志模块。核心不是"记录日志",而是审计独立性 审计员独立于被审计者(管理员),防止管理员篡改/删除自己的操作记录。

核心原则

  1. 审计独立性:单独 owner.audit 角色,审计查看/备份/删除权限 audit 角色, owner.superuser 也无权superuser 不会自动放行审计路径)。审计员是独立角色,不是管理员。
  2. append-only:审计日志只 INSERT 不 UPDATE禁止修改已写记录。
  3. 防自删:删除操作留痕永不删除(见下)。

数据表

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 字段):

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 语句排除它:

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 排除留痕本身, 否则"删全部"会把刚写的留痕一起删掉。