M2 系统管理实施记录
# M2 系统管理实施记录
更新于 2026-10-23。M2 已完成。部门、岗位、用户、多角色与按权限绑定的数据范围已实现并通过自动验证;租户安全策略、账号解锁和口令有效期已接入;审计与登录日志查询已接入;用户于 2026-10-23 明确确认“M2人工验收成功”,原独立人工门槛据此结项。返回路线图。
# 执行顺序
2026-10-22 用户曾明确要求“先不要人工,先记住,继续往下走”,因此独立推进了 M2。2026-10-23 用户进一步确认“M1 已经人工验收”,M1 原人工门槛现按用户确认结项,见 M1 验收记录。M1 确认只覆盖 M1;随后用户独立明确确认“M2人工验收成功”,据此完成 M2 新建账号、首次改密/重置、实际授权及生效、安全策略写入与账号解锁等人工项的结项。证据来源为用户直接确认,记录于 docs/evidence/m2-manual-acceptance.json;本次没有新增代理浏览器观测,不推断浏览器、账号、证书或数据库环境细节。
# 已实现的部门与岗位
- 部门树、创建、修改、移动、删除;根节点
parentId: null,ID 为正 int64 十进制字符串。整棵树最多 10000 个有效节点、64 层,容量或结构异常明确失败,不返回截断树。 - 岗位分页、名称/编码搜索、状态筛选、创建、编辑、启停和删除;
pageSize为 1–100,pageNo为 1–1000000。搜索中的%、_按普通字符处理。编码只允许小写 ASCII 字母开头及小写字母、数字、下划线、短横线,长度最多 64,创建后不能修改。 - Vben 真实部门树页面和岗位表格/编辑弹窗;按钮使用服务端同名权限码,错误保留编辑内容,写入失败不自动重试。异步读取只发布最后一次请求,切换身份或页面卸载后,旧结果不能更新新页面或触发旧页面副作用。
- 本地组件键、精确路径和 realm 成对登记:
/system/departments/index→/system/departments,/system/posts/index→/system/posts。未登记组件、路径错配、保留路径和平台跨入租户区域均拒绝注册。
# 迁移、隔离与事务
组织基础由 schema v5 的两方言 0005_organization.sql 引入,当前 schema 为 v9,继续追加 0006_users_roles.sql、0007_security_policy.sql、0008_security_navigation.sql 和 0009_audit_queries.sql;旧迁移历史字节不变。新增部门、岗位、用户岗位关联与组织操作审计表,以及兼容已有用户的 nullable department_id。同租户归属由 service 查询条件和 (tenant_id,id) 复合外键双重保证,平台身份无业务数据通道。在线管理必须具有主库权限快照;仅有离线 bootstrap Actor 不授予管理权限。
每个组织/岗位写事务先 SELECT tenants ... FOR UPDATE,再检查父链、有效标记、引用及 expected version,再修改业务行、递增 authz_version 并写入白名单审计,统一提交或回滚。租户锁让 A→B/B→A 的并发移动无法各自通过检查后构成环。用户归属、岗位/角色关联与授权修改复用相同租户锁协议。涉及凭证的管理写入按租户→用户 ID 升序→操作者会话加锁,再核对有效会话、权限和版本,避免与身份服务的账号→会话→token 锁序形成反向等待。
部门存在有效子部门或任意用户引用时不能删除;禁用用户仍视为引用。岗位存在用户关联时不能删除。软删除设置 deletion_marker = id 和 deleted_at,保留历史;岗位编码可以在删除后重建。MySQL 不允许 CHECK 引用自增 id,因此树防环和删除标记不变量由服务锁协议保证;PostgreSQL 另保留对应 CHECK。两库均保留版本/状态 CHECK 及复合外键。
更新、删除必须携带当前版本。竞争只允许一方成功,另一方返回 HTTP 409 / 409001。409002 表示编码重复,409003 表示引用等业务状态冲突,409004 表示部门树结构或容量/深度冲突;内部 SQL/Oops 错误链不对外输出。
组织审计只序列化本模块构造的部门/岗位 DTO,不复制用户实体、密码、凭证或请求正文。审计保留服务端 requestId、TCP 来源 IP、操作人与前后值;审计失败回滚业务和授权版本。用户差异现已使用白名单 DTO 并对姓名脱敏;查询出口再次执行差异白名单投影,保留规则见下文。
# API 与生产加密
以下八个逻辑端点已实现,不再是仅合同:
| 方法 | 路径(前缀 /api/admin/v1/system) | 权限 |
|---|---|---|
| GET | /departments/tree | system:dept:read |
| POST | /departments | system:dept:create |
| PUT | /departments/{id} | system:dept:update |
| DELETE | /departments/{id}?version=... | system:dept:delete |
| GET | /posts | system:post:read |
| POST | /posts | system:post:create |
| PUT | /posts/{id} | system:post:update |
| DELETE | /posts/{id}?version=... | system:post:delete |
所有接口复用主库认证、受限会话拒绝、权限快照、精确 Origin、X-App-Request 和统一响应出口。删除正文要求空 JSON 对象。生产请求把 query/body 都放入 JWE;GET 使用 X-Fago-Payload,写请求使用 application/jose。成功、来源错误、权限错误及业务冲突响应均加密。开发/测试默认明文,不改变登录与刷新策略。未知/重复查询参数与漏传 parentId 会被拒绝。
schema v5 为受保护 tenant_admin 增加上述八个权限和两个菜单;v6 继续发布用户、角色、安全和审计权限及用户/角色菜单。普通用户仍无默认权限,租户 custom 上限继续与角色权限取交集。
# 用户、角色与按权限绑定的数据范围
schema v6 新增 system_roles、system_user_roles、system_role_permissions、system_role_menus;所有关联都有同租户复合外键,权限/菜单限定为已发布的 admin audience。已有用户、口令散列、密码版本及会话原样保留,迁移回填保留角色和旧自定义角色关系。登录及授权快照只读真实多角色关联,不再用 users.role_code 回退授予权限;该旧列仅保留兼容和诊断用途。
用户 API 与 Vben 页面支持分页、用户名/姓名搜索、状态/部门筛选、新增/编辑/启停/软删除、密码重置、岗位和多角色关联。列表、count、详情、写入和关联入口共用同一租户及数据范围条件;范围外 ID 返回 404。用户名固定小写,新增账号没有角色且必须首次改密。删除解除组织与角色关联并撤销全部会话;同名重建使用新 ID,旧凭证不能复活。账号管理 DTO 不返回散列、密码或内部锁定信息。
角色页面支持分页、创建、名称编辑、删除及菜单/权限/数据范围完整授权。新角色默认为空权限 SELF,不因为创建角色而产生访问权。四个保留角色不能普通编辑、删除或修改授权:tenant_admin、tenant_sys_admin、tenant_security_admin、tenant_audit_admin。具有 system:role:read 的用户可读角色元数据;角色写入、授权和用户角色分配仍仅限受保护的 tenant_admin,不能靠自定义角色码或客户端字段冒充。
授权菜单使用稳定目录键 menuIds,同时校验所需读取权限;未知目录、平台权限和租户上限以外的授权拒绝。用户角色/岗位全量替换最多 100 项,禁止重复和跨租户 ID。读取关联返回 {items, version},写入带最新版本;角色授权读取返回当前 version,写入同事务更新菜单、权限、范围、版本、租户授权版本和脱敏审计。写失败不重试覆盖。
数据范围按“角色 + 具体权限”逐项求并集:SELF 为本人,DEPT 为请求时有效的本部门(不含下级,无部门为空),ALL 仍限定当前租户。SELF 读取角色与另一个只有 ALL 写入的角色不能拼出 ALL 读取。修改部门同时检查旧记录和新部门;SELF 用户不能移动自己来扩大 DEPT 范围。
重置他人密码、用户角色分配和角色授权写入在业务事务内重新校验操作者有效会话、当前角色/权限及 operatorPassword。输入错误返回 403005,失败次数、锁定和审计独立提交,目标业务保持不变;达到阈值撤销操作者会话。正确再鉴别不会跳过版本、最后管理员或租户上限检查。至少保留一个启用账号关联启用的保留 tenant_admin;并发停用/清空角色也不能绕过此不变量。
用户及角色的 20 个逻辑端点已接入与组织接口相同的认证、Origin、严格 JSON、JWE 请求/响应链。状态修改、关联保存和授权保存成功返回 data: null,编辑器随后重新读取当前结果。浏览器新口令输入和实际授权扩展当时暂缓;现按 2026-10-23 用户独立人工验收确认结项,自动测试与人工确认分别记录。
# 租户安全策略、解锁与口令有效期
schema v7 新增 tenant_security_policies,现有租户由迁移回填继承行,新租户在 bootstrap 同一事务中初始化;v8 发布独立安全策略菜单。配置使用 inherit/custom 完整模式和独立版本,不支持逐字段混合继承。缺行、坏配置或未知模式拒绝为 503,不能退回更松的默认。平台原始未生效的 0 天种子在激活迁移中设为 90 天,已有非零有效期和其他策略值保留;启用后仍可由安全管理员显式覆盖为 0 天。
登录、刷新、访问认证、权限快照、本人改密、管理新建/重置和特权再鉴别统一读取租户有效策略;平台身份独立读取平台默认。改密在等待账号/会话锁后重新校验有效策略,管理口令继续保留至少 14 字符的下限。策略缩短 idle 后根据上次活动时间拒绝仍持有较长旧窗口的会话,失败请求不会重新续期。
自然过期在 90 天精确边界触发,历史缺失或未来改密时间不能被当成刚改密。正确口令仍可进入受限改密,失败计数不增加;已有正常 access 或 refresh 也会在锁内转成受限会话并撤销其他旧会话,留下到期审计。转换后的绝对期限最多 30 分钟,刷新不延长;成功改密更新真实改密时间、密码版本并撤销全部旧凭证。关闭自然过期不会解除已经持久化的强制改密。
租户 API 新增:
| 方法 | 路径(前缀 /api/admin/v1/system) | 权限/行为 |
|---|---|---|
| GET | /security-policy | system:security-policy:read,返回模式、版本、生效值及平台默认 |
| PUT | /security-policy | system:security-policy:update,最新 version 与 operatorPassword;同事务更新和审计 |
| GET | /security-users | system:user:unlock,独立分页/关键词目录与该权限的数据范围,姓名脱敏 |
| POST | /users/{id}/unlock | system:user:unlock,version 与 operatorPassword;清除锁定/失败计数并撤销旧会话 |
覆盖模式必须显式提交全部六项规则,漏传/传 null 的有效期不能意外变成 0。继承模式不接受隐藏覆盖值。参数范围:长度 8–64 字符,有效期 0–365 天,失败阈值 1–20 次,锁定/idle 1–1440 分钟;复杂度为 off/three_of_four。策略写入与解锁均使用同事务本人再鉴别,安全管理员无需用户 CRUD 或角色授权权限,也不能因此更改停用状态、密码、角色或强制改密标记。解锁后旧会话必须失效,不能因清除锁定使凭证复活。
真实 Vben 页面显示来源与生效值、最新版本编辑器和独立脱敏解锁目录;取消、失败、完成或卸载时清空本人口令。HTTP 浏览器核对了规则展示、继承/覆盖表单和取消;本轮未通过页面保存安全规则、执行解锁或输入新口令。

# 审计、登录日志和持续保留
schema v9 只追加登录事件的 user_agent 与 account_fingerprint,以及租户/时间/requestId 索引和两个菜单;不改写已有事件。旧字段为空时显示未知,迁移不会补造旧 UA、账号或时间。新增 UA 仅保存浏览器家族加 SHA256,已知账号按 realm/tenant/user ID 计算指纹;未知账号用规范化登录标识摘要关联失败事件,不保存登录名原文。摘要用于关联,不作为认证因素。
| 方法 | 路径(前缀 /api/admin/v1/system) | 独立权限 |
|---|---|---|
| GET | /audit-logs | system:audit-log:read |
| GET | /login-logs | system:login-log:read |
两页提供精确 requestId、动作、主体 ID、起止时间筛选,标准分页、总数及只读详情。操作记录主体为 actor_id,身份记录主体为 user_id;业务变更目标 entity_id 不决定可见范围。SELF 仅本人事件,DEPT 按有效主体当前部门,无部门或已删除主体不匹配,ALL 保留本租户未知及已删除主体历史。登录事件固定 admin realm,平台账号无租户入口。两个权限按各自角色范围计算,不能拼出更大权限。
同一可重复读事务先校验主库权限,再读 count 和有序分页,最后追加 system:audit-query:read。查询留痕只包含操作人、时间、requestId、来源、日志类别和条件摘要,不复制筛选正文;追加失败返回 503 且不返回已读数据。本次查询不进入当前结果,不递增 authz_version,避免查询引发权限导航刷新循环。页面详情仅使用本页授权投影,无任意 ID 读取、修改或删除入口。
姓名再次脱敏,IP 只返回 IPv4 /24 或 IPv6 /64 网络,session_id 只返回摘要。历史差异按实体/动作逐字段投影;未知字段丢弃,异常 JSON、超限、非法类型和未知实体仅显示 redacted 标记,不回退原文。JSON 使用文本插值展示,不能执行历史内容中的 HTML。时间以 UTC 毫秒存储和 RFC3339 传输,当前未实现租户时区配置,页面明确显示并使用浏览器时区输入/展示。
当前保留策略为持续保留:运行代码没有按时间删除、覆盖或自动归档审计的任务,不提供租户管理员清理接口;超过六个月的历史仍保留并可按授权查询。未来归档必须独立立项,具备专用平台权限、完整校验、可恢复备份及归档行为审计后才能启用。M4 部署验收须限制应用数据库账号对审计表的 UPDATE/DELETE 权限、落实磁盘容量监控、备份和恢复演练;当前开发库账号权限不代表生产防篡改或测评认证。
双库审计测试覆盖 SELF/DEPT/ALL、跨租户/realm、已删除/未知主体、精确条件和分页一致性、恶意筛选字面量、查询失败关闭、授权版本不变、差异脱敏和指纹稳定;v8 升级测试保留旧事件内容及空未知字段。


# 已执行的自动验证
- MySQL 8.4.13、PostgreSQL 16.6:全量 race 集成通过,含身份、组织、用户/角色、迁移和工作流已有回归;空库、v1、v4、v5、v6、v8 有数据升级均通过,确认历史账号/散列/版本/会话保留及角色回填。
- 双库用户/角色回归覆盖跨租户及复合外键、同版本竞争、权限即时撤回、SELF/DEPT/ALL 的列表/count/详情/写入/关联边界、保留角色与租户权限上限、最后管理员并发保护、再鉴别错误持久化、重置与 refresh 并发、删除重建旧凭证失效、事务审计脱敏。
- 每库明文/JWE 各 82 个真实 HTTP 响应,双库共 328 个;解密后的逻辑响应全部通过发布 OpenAPI 校验(加密 164、明文 164)。CI 集成任务会采集经敏感检查的响应再执行
scripts/check-live-contracts.mjs,不记录请求或凭证。 - 策略双库回归覆盖继承/覆盖、缺行拒绝、租户隔离、策略 CAS 竞争、审计失败回滚、再鉴别按租户阈值锁定、已有 idle 窗口缩短、独立安全管理员目录/解锁及越界拒绝;口令到期覆盖 access/refresh/login、未知/未来时间、显式关闭、平台身份、受限期限不续长和改密后旧凭证失效。
- 部门/岗位每库另有 13 项加密 HTTP 合同,树防环、删除引用保护及审计失败回滚已有双库验证。
- 14 模块 workspace race/vet、
GOWORK=off独立检查、本机及 Linux 构建通过。 - 客户端/页面共 67 项回归通过,覆盖权限/路由、身份切换、异步取消、真实 Vue 页面授权编辑、关联版本、错误后口令清空;类型检查、OpenAPI 76 操作/618 示例、生产构建与无 mock 检查通过。
双库响应校验可通过专用集成环境复现:设置 FAGO_HTTP_CONTRACT_EVIDENCE 为受保护的临时文件路径,运行 scripts/integration.sh,随后执行 node scripts/check-live-contracts.mjs <响应证据路径>。采集内容限经过敏感值断言的测试响应,文件权限 0600,不应对生产流量启用。
原始本机日志位于 /tmp/fago-m2-*.log,不作为可移植交付链接;结构化证据为 docs/evidence/m2-results.json。既有自动化与代理浏览器观测保持原始范围;M2 整体人工验收另依据用户直接确认归档,不由此宣称等保测评通过。
# HTTP 浏览器核对
使用已有且已经完成改密的专用开发测试账号,在 http://127.0.0.1:5174 实际完成登录、部门根/子节点创建、展开树、编辑名称及 version 递增、岗位创建/名称编辑/停用和关键词查询。保存后的授权重载保留仍有权限的当前页面;撤权回退及切换身份期间停止旧导航另有客户端回归。2026-10-23 继续验证用户姓名编辑、搜索、重载恢复,保留角色授权只读,以及空权限自定义角色创建、名称编辑和版本递增。随后核对操作审计/登录日志的 requestId 与动作精确筛选、只读脱敏详情、已有账号退出重登后新事件指纹和重载恢复。该轮没有操作当时暂缓的 M1 人工账号,也没有访问证书警告、填写新口令或通过页面保存授权。
截图使用本地测试组织与岗位,不含凭证;保留这些命名明确的开发数据供下一工作包联调:




# C06 普通用户页面范围验收
2026-10-23 以实际 Vben 页面分别连接专用 MySQL 和 PostgreSQL,三个临时普通用户仅拥有用户目录与两类日志的读取权限,没有保留管理员角色。两部门、无部门管理员、同部门同事、异部门用户及第二租户同名用户共同构成范围样本。浏览器不是管理员视图的替身,各次登录均读取主库发布的菜单与权限。
| 身份范围 | MySQL 可见用户数 | PostgreSQL 可见用户数 | 两库可见操作标记 |
|---|---|---|---|
| SELF | 1(仅本人) | 1(仅本人) | 1(仅本人作为操作人) |
| DEPT | 3(本人当前部门) | 3(本人当前部门) | 3(同部门操作人) |
| ALL | 6(本租户,含无部门管理员) | 6(本租户,含无部门管理员) | 5(本租户设有标记的五位操作人) |
操作标记为临时夹具构造的 qa.scope.marker,目标固定为乙部门 peer_b,SELF/DEPT 的操作主体均与目标不同,用来区分“操作人”与“被操作对象”;真实登录/退出及日志查询留痕仍由运行服务产生。两库登录日志分别只显示本人、本部门已有登录主体、本租户已有登录主体,未混入第二租户。ALL 用户列表没有第二租户的同名用户标记,MySQL 的 DEPT 账号直输安全策略 URL 返回无法访问;只读账号不显示创建/编辑/删除/授权按钮。跨标签身份变更也使原页面回到登录入口,随后已恢复原有开发账号。
浏览器结果记录在 docs/evidence/m2-scope-browser.json 与 m2-scope-postgres-browser.json,均不含凭证。临时租户、会话、授权关联、审计及私有凭证文件已由夹具清理,两个数据库查询确认随机租户计数为零;临时 PostgreSQL API/Vite 已关闭,原开发服务保留。




# 复现范围夹具
browser_acceptance_test.go 仅随 browser 构建标签进入测试,不进入生产二进制,也不在普通 CI 中运行。必须显式使用专用集成库和 APP_ENV=test。先让开发 API 与浏览器页面连接同一专用库,再在另一个终端运行:
source scripts/go-env.sh
export FAGO_INTEGRATION=1
export APP_ENV=test
export FAGO_BROWSER_FIXTURE_FILE="$(mktemp -d)/fixture.json"
cd fago
go test -race -tags=browser -count=1 -v -run '^TestBrowserScopeAcceptance$' -timeout=12m ./apps/fago-server
仅从该 0700 目录中的独占 0600 文件取得临时测试登录信息,不把内容复制到报告。检查结束后创建 ${FAGO_BROWSER_FIXTURE_FILE}.release,测试会清理自身两个随机租户与文件并查证结果;即使未释放,十分钟到期也会正常进入清理并使该次验收失败。外部强杀测试进程不保证回调执行,应避免强杀并保留日志排查。没有显式输出路径时测试跳过,不创建账号。
这些正常登录身份由测试夹具准备,本身不构成首次改密表单、真实页面创建账号或授权变更的验收证据;这些人工项现独立依据 2026-10-23 用户确认结项。
# M2 退出检查状态
| 检查范围 | 自动验证 | 真实浏览器 |
|---|---|---|
| 部门/岗位、用户资料及空权限角色 | 双库事务/隔离/版本通过 | HTTP 已核对安全操作和重载 |
| 普通用户 SELF/DEPT/ALL | 双库范围/隔离回归通过 | MySQL 与 PostgreSQL 真实页面均通过,临时数据已清理 |
| 多角色授权、重置、新账号首次改密 | 双库及虚拟 DOM 通过 | 2026-10-23 用户确认人工验收成功,原暂缓项结项 |
| 安全策略写入、账号解锁 | 双库及虚拟 DOM 通过 | 展示、表单与取消已核对;实际写入按 2026-10-23 用户确认结项 |
| 审计/登录日志及脱敏详情 | 双库、真实明文/JWE 合同及前端事件通过 | HTTP 精确筛选、详情、新事件和重载已核对 |
| 生产加密与 HTTPS | 实际 JWE 请求/响应自动验证通过 | M1 共用 HTTPS 门槛与 M2 独立人工业务项均已分别获用户确认 |
| 升级、构建、文档 | v1/v4/v5/v6/v8 升级、14 模块及前后端构建通过 | 文档站已刷新核对 |
C01–C06 工作包均已完成。M1、M2 分别依据用户确认完成人工验收,M2 已退出;既有自动化结果与本次人工确认分别归档。持续保留属于应用行为;生产数据库防篡改权限与备份恢复演练另在 M4 取证。
# 接下来继续
- M2 已结项,下一阶段为 M3 平台与 SaaS,范围与退出条件见路线图;M4 发布、生产数据库权限和备份恢复演练保留独立验收。