Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,61 @@
- agent 主体交集规则(需 MCP OAuth 通道;运行时已实施,showcase REST 无法直接构造)。
- 层级深度 `own_and_reports/unit/unit_and_below`(企业版 hierarchy-security;开放版 fail-closed)。
- 到期授权/职责分离/环境晋升(文档 §10 明确"planned")。
- Access-matrix 快照门(已由 `os compile` 与仓库 CI 覆盖,见 access-matrix.json)。

---

# 第二轮:配置面测试(管理员配岗位 / 岗位映射权限集 / 开发者定义权限集)

> 第一轮(A~L)测的是**运行时行为**:给定配置,权限层序是否按设计裁决。
> 本轮(N~P)测的是**配置链路本身**:三种角色各自怎么把配置放进系统、配置如何生效。
>
> | 角色 | 配置面 | 通道 |
> |---|---|---|
> | 系统管理员 | 岗位、岗位↔权限集绑定、用户指派 | Setup 应用 UI(Access Control 组)/ REST `sys_*` |
> | 应用开发者 | 权限集、岗位声明、共享规则 | 代码 `definePermissionSet`/`definePosition` → `defineStack` → 构建产物 |
> | 引擎 | 声明→运行时投影 | `bootstrapDeclaredPositions` 种子;ADR-0094 write-through 投影 |

## N. 管理员配置岗位(Setup UI,视觉实测)

入口:`/_console/apps/setup/sys_position`(Setup 应用 → Access Control → Positions;应用级门 `setup.access`)。

| # | 用例 | 步骤 | 预期(截图) |
|---|---|---|---|
| N1 | 岗位列表页 | admin 打开 Positions | 网格列 Display Name/API Name/Default Position/Updated At;视图 tabs Active/Default/Custom/All;New 按钮 + Edit inline;含 7 个 showcase 岗位 + 内置 platform_admin/org_* + everyone/guest 锚点行 |
| N2 | 新建岗位表单 | 点 New | 弹窗字段:Display Name*、API Name*(机器名)、Description、Permissions(JSON,遗留)、Active、Default Position(自动指派新用户)、Delegatable(ADR-0091 D3 说明文字) |
| N3 | UI 建岗位 | 填 qa_lead / QA Lead → Create | 列表出现 QA Lead;REST 查 `managed_by` 非 system |
| N4 | 岗位详情 overlay | 点击 Contributor 行(`?recordId=…`) | overlay 含基本信息 + **Holders**(sys_user_position related list,Assign user 按钮)+ **Permission Sets**(sys_position_permission_set related list)两个 tab |
| N5 | 非管理员负例 | ada(无 setup.access)访问 `/_console/apps/setup/sys_position` | 无 Setup 入口 / 页面拒绝(截图) |
| N6 | 锚点行保护 | admin DELETE everyone 岗位行 | 拒绝(managed_by:system 不可删) |

## O. 岗位→权限集映射 + 指派端到端(管理员配置生效链)

绑定表 `sys_position_permission_set`(唯一键 position_id+permission_set_id,无独立导航,经岗位详情 related list 或 REST);指派表 `sys_user_position`(唯一键 user_id+position+organization_id,position 存**机器名**)。

| # | 用例 | 步骤 | 预期(截图) |
|---|---|---|---|
| O1 | 前置对照 | newbie 登录 console 看询价列表 | 空(仅 everyone 基线,private OWD)——复用 L4 |
| O2 | 建岗位+绑集 | admin POST sys_position{qa_lead} → POST sys_position_permission_set{qa_lead↔showcase_auditor} | 两行创建成功;岗位详情 Permission Sets tab 出现 Showcase Auditor |
| O3 | 指派用户 | admin POST sys_user_position{newbie, position:'qa_lead'} | 创建成功;`/auth/me/permissions`(newbie)positions 含 qa_lead,permissionSets 含 showcase_auditor |
| O4 | 能力即时生效(UI) | newbie 刷新询价列表 | **全量可见**(showcase_auditor 的 viewAllRecords 经新岗位到达)——与 O1 截图前后对比 |
| O5 | VAMA 只读边界 | newbie PATCH 他人询价 | 仍拒绝(viewAllRecords ≠ modifyAllRecords) |
| O6 | 重复绑定 | 再 POST 同一 qa_lead↔showcase_auditor | 唯一约束拒绝 |
| O7 | 解除指派→回收 | admin DELETE sys_user_position 行 | newbie 询价列表回到空;`/auth/me/permissions` 不再含 qa_lead |
| O8 | 锚点门(引用) | 绑 VAMA 集到 everyone → 403 | H3 已测,配置面视角复核 |
| O9 | 清理 | 删绑定行、qa_lead 岗位 | 环境还原 |

权限集详情页(`/_console/apps/setup/sys_permission_set?recordId=…`)预期:OWNING PACKAGE / MANAGED BY(package|user|system)、OBJECT/FIELD/SYSTEM/RLS/TAB 权限计数 +“Design in Studio”链接、**Assigned Users** related list(显示 `via position <name>` 的间接指派)。

## P. 应用开发者配置权限集(代码 → 构建 → 运行时投影)

authoring 面:`src/security/permission-sets.ts`(`definePermissionSet` from `@objectstack/spec`)、`src/security/positions.ts`(`definePosition`)、`objectstack.config.ts` 的 `positions:`/`permissions:`/`sharingRules:` 三键;构建产物 `dist/objectstack.json` 顶层 `positions[]`/`permissions[]`。

| # | 用例 | 步骤 | 预期 |
|---|---|---|---|
| P1 | 声明结构走查 | 检查 showcase 8 集 7 岗位的 authoring 源码与 defineStack 接线 | 与设计文档 §3/§4 对齐(FLS/RLS/adminScope/isDefault 全形态覆盖) |
| P2 | 构建产物 | `pnpm build` 后查 dist/objectstack.json | 顶层 positions/permissions 数组含全部声明 |
| P3 | FLS 改动端到端 | 把 `showcase_project.spent` editable:false→true → build → 重启 → ada 打开编辑弹窗 | spent 从灰显变可编辑(截图前后对比);`/auth/me/permissions` fieldPermissions 同步变化;**测后恢复源码** |
| P4 | access-matrix 快照门 | 给某集加对象能力(如 auditor+allowDelete)→ `objectstack compile` | 矩阵漂移 → 构建失败;`--update-access-matrix` 重新生成后通过;**测后恢复** |
| P5 | ADR-0094 投影(package 集) | admin REST PATCH showcase_contributor(managed_by:package) | 环境门编辑被拒/被重置为声明体(package 所有权保护,ADR-0086/0094) |
| P6 | ADR-0094 投影(runtime 集) | admin REST POST 新权限集 → PATCH → DELETE | write-through 到元数据层;行 managed_by:'user';删除即元数据删除 |
| P7 | 声明种子幂等 | 重启 dev 服务器两次 | sys_position/sys_permission_set 行数不变、不重复 |
64 changes: 55 additions & 9 deletions examples/app-showcase/docs/test/permission-model-test-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
## 1. 结论摘要

- **REST 全量套件:49/49 断言通过**([scripts/perm-test.sh](./scripts/perm-test.sh),覆盖清单 A~K 节)。
- **Playwright 套件:11/11 通过**([e2e/permission-model.spec.ts](../../e2e/permission-model.spec.ts):REST 代表用例 6 条 + console UI 实测 5 条,UI 全部留证截图)。
- **Playwright 套件:25/25 通过**(行为面 [e2e/permission-model.spec.ts](../../e2e/permission-model.spec.ts) 11 条 + 配置面 [e2e/permission-model-admin.spec.ts](../../e2e/permission-model-admin.spec.ts) 14 条,UI 全部留证截图)。
- **第二轮配置面测试(清单 N/O/P 节)全部通过**:管理员经 Setup UI 配岗位、岗位↔权限集映射端到端生效/回收、开发者代码定义→构建→运行时投影、access-matrix 快照门、ADR-0094 write-through。
- 实测发现并修复 **2 个引擎级缺陷**(见 §3):写时 RLS check 漏判岗位适用域;`/auth/me/permissions` 身份解析漂移导致 console 假只读。修复后全套件复验通过。
- 另有 6 项行为观察 / 测试注意事项(见 §4),均为符合设计或可解释的行为,记录备查。
- 另有 8 项行为观察 / 测试注意事项(见 §5),均为符合设计或可解释的行为,记录备查。

## 2. 执行结果(按清单分节)

Expand All @@ -27,8 +28,11 @@
| I | guest 匿名 | ✅ 3/3 | 匿名数据 API 401;公共表单通道可提交 |
| J | 委托管理(D12) | ✅ 6/6 | 子树 + 白名单 + 锚定三重校验全部生效 |
| K | explain 引擎 | ✅ 2/2 | 逐层解释;查他人需管理员 |
| L | console UI 实测 | ✅ 5/5 | **L5 首测失败 → 引擎缺陷②,修复后通过**;截图见 §5 |
| L | console UI 实测 | ✅ 5/5 | **L5 首测失败 → 引擎缺陷②,修复后通过**;截图见 §6 |
| M | 范围外 | — | 按设计文档标注跳过(planned / 企业版) |
| N | 管理员配置岗位(Setup UI) | ✅ 6/6 | 列表/新建表单/详情 related lists/非管理员负例/锚点保护,截图 N1~N5 |
| O | 岗位↔权限集映射端到端 | ✅ 8/8 | UI 建岗位→绑集→指派→能力生效→回收,前后对比截图 O1/O4/O7 |
| P | 开发者权限集定义链路 | ✅ 7/7 | FLS 改动端到端(截图 P3a/P3b)、快照门、ADR-0094 投影、种子幂等 |

## 3. 实测发现的引擎缺陷(均已修复)

Expand All @@ -46,16 +50,45 @@
- **修复**:`resolveCtx` 整体替换为委托 `resolveAuthzContext`(与 REST 服务器、runtime 调度器同源),返回 shape 对齐 rest-server;`hono-plugin.test.ts` 增加 2 条回归测试(岗位授予进入权限集解析、匿名返回 `authenticated:false`)。
- **复验**:ada `/auth/me/permissions` 返回 `positions:[org_member, contributor, everyone]`、`showcase_contributor` 权限集、`showcase_project.allowEdit:true` 且 budget FLS 仍锁定;L5 编辑弹窗 name/status/日期可编辑、budget/spent 灰显禁用(截图 L5)。

## 4. 行为观察与测试注意事项
## 4. 配置面实测详情(第二轮,清单 N/O/P 节)

### 4.1 系统管理员怎么配置岗位(N 节,Setup UI)

- 入口:Setup 应用(应用级门 `setup.access`)→ **Access Control** 组 → Positions(`/_console/apps/setup/sys_position`)。
- 列表页含 New / Edit inline / 视图 tabs(Active/Default/Custom/All);14 行 = 7 个 showcase 声明岗位 + 内置身份岗位(platform_admin/org_owner/org_admin/org_member)+ everyone/guest 锚点(截图 N1)。
- 新建表单字段:Display Name*、API Name*(机器名)、Description、Permissions(JSON,遗留通道)、Active、Default Position(自动指派新用户)、Delegatable(ADR-0091 D3)(截图 N2);实测经 UI 创建 `qa_lead` 落库为普通行(截图 N3)。
- 岗位详情 overlay 是配置枢纽:**Holders** tab(`sys_user_position`,Assign user 按钮)+ **Permission Sets** tab(`sys_position_permission_set`,Bind permission set 按钮)(截图 N4)。两张 junction 表无独立导航,这里是唯一 UI 入口。
- 负例:ada(无 `setup.access`)访问 Setup URL 回落到默认应用壳,侧边栏无 Access Control 组,网格数据加载被数据面拒绝(截图 N5)。
- 锚点保护:everyone/guest 行 `managed_by:system`,DELETE 被拒(N6)。

### 4.2 岗位怎么映射到权限集并生效(O 节,端到端)

完整链路实测:**UI 建岗位 qa_lead → REST 绑 showcase_auditor → 指派 newbie → 能力立刻生效 → 解除立刻回收**。

- 绑定后岗位详情 Permission Sets tab 显示 `showcase_auditor` 行(截图 O2)。
- 指派后 newbie 的 `/auth/me/permissions` 即含 `qa_lead` 岗位与 `showcase_auditor` 集;console 询价列表从**空**(截图 O1)变**7 条全量**(截图 O4)——viewAllRecords 经新岗位到达,无需重登。
- VAMA 只读边界仍成立(newbie 改他人询价 → 4xx);重复绑定被唯一约束拒绝;解除指派后列表回空(截图 O7),证明授予与回收都是即时的。

### 4.3 应用开发者怎么配置权限集(P 节,代码 → 构建 → 运行时)

- **authoring**:`src/security/permission-sets.ts`(`definePermissionSet`)+ `positions.ts`(`definePosition`)→ `objectstack.config.ts` 的 `positions:`/`permissions:`/`sharingRules:` 三键;构建产物 `dist/objectstack.json` 顶层数组(P1/P2)。
- **FLS 改动端到端(P3)**:把 `showcase_project.spent` 的 `editable:false→true` → `pnpm build` → 重启 → ada 的 `/auth/me/permissions` 立即反映;编辑弹窗中 spent 从灰显(截图 P3a)变可编辑、budget 仍锁(截图 P3b);REST 写入持久化。改回后行为复原——**声明是权威源,boot 重投影覆盖运行时行**(ADR-0094)。
- **access-matrix 快照门(P4)**:FLS 改动不驱动矩阵(矩阵只含对象级能力);给 auditor 加 `allowDelete` 后 `pnpm build` 失败:`✗ Access matrix drift (1 change) — 'showcase_auditor' gains delete on 'showcase_inquiry'`;`objectstack build --update-access-matrix` 重新生成,git diff 恰为一行 `"delete": false→true`(diff 即评审工件,ADR-0090 D6)。
- **ADR-0094 write-through(P5/P6)**:admin PATCH package 所有的集(showcase_contributor)→ **403**,报错明示 ADR-0086 两门分离(“change it by editing its package and re-publishing”);REST 新建运行时集 → 行 `managed_by:'user'`,PATCH/DELETE 全生命周期经元数据层 write-through 正常。
- **种子幂等(P7)**:两次重启后 sys_position=13、sys_permission_set=15,不重复。

## 5. 行为观察与测试注意事项

1. **共享规则是写时物化**:条件 sharing 在记录写入/更新时物化到 `sys_record_share` 并重算;改规则后需 touch 记录才生效。测试共享用例要先写一次记录。
2. **岗位↔权限集绑定是管理员显式配置**:岗位本身不携带权限,必须有 `sys_position_permission_set` 绑定行;漏绑时用户静默落到 everyone 基线(无报错)。
3. **能力门先于记录范围**:对象级能力未授予时直接 403,OWD/sharing/VAMA 完全不参与 —— 与设计文档 §2 的层序一致。
4. **guest 孤儿行**:公共表单通道创建的记录 `owner_id=null`,private OWD 下无深度/VAMA 的用户(包括提交者)都不可见、不可改删,只有 org 深度或 VAMA 持有者能处理 —— 询价场景正好合理,但建业务对象时要留意。
5. **scope 层拒绝的响应形态**:记录范围层拒写返回 **HTTP 400 + body `code:FORBIDDEN`**(非 403);能力门层才是 403。断言时按层区分。
6. **审批锁与权限测试相互作用**:`budget>100000 && changed` 的审批规则会锁记录(RECORD_LOCKED),**管理员恢复现场的写入同样触发**,需由待批人 reject/approve 解锁后再继续。权限用例应避开会触发审批的字段组合,或测试后显式清理审批请求。
7. **console FLS 渲染依赖 cookie 会话**:只向 localStorage 注入 bearer token(无 better-auth cookie)时,编辑表单的 FLS 禁用态不生效——全字段渲染为可编辑(UI 层 fail-open;数据面仍强制,写入照样被剥离)。真实登录总带 cookie,影响限于 token 注入类自动化;写 UI 测试必须像 spec 的 `signIn` 一样把 cookie 一并注入。
8. **绑定 related list 显示权限集 API name**:岗位详情 Permission Sets 网格的 lookup 列解析目标对象 `name` 字段,显示 `showcase_auditor` 而非 display label(与"网格 lookup 只认 name 字段"的既知行为一致),断言/截图预期按 API name 写。

## 5. UI 实测截图索引([screenshots/](./screenshots/))
## 6. UI 实测截图索引([screenshots/](./screenshots/))

| 截图 | 用例 | 证明 |
|---|---|---|
Expand All @@ -65,8 +98,19 @@
| [L3b-audrey-private-notes-vama.png](./screenshots/L3b-audrey-private-notes-vama.png) | L3 | audrey 可见他人 private_note(VAMA 越过 private OWD) |
| [L4-newbie-inquiries-empty.png](./screenshots/L4-newbie-inquiries-empty.png) | L4 | newbie 询价列表为空(private OWD,无深度/共享/VAMA) |
| [L5-ada-project-budget-fls-locked.png](./screenshots/L5-ada-project-budget-fls-locked.png) | L5 | ada 编辑弹窗:budget/spent 灰显禁用(FLS),status/日期可编辑 |

## 6. 复跑方法
| [N1-admin-positions-list.png](./screenshots/N1-admin-positions-list.png) | N1 | Setup→Positions 列表:14 岗位(声明+内置+锚点)、New/视图 tabs |
| [N2-position-new-form.png](./screenshots/N2-position-new-form.png) | N2 | 新建岗位表单:label/name/description/permissions/active/is_default/delegatable |
| [N3-position-qa-lead-created.png](./screenshots/N3-position-qa-lead-created.png) | N3 | UI 创建的 QA Lead 出现在列表 |
| [N4-position-detail-related-lists.png](./screenshots/N4-position-detail-related-lists.png) | N4 | 岗位详情 overlay:Holders + Permission Sets related lists、Assign user |
| [N5-nonadmin-positions-denied.png](./screenshots/N5-nonadmin-positions-denied.png) | N5 | ada 无 setup.access:无 Access Control 导航,网格加载被拒 |
| [O1-newbie-inquiries-before.png](./screenshots/O1-newbie-inquiries-before.png) | O1 | 指派前:newbie 询价列表空(Nothing here yet) |
| [O2-qa-lead-bound-showcase-auditor.png](./screenshots/O2-qa-lead-bound-showcase-auditor.png) | O2 | qa_lead 详情 Permission Sets tab 显示 showcase_auditor 绑定 |
| [O4-newbie-inquiries-after-assignment.png](./screenshots/O4-newbie-inquiries-after-assignment.png) | O4 | 指派后:同一 newbie 看到 7 条全量询价(viewAllRecords 经岗位到达) |
| [O7-newbie-inquiries-revoked.png](./screenshots/O7-newbie-inquiries-revoked.png) | O7 | 解除指派后:列表回空(即时回收) |
| [P3a-ada-spent-fls-locked-before.png](./screenshots/P3a-ada-spent-fls-locked-before.png) | P3 | 改声明前:spent/budget 灰显禁用 |
| [P3b-ada-spent-editable-after.png](./screenshots/P3b-ada-spent-editable-after.png) | P3 | 改声明+重建+重启后:spent 可编辑、budget 仍锁 |

## 7. 复跑方法

```bash
# 1. 启动 showcase(dev,自动种子 + seed admin)
Expand All @@ -80,9 +124,11 @@ docs/test/scripts/perm-setup.sh # BASE 默认 http://localhost:3777
# 3. REST 全量套件(49 断言)
docs/test/scripts/perm-test.sh

# 4. Playwright(REST 代表用例 + UI 截图;spec 自我供给,亦可独立于 2/3 运行)
# 4. Playwright(行为面 + 配置面两个 spec 共 25 条;自我供给,亦可独立于 2/3 运行)
PERM_BASE_URL=http://localhost:3777 \
pnpm exec playwright test --config playwright.permission.config.ts
```

Playwright 配置不设 `PERM_BASE_URL` 时会自行拉起 :3000 后端(CI 模式)。该 spec 已从默认 smoke 配置排除(`playwright.config.ts` 的 `testIgnore`),仅经上述专用配置显式运行。
Playwright 配置不设 `PERM_BASE_URL` 时会自行拉起 :3000 后端(CI 模式)。两个 spec 已从默认 smoke 配置排除(`playwright.config.ts` 的 `testIgnore: 'permission-model*.spec.ts'`),仅经上述专用配置显式运行。

P 节的构建链路用例(P3/P4)是破坏性的(改源码、重建、重启),不进 spec,按 §4.3 手工复跑:改 `src/security/permission-sets.ts` → `pnpm build`(观察快照门)→ 重启 → 验证 → 还原。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading