Skip to content

feat(access): PDP <Can> / 路由网关接真实后端 /api/v1/access/decide - #63

Merged
ghbvf merged 8 commits into
developfrom
017-pdp-decide
Jun 18, 2026
Merged

ghbvf merged 8 commits into
developfrom
017-pdp-decide

Conversation

@ghbvf

@ghbvf ghbvf commented Jun 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

<Can> / 路由 PDP 网关接入真实后端 POST /api/v1/access/decide(contract
http.auth.decide.v1,gocell#1863 已上线)。决策源从占位 mock 切换到真实 HTTP,并
补齐 UI 授权词表 → 后端注册权限名的翻译层。全站受保护路由(identities / policies /
audit / config / flags / cells)现按登录用户的真实权限放行/拒绝。

历史:本 PR 早期版本为「mock-first 结构化决策」(后端端点未交付时不阻塞推进)。
后端端点上线后已替换为真实调用,mock 决策源退场。

改了什么

  • @gocell/access
    • 新增 createHttpDecide():接 POST /api/v1/access/decide,响应
      { data: { allowed } } 映射回 Decision。HTTP 失败(400 未注册 action /
      403 PDP 无法评估 / 503 策略库不可达)→ 抛出 → createPdpClient 链路统一
      fail-closed 成 deny('error')。coarse 页面/能力检查不带后端实例 resource。
    • 新增 toPermission(action, resource)(pdp/permissionMap):UI 细粒度
      (action, resource) → 后端 <domain>:<verb> 注册权限名。资源域名对齐
      (identity→user、cell→system),写类动作收敛到 :write,verify→
      audit:read,未登记 → best-effort 拼名 → 后端 400 → fail-closed。真相源为后端
      framework/pkg/authz/permission.go 的 allPermissions。
    • createPdpClient 默认决策源由 mock allow-all 改为 fail-closed deny-all,
      杜绝忘记装配导致的 fail-open。抽 DecideFn/DecisionRequest 到 decideSource.ts。
    • 删除 mockDecide(占位源退场)。
  • apps/web:装配层 main.ts 显式注入 createPdpClient({ decide: createHttpDecide() })。
  • @gocell/core:PdpClient.decide() / 结构化 Decision / access.pdp.deny.* i18n
    键(首版引入,保留不变)。

路由 meta 资源 → 后端权限对照(全部已注册)

路由 meta 后端权限
read + identity user:read
read + policy policy:read
read + audit audit:read
read + config config:read
read + flag flag:read
read + cell system:read

一个有意的行为差异

<Can action="assign|revoke" resource="role"> 会 fail-closed 隐藏——后端 role
assign/revoke 是 cell 内部路由(RequireCallerCell),无面向用户的权限、浏览器端
本不可达,隐藏是诚实建模。admin 的其余真实功能按钮(user/config/flag 写操作、审计
校验)照常显示。

验收标准(#50)对照

  • createPdpClient 调真实决策源,缓存 + TTL 失效策略
  • 路由守卫真实 allow/deny;deny 原因 → i18n(access.pdp.deny.<reasonCode>)
  • 不在 route meta 硬编 role(保持既有 requiredAction/requiredResource 语义)
  • 默认 fail-closed(deny-all),不 fail-open

Test plan

  • pnpm -w lint(0 issues)
  • pnpm -w typecheck(全包通过)
  • pnpm -w test --run(121 files / 1809 tests 全绿,含 ESLint 边界锁)
  • pnpm -F @gocell/web build
  • 新增单测:permissionMap(全 (action,resource) 对)+ httpDecide(请求/映射/错误)

涉及包

@gocell/access、apps/web。未触及 packages/contracts/(decide 契约已由 develop
codegen 生成)。已 merge 最新 develop(解决 BR-009 tenant 冲突 + useAuthStore 重复合并)。

Refs #50

ghbvf and others added 5 commits June 13, 2026 07:05
…keys

PdpClient 增加异步结构化 decide(action,resource):Promise<Decision>,与响应式
can() 共享语义。新增 Decision { effect: 'allow'|'deny'; reasonCode } —— 对标
BR-004 §3.2 的前端消费子集。新增 access.pdp.deny.* i18n 键(role-missing / error)
供路由守卫本地化拒绝提示。

Refs #50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createPdpClient 不再永久 fail-closed(后端 /access/decide 未上线,gocell#1863)。
按 epic #62「mock-first」原则注入确定性 RBAC mock 决策源(mockDecide),默认
ADMIN_GRANT 全量放行 → 全站路由网关恢复可达;受限 grant(VIEWER_GRANT)驱动真实
deny 路径。client 保留缓存 + TTL + 单飞 + fail-closed,并实现 decide() 异步结构化决策。
后端端点交付后仅需 createPdpClient({ decide: realFn }) 注入即可替换。

Refs #50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
路由守卫 PDP 段改 await pdpClient.decide()(替代读响应式 can()),修复首次导航
pending→false 误把已授权用户重定向回 home 的潜在缺陷;拒绝时把 reasonCode 交给
注入的 onAccessDenied 回调。main.ts 装配层用 AntD message.warning + i18n 本地化
拒绝提示,守卫本身不耦合 AntD / i18n。

Refs #50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PdpClient 新增 decide()(#50)→ 各视图测试的 PdpClient stub 补上 decide 成员以满足
类型完整性;测试行为不变。

Refs #50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
三维度 review 收口:原 deny 提示走 AntD message 静态 API 有三个问题——脱离
ConfigProvider 主题树、无 aria-live/role=alert(WCAG 4.1.3 屏阅不可感知)、main.ts
需 `i18n.global.t as unknown as` 双重强转。改为:守卫经 onAccessDenied 把 reasonCode
推入 useUiStore,AppShellLayout 用 useI18n 本地化并渲染到 role="alert" aria-live
区域(tokens-only 样式 + 6s 自动消除)。未知 reasonCode 经 te() 回退到通用文案,
不再泄漏裸 i18n key。守卫保持 UI/i18n 解耦,main.ts 去掉 AntD message + 强转。

Refs #50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf

ghbvf commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

六维度 Review 结果(3 reviewer,diff 687 行 → 3 路并行)

核心 mock-first 决策逻辑(fail-closed / 单飞 / 缓存共享 / 守卫 await decide / i18n 键齐备)零 bug。三 reviewer 在「deny 提示面」收敛出同一组问题,已在 45401a2 一并修复。

已修(Cx2)— 45401a2

维度 问题 修复
a11y (WCAG 4.1.3) AntD message 无 aria-live/role=alert,屏阅不可感知 deny 改 role="alert" aria-live="assertive" 区域(AppShellLayout)
设计一致性 message 静态 API 脱离 ConfigProvider 主题树 改组件内渲染,tokens-only 样式
TS 安全 main.ts i18n.global.t as unknown as 双重强转 本地化移到 useI18n(layout),强转删除
健壮性 未知 reasonCode → 泄漏裸 i18n key te() 回退到通用文案

已评估并刻意保留(不修,附理由)

Finding Cx 决策
can() computed getter 内 void fetchDecision() 副作用 Cx1/2 保留。两 reviewer 均判 "not a bug"——单飞守护、惰性取数(避免 v-if 掉的 Can 误发请求);watchEffect 替代会在组件 scope 外泄漏 effect。属既有模式,注释已说明意图。
fetchDecision 无超时 Cx2 延后。后端 /access/decide 未上线(mock 同步 resolve),现加超时是预设未来需求(违 优雅简洁)。后端接入 PR 再补 AbortController。
store.entries/computedCache 无 LRU Cx1 延后。MVP key 空间有界(read×{identity,policy,audit,config,flag,cell})。resource 变动态 ID 时再补,reviewer 亦判 OUT_OF_SCOPE。
DecideFn 未经 exports 暴露 Cx1 延后。当前无外部消费方;后端接入走「替换默认 decide」时再 export。
Decision.reasonCode 未收窄为 union Cx1 保留 string。后端可能新增 code,union 会限制前向兼容;改用 surface 层 te() 回退兜底(已实现)。
@gocell/core 持有 access.* i18n Cx1 保留。既有集中式 i18n 策略(access.login 等早已在 core),非本 PR 引入。

@ghbvf

ghbvf commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

CI 说明(2 项 readonly 检查标红 — 与本 PR 无关)

  • ✅ typecheck / lint / test / build — pass(4m6s)
  • ✅ Playwright smoke — pass
  • ❌ codegen-readonly — fail
  • ❌ cell-manifest-readonly — fail

根因:后端 schema 漂移,非本 PR 引入。 本 PR git diff --name-only origin/develop 仅含 app/package 源码 + 测试,零接触 packages/contracts/、tools/、.github/。

codegen-readonly 重新 pnpm codegen 后发现已提交的 packages/contracts/src/ 落后于后端 ../gocell/contracts/:

  • auth/role/assign + auth/role/revoke 新增必填 tenantId
  • 新增 policy/* 契约(create/get/list/update)—— 后端 contracts/policy/ 于 6-12 演进

这是 develop 当前就存在的待修项(上次 develop 绿灯的 CI 早于该后端演进)。不并入本 PR:① 违「只改需要改的」;② 重新 codegen 会引入新必填 tenantId,大概率连带要改 packages/access/api/roles.ts 等消费端,属独立 chore(contracts) 工作项。

→ 建议单开 chore(contracts): pnpm codegen 同步后端 schema(tenantId + policy/*) PR/issue 处理。本 PR 的 PDP 改动与之正交。

ghbvf and others added 3 commits June 17, 2026 22:14
# Conflicts:
#	.github/workflows/cell-manifest-diff.yml
#	packages/access/src/api/roles.spec.ts
#	packages/access/src/stores/usePoliciesStore.spec.ts
#	packages/access/src/stores/usePoliciesStore.ts
#	packages/contracts/src/index.ts
#	packages/devboard/src/stores/useCellsStore.spec.ts
后端 PDP 端点(http.auth.decide.v1,gocell#1863)已上线,决策源从占位 mock
切换到真实 HTTP,并补齐 UI 词表 → 后端注册权限名的翻译层。

- 新增 `createHttpDecide()`:接 `POST /api/v1/access/decide`,响应
  `{ data: { allowed } }` 映射回 `Decision`;HTTP 失败(400 未注册 action /
  403 / 503)→ 抛出 → createPdpClient 链路 fail-closed deny。
- 新增 `toPermission(action, resource)`(permissionMap):UI 细粒度
  (action, resource) → 后端 `<domain>:<verb>` 权限名。资源域名对齐
  (identity→user、cell→system),写类动作收敛到 `:write`,未登记 → best-effort
  拼名 → 后端 400 fail-closed。真相源 backend authz/permission.go。
- `createPdpClient` 默认决策源由 mock allow-all 改为 **fail-closed deny-all**,
  杜绝忘记装配导致的 fail-open;`apps/web/main.ts` 显式注入 `createHttpDecide()`。
- 抽 `DecideFn`/`DecisionRequest` 到 `decideSource.ts`(mock ↔ real 公共缝)。
- 删除 `mockDecide`(后端已上线,占位源退场)。
- 单测:permissionMap 全 (action,resource) 对 + httpDecide 请求/映射/错误。

Refs #50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf ghbvf changed the title feat(access): PDP <Can> 接决策(mock-first allow/deny) feat(access): PDP <Can> / 路由网关接真实后端 /api/v1/access/decide Jun 17, 2026
@ghbvf
ghbvf merged commit 6cec2df into develop Jun 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant