| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- import { Me } from './api/models';
- /**
- * ACL 判级 —— **只决定「请求哪个 scope」**,不做任何视图放行/拒绝
- *
- * 依据《API-契约.md》§五 判级链与 SPEC FR-20。
- *
- * ## 🔴 与 FR-20 AC-20.2 的边界(评审必读)
- *
- * 契约要求「前端代码中**无**基于 `roles` 的**授权分支**(判据:grep 命中 0)」,
- * 指的是**不得**在前端用 roles 决定「这个视图/数据能不能给用户看」。
- *
- * 本文件是**合规的**,理由:
- * · 它只回答「向服务端请求哪个范围」(`scope` 是**请求参数**,不是放行判据)
- * · 真正的可见范围判定**一律由服务端**返回结果决定(越权应返回 403,不是空列表)
- * · `roles` 缺失/异常时**回落最保守的 `mine`**,不存在「猜错了就放开」的路径
- *
- * 即:roles 是**期望值**输入,不是**授权**依据(契约 §五 原文口径)。
- *
- * ## 判级链(顺序不可调换)
- *
- * ```
- * roles 含 'masters' | 'FMODE_AGENT_SUPERADMIN' → 'all' 超管:跨租户全量
- * roles 含 '<companyId>_Admin' → 'company' 公司管理员:本公司
- * 否则 → 'mine' 普通用户:仅本人
- * ```
- */
- /** 向服务端请求的可见范围(云函数 `xlosi3m46q` 的 `scope` 参数) */
- export type Scope = 'all' | 'company' | 'mine';
- /** 身份级别(与 v1 `fmode-app/src/app/core/acl.ts` 对齐) */
- export type AclLevel = 'guest' | 'superadmin' | 'company-admin' | 'company-member' | 'personal';
- /** 判级结果:级别 + 展示标签 + 所属公司 */
- export interface AclScope {
- level: AclLevel;
- label: string;
- companyId?: string;
- }
- /** 跨租户超管角色名(与《API-契约》§五 一致,顺序无关) */
- const SUPERADMIN_ROLES = ['masters', 'FMODE_AGENT_SUPERADMIN'];
- /**
- * 判级 —— 对照 v1 `fmode-app/src/app/core/acl.ts:50-69` 逐条移植
- *
- * ```
- * ① 无 me → guest
- * ② roles 含超管角色 → superadmin(不要求有公司)
- * ③ username === 超管白名单 → superadmin
- * ④ 有 company + <cid>_Admin → company-admin
- * ⑤ 有 company → company-member
- * ⑥ 其余 → personal
- * ```
- *
- * 🔴 **管理员与成员必须区分**(`company-admin` vs `company-member`)——
- * 虽然**当前**两者权限相同(都请求 `scope='company'`),
- * 但将来若收紧为「成员只看自己」,只需改 `scopeOf()` 一处,不动判级逻辑。
- */
- export function resolveScope(me: Me | null | undefined): AclScope {
- if (!me) return { level: 'guest', label: '未登录' };
- const roles = me.roles ?? [];
- const companyId = me.companyId;
- // ② 超管(角色)—— 不要求有公司
- if (roles.some((r) => SUPERADMIN_ROLES.includes(r))) {
- return { level: 'superadmin', label: '系统超管' };
- }
- // ③ 超管(username 白名单,与 v1 SUPERADMIN_USERNAME 一致)
- if (me.name === 'FMODE_AGENT_SUPERADMIN') {
- return { level: 'superadmin', label: '系统超管' };
- }
- // ④/⑤ 有公司:区分管理员 vs 成员
- if (companyId) {
- const isAdmin = roles.includes(`${companyId}_Admin`);
- return {
- level: isAdmin ? 'company-admin' : 'company-member',
- label: isAdmin ? '公司管理员' : '公司成员',
- companyId,
- };
- }
- // ⑥ 无公司 → 个人
- return { level: 'personal', label: '个人用户' };
- }
- /**
- * 身份级别 → 请求 scope。
- *
- * | level | scope | 说明 |
- * |---|---|---|
- * | superadmin | `all` | 全部容器 |
- * | company-admin | `company` | 本公司 + 自己的(兜底)|
- * | company-member | `mine` | **仅自己**(2026-10-31 用户裁决:普通成员不应看到同事的容器)|
- * | guest / personal | `mine` | 仅自己 |
- *
- * 🔴 2026-10-31 隔离收紧:普通成员此前与管理员一样请求 `company`,
- * 导致同公司所有人都能看到彼此的容器(用户实测:看到其他人的 lj-test / 兔爷001)。
- * 现在成员统一请求 `mine`;服务端 `xlosi3m46q` 本来就把 `mine` 映射为
- * `user === 当前用户` 的强制过滤,因此这是**更小**的请求范围,不存在越权放大。
- * 无 company 的容器也归 `mine` 覆盖(`q.equalTo('user', user)`),不会漏看。
- */
- export function scopeOf(scope: AclScope): Scope {
- switch (scope.level) {
- case 'superadmin':
- return 'all';
- case 'company-admin':
- return 'company';
- case 'company-member':
- /*
- * 普通成员只看自己的(2026-10-31 用户裁决)。
- * 🔴 不要改回 company:那会把整个公司的容器暴露给每个成员。
- */
- return 'mine';
- default:
- return 'mine';
- }
- }
- /**
- * 判定请求 scope(`resolveScope` + `scopeOf` 的组合,保留旧调用签名)。
- *
- * 🔴 **`me` 为 null 时返回 `'mine'`** —— 安全默认:拿不到判级依据时按最小权限请求。
- */
- export function scopeFor(me: Me | null | undefined): Scope {
- return scopeOf(resolveScope(me));
- }
|