acl.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { Me } from './api/models';
  2. /**
  3. * ACL 判级 —— **只决定「请求哪个 scope」**,不做任何视图放行/拒绝
  4. *
  5. * 依据《API-契约.md》§五 判级链与 SPEC FR-20。
  6. *
  7. * ## 🔴 与 FR-20 AC-20.2 的边界(评审必读)
  8. *
  9. * 契约要求「前端代码中**无**基于 `roles` 的**授权分支**(判据:grep 命中 0)」,
  10. * 指的是**不得**在前端用 roles 决定「这个视图/数据能不能给用户看」。
  11. *
  12. * 本文件是**合规的**,理由:
  13. * · 它只回答「向服务端请求哪个范围」(`scope` 是**请求参数**,不是放行判据)
  14. * · 真正的可见范围判定**一律由服务端**返回结果决定(越权应返回 403,不是空列表)
  15. * · `roles` 缺失/异常时**回落最保守的 `mine`**,不存在「猜错了就放开」的路径
  16. *
  17. * 即:roles 是**期望值**输入,不是**授权**依据(契约 §五 原文口径)。
  18. *
  19. * ## 判级链(顺序不可调换)
  20. *
  21. * ```
  22. * roles 含 'masters' | 'FMODE_AGENT_SUPERADMIN' → 'all' 超管:跨租户全量
  23. * roles 含 '<companyId>_Admin' → 'company' 公司管理员:本公司
  24. * 否则 → 'mine' 普通用户:仅本人
  25. * ```
  26. */
  27. /** 向服务端请求的可见范围(云函数 `xlosi3m46q` 的 `scope` 参数) */
  28. export type Scope = 'all' | 'company' | 'mine';
  29. /** 身份级别(与 v1 `fmode-app/src/app/core/acl.ts` 对齐) */
  30. export type AclLevel = 'guest' | 'superadmin' | 'company-admin' | 'company-member' | 'personal';
  31. /** 判级结果:级别 + 展示标签 + 所属公司 */
  32. export interface AclScope {
  33. level: AclLevel;
  34. label: string;
  35. companyId?: string;
  36. }
  37. /** 跨租户超管角色名(与《API-契约》§五 一致,顺序无关) */
  38. const SUPERADMIN_ROLES = ['masters', 'FMODE_AGENT_SUPERADMIN'];
  39. /**
  40. * 判级 —— 对照 v1 `fmode-app/src/app/core/acl.ts:50-69` 逐条移植
  41. *
  42. * ```
  43. * ① 无 me → guest
  44. * ② roles 含超管角色 → superadmin(不要求有公司)
  45. * ③ username === 超管白名单 → superadmin
  46. * ④ 有 company + <cid>_Admin → company-admin
  47. * ⑤ 有 company → company-member
  48. * ⑥ 其余 → personal
  49. * ```
  50. *
  51. * 🔴 **管理员与成员必须区分**(`company-admin` vs `company-member`)——
  52. * 虽然**当前**两者权限相同(都请求 `scope='company'`),
  53. * 但将来若收紧为「成员只看自己」,只需改 `scopeOf()` 一处,不动判级逻辑。
  54. */
  55. export function resolveScope(me: Me | null | undefined): AclScope {
  56. if (!me) return { level: 'guest', label: '未登录' };
  57. const roles = me.roles ?? [];
  58. const companyId = me.companyId;
  59. // ② 超管(角色)—— 不要求有公司
  60. if (roles.some((r) => SUPERADMIN_ROLES.includes(r))) {
  61. return { level: 'superadmin', label: '系统超管' };
  62. }
  63. // ③ 超管(username 白名单,与 v1 SUPERADMIN_USERNAME 一致)
  64. if (me.name === 'FMODE_AGENT_SUPERADMIN') {
  65. return { level: 'superadmin', label: '系统超管' };
  66. }
  67. // ④/⑤ 有公司:区分管理员 vs 成员
  68. if (companyId) {
  69. const isAdmin = roles.includes(`${companyId}_Admin`);
  70. return {
  71. level: isAdmin ? 'company-admin' : 'company-member',
  72. label: isAdmin ? '公司管理员' : '公司成员',
  73. companyId,
  74. };
  75. }
  76. // ⑥ 无公司 → 个人
  77. return { level: 'personal', label: '个人用户' };
  78. }
  79. /**
  80. * 身份级别 → 请求 scope。
  81. *
  82. * | level | scope | 说明 |
  83. * |---|---|---|
  84. * | superadmin | `all` | 全部容器 |
  85. * | company-admin | `company` | 本公司 + 自己的(兜底)|
  86. * | company-member | `mine` | **仅自己**(2026-10-31 用户裁决:普通成员不应看到同事的容器)|
  87. * | guest / personal | `mine` | 仅自己 |
  88. *
  89. * 🔴 2026-10-31 隔离收紧:普通成员此前与管理员一样请求 `company`,
  90. * 导致同公司所有人都能看到彼此的容器(用户实测:看到其他人的 lj-test / 兔爷001)。
  91. * 现在成员统一请求 `mine`;服务端 `xlosi3m46q` 本来就把 `mine` 映射为
  92. * `user === 当前用户` 的强制过滤,因此这是**更小**的请求范围,不存在越权放大。
  93. * 无 company 的容器也归 `mine` 覆盖(`q.equalTo('user', user)`),不会漏看。
  94. */
  95. export function scopeOf(scope: AclScope): Scope {
  96. switch (scope.level) {
  97. case 'superadmin':
  98. return 'all';
  99. case 'company-admin':
  100. return 'company';
  101. case 'company-member':
  102. /*
  103. * 普通成员只看自己的(2026-10-31 用户裁决)。
  104. * 🔴 不要改回 company:那会把整个公司的容器暴露给每个成员。
  105. */
  106. return 'mine';
  107. default:
  108. return 'mine';
  109. }
  110. }
  111. /**
  112. * 判定请求 scope(`resolveScope` + `scopeOf` 的组合,保留旧调用签名)。
  113. *
  114. * 🔴 **`me` 为 null 时返回 `'mine'`** —— 安全默认:拿不到判级依据时按最小权限请求。
  115. */
  116. export function scopeFor(me: Me | null | undefined): Scope {
  117. return scopeOf(resolveScope(me));
  118. }