feat(access): PDP <Can> / 路由网关接真实后端 /api/v1/access/decide - #63
Merged
Merged
Conversation
…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>
三维度 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>
Owner
Author
六维度 Review 结果(3 reviewer,diff 687 行 → 3 路并行)核心 mock-first 决策逻辑(fail-closed / 单飞 / 缓存共享 / 守卫 await decide / i18n 键齐备)零 bug。三 reviewer 在「deny 提示面」收敛出同一组问题,已在 已修(Cx2)—
|
| 维度 | 问题 | 修复 |
|---|---|---|
| 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 引入。 |
Owner
Author
CI 说明(2 项 readonly 检查标红 — 与本 PR 无关)
根因:后端 schema 漂移,非本 PR 引入。 本 PR
这是 develop 当前就存在的待修项(上次 develop 绿灯的 CI 早于该后端演进)。不并入本 PR:① 违「只改需要改的」;② 重新 codegen 会引入新必填 → 建议单开 |
# 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
<Can>/ 路由 PDP 网关接入真实后端POST /api/v1/access/decide(contracthttp.auth.decide.v1,gocell#1863 已上线)。决策源从占位 mock 切换到真实 HTTP,并补齐 UI 授权词表 → 后端注册权限名的翻译层。全站受保护路由(identities / policies /
audit / config / flags / cells)现按登录用户的真实权限放行/拒绝。
改了什么
@gocell/accesscreateHttpDecide():接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 资源 → 后端权限对照(全部已注册)
read+identityuser:readread+policypolicy:readread+auditaudit:readread+configconfig:readread+flagflag:readread+cellsystem:read一个有意的行为差异
<Can action="assign|revoke" resource="role">会 fail-closed 隐藏——后端 roleassign/revoke 是 cell 内部路由(
RequireCallerCell),无面向用户的权限、浏览器端本不可达,隐藏是诚实建模。admin 的其余真实功能按钮(user/config/flag 写操作、审计
校验)照常显示。
验收标准(#50)对照
createPdpClient调真实决策源,缓存 + TTL 失效策略access.pdp.deny.<reasonCode>)requiredAction/requiredResource语义)Test plan
pnpm -w lint(0 issues)pnpm -w typecheck(全包通过)pnpm -w test --run(121 files / 1809 tests 全绿,含 ESLint 边界锁)pnpm -F @gocell/web buildpermissionMap(全 (action,resource) 对)+httpDecide(请求/映射/错误)涉及包
@gocell/access、apps/web。未触及packages/contracts/(decide 契约已由 developcodegen 生成)。已 merge 最新
develop(解决 BR-009 tenant 冲突 +useAuthStore重复合并)。Refs #50