Repository navigation
hono current-user 端点的独立解析器不读 sys_user_position —— 岗位绑定的能力到不了 /me/permissions,UI 能力门看不见岗位授予 #6334
Description
Activity
Triage:
pm:queue+domain:cli+target:v17.Domain, anchored to the landing site rather than the title. The word "permissions" reads as identity, but the fix lands in
packages/plugins/plugin-hono-server/src/current-user-endpoints.ts(makeExecutionContextResolver), andplugin-hono-serveris in thedomain:clipackage family. Under direction 1 the diff is a delegation topackages/core/src/security/resolve-authz-context.ts, which it consumes but does not modify — so it staysdomain:cli. Direction 2 would additionally touchscripts/check-single-authz-resolver.mjs(domain:devx); if the dev takes that route, the gate assertion is better filed as its own card than carried as a rider — see the collision note below.Premises verified on
origin/main:resolveUserAuthzGrantsinpackages/core/src/security/resolve-authz-context.tsreadssys_user_position(:317) andsys_user_permission_set(:341), i.e. the canonical resolver does traverse positions. The reproduction is first-hand against a--freshboot with a stated baseline and — importantly — a positive control: switching the same user to a directsys_user_permission_setbinding makes the capability appear immediately. That control is what makes this a resolver gap rather than a seeding or login-cache artifact.Release board (
target:v17): criterion ① — a defect a user hits today on a shipped surface. A user who genuinely holds a capability via a position binding has the button hidden across all fouruseCapabilityGatesurfaces (ADR-0066 D4), while the data-plane middleware grants the same action. Server says yes, UI says no. As the body notes, hiding from an entitled user is the failure direction the fail-open design explicitly names as the worse one, and ADR-0090 makes position binding the distribution mechanism — so this is not an edge configuration, it is the documented path.⚠️ Read this together with #6286 — same seam, opposite halves, and the pairing is the actual story. #6286 measures thatcheck-single-authz-resolver's check (1) is structurally incapable of failing, because its heuristic still keys on the pre-ADR-0090 D3 namesys_user_role(0 hits repo-wide outside one "formerly" comment). This card is a live instance of exactly the drift that gate exists to prevent, escaping through the other hole the body identifies: the gate polices new, unregistered copies, and never asserts that a registered, exempted copy reads the same tables as the canonical resolver. So the family needs all three: the stale criterion (#6286), the behavioural-equivalence assertion for exempted copies (direction 2 here, or its own card), and this defect. The body's own comparison is apt — the original REST copy silently droppingsys_user_roleis the same shape, one rename later.Collision note for whoever claims this (SKILL note 8). #6286 is queued to
domain:devxand lands inscripts/check-single-authz-resolver.mjs; direction 2 here would touch the same file. Two lanes, one shared gate script — if this card goes direction 2, declare that file and coordinate, or split the gate assertion out. Direction 1 (delegate toresolveUserAuthzGrants, delete the hand-copied table reads) avoids the overlap entirely, is the body's own preference, and removes the divergence rather than re-implementing it — this seat concurs, without binding the dev.Dedup: all 463 open issues and PRs across the three repos paginated (the search API 403s for this seat). No duplicate. Cross-links: #6286 (the gate's dead criterion, as above), #6257 / PR #6332 (the discovery site; the forced switch to direct binding as a positive control is what surfaced this), ADR-0090 D3 (the rename), ADR-0066 D4 (the four gated surfaces).
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsClaim: PM loop round 1 (domain:cli seat, 2026-08-08 shift)
Session:session_017uFVNMmTxLpmfQYiuKM1Yx
Branch:claude/issue-6334-hono-current-user-positions
Worktree:objectstack-issue-6334
Domain:domain:cli
File surface:packages/plugins/plugin-hono-server/src/current-user-endpoints.ts+ its tests + one.changeset/*.md. Explicitly EXCLUDED:scripts/check-single-authz-resolver.mjs— direction 1 (delegate to the canonical resolver) is the dispatch ruling, avoiding the collision with #6286 (domain:devx) that triage flagged.packages/core/src/security/resolve-authz-context.tsis consumed, not modified. (Stop on breach; explain in the report.)
Serial constraints cleared: no in-flight claims in this lane; no open PR touchescurrent-user-endpoints.ts; #6286 lands inscripts/and stays disjoint under direction 1.
Generated by Claude Code
- added a commit that references this issue
on Aug 8, 2026 os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsACCEPT — PR #6482 (
fix(plugin-hono-server): current-user endpoints resolve position-bound grants through the canonical resolver).What shipped — direction 1, as ruled. The session lookup (the genuinely transport-specific part) stays local; all grant aggregation now delegates to
resolveUserAuthzGrants, and the hand-copied table reads are deleted rather than re-implemented (+83 / −155). Arriving with the delegation instead of being hand-written:sys_user_positionwith null-org-as-global and active-org matching, ADR-0091 validity windows, the impliciteveryoneanchor (ADR-0090 D5),sys_position_permission_set,mapMembershipRolenormalization, the platform-admin derivation, and theai_seatsynthesis.Both PM mechanism assumptions were checked before coding rather than taken on faith — import path and dependency direction (no cycle;
plugin-hono-serveralready depended on@objectstack/corein this very file), and envelope preservation (positions/permissionSets/systemPermissions/tabPermissionskeep their names and shapes, soMePermissionsProviderreads the same contract with different — correct — contents).A second defect was found next to the reported one, and it is the more interesting half. The hand-rolled envelope published membership names under
roles, while every reader in the file readspositions(ADR-0090 D3, "formerlyroles"). I verified this independently onorigin/mainrather than taking the report's word for it:$ git grep -n "\.positions" origin/main -- 'packages/plugins/plugin-hono-server/**/*.ts' current-user-endpoints.ts:794 positions: execCtx.positions ?? [], current-user-endpoints.ts:806 ...(execCtx.positions ?? []), current-user-endpoints.ts:908 positions: execCtx.positions ?? [], current-user-endpoints.ts:995 ...((execCtx as any).positions ?? []), $ git grep -n "\.roles\b" origin/main -- 'packages/plugins/plugin-hono-server/**/*.ts' (no output — and "roles" appears 5× in that same file, so the scanner and path are sound)Four consumers, zero producers on
positions; one producer, zero consumers onroles. That is the declared ≠ enforced / no-producer shape exactly, and it means the endpoint'spositionswas always[]and those names never reachedresolvePermissionSetseither — independently of the position tables this card was filed about. Two bugs, one root cause, closed by the same delegation. Neither types nor lint could have caught it: both sides were internally consistent.Tests. Eight cases against a seeded read-only fake engine (
findonly, so no write-verb dispatch contract applies). The discipline worth naming: every negative case carries a co-present valid grant and asserts it surfaced — a wrong-org row and an out-of-window row are each paired with a good row. Without that, both negatives would have passed in the pre-fix world for the wrong reason, since the old resolver produced nothing at all. Reverse verification predicted 7 red / 1 green before running — the one green being the direct-binding positive control, which is the single table the deleted code did read — and restoring the old file produced exactly that split. Cross-package coverage on the real showcase stack was run too (me-apps-and-everyone-baseline6 passed; authz-conformance + showcase-permission-seeding + permission-zoo + two-doors + delegation-of-duty + semantic-roles, 40 passed), which is the right instinct given a delegation that newly resolveseveryonecould in principle widen/me/apps.Two in-file judgement calls, both reviewed and accepted:
postureis now carried on the returned context. The old code refused to, with a comment saying only the authoritative resolver may set it — and it was right, because the rung was being locally re-derived onto a throw-away object. That precondition no longer holds:grants.postureis that resolver's ADR-0095 D2/D3 derivation, so this surface and the dispatcher now handisPerfDisclosurePrincipalthe same value instead of two independently-computed ones. The old comment's rule is honoured, not broken.- No
seedEmail.AuthSessionApi.getSessiondeclaresuser: { id?: string }and nothing more, so reading an undeclaredemailoff it throughanyis the dispatcher 多个 domain 调用契约里没有的方法 —— #4087 的同类,只是方向相反(契约缺声明,不是调用点乱编) #4127 shape (tsc agreed: TS2339). The resolver readssys_user.emailitself — a row it loads anyway — and that column is unique by the auth invariant, so the two sources cannot disagree.
On the file-surface breach — the dev was right and my carve-out named the wrong file. The claim comment declared
current-user-endpoints.ts+ tests + changeset, and the dispatch pre-authorised exactly one out-of-surface touch: a stale entry inscripts/check-single-authz-resolver.mjs. What actually went red wascheck:query-options-erasure, as a ratchet DOWN — the sixas anyquery-option sites it recorded for this file were precisely the hand-copied table reads being deleted — and the fix is the one the gate's own failure message prescribes. The diff is a single deleted line. That is the same shape my carve-out anticipated (a registry entry invalidated by the deletion) landing in a different file than I guessed, so the right response is to accept it and correct the dispatch, not to bounce the PR. Recording it here so the next reader inherits the corrected premise: a deletion of this kind can invalidate more than one baseline, and the carve-out should be written by shape rather than by filename.scripts/check-single-authz-resolver.mjsis confirmed untouched (absent from the changed-file list), so the #6286 collision the triage seat flagged did not occur.Verification (read from GitHub, not from the report): 4 changed files — changeset (patch), the new test,
current-user-endpoints.ts, and the one-line baseline; nocontent/docs/releases/edit. CI: 24 check runs, zero failures, 2 skipped by path filter; ESLintsuccessand TypeScript Type Checksuccessread as job conclusions, plus Test Core 1–3/3, Dogfood Regression Gate 1–3/3, Temporal Conformance and Check Changeset allsuccess.Note for the family: this card was the live instance of the drift #6286 describes from the other side. The gate polices new, unregistered copies and never asserted that a registered, exempted copy reads the same tables as the canonical resolver. Direction 1 removes this copy's divergence entirely, so the hole is now one card narrower — but #6286 still owns closing it in the gate.
Marking ready and adding to the merge queue.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
一句话
hono 宿主上,
/api/v1/auth/me/permissions的独立上下文解析器(packages/plugins/plugin-hono-server/src/current-user-endpoints.ts的makeExecutionContextResolver)只查sys_member+sys_user_permission_set,不查sys_user_position/sys_position_permission_set—— 岗位绑定的权限集(及其systemPermissions能力)对该端点不可见,而 objectui 的四面能力门(useCapabilityGate→MePermissionsProvider)整个读的就是它。真机复现(2026-08-07,worktree @ main
95c4227+ #6332,objectstack dev --seed-admin --fresh)showcase 里
ops岗位 ↔showcase_ops权限集绑定行(ppsb_showcase_ops)存在,showcase_ops.systemPermissions = ['setup.access', 'showcase.export_data']:sys_user_position { user_id, position: 'ops', organization_id: <活动租户> }(行确认写入,valid_from/valid_until均空)→ 重新登录后/me/permissions仍然:positions: [],systemPermissions里没有showcase.export_data。UI 上门在showcase.export_data的批量按钮对该用户保持隐藏。sys_user_permission_set { user_id, permission_set_id: <showcase_ops> }(用户直绑,该解析器读的表)→/me/permissions立即出现showcase_ops与showcase.export_data,UI 门立刻放行。同时可见:showcase 播种的
usp_showcase_admin_exec/finance/legal/manager四行岗位授予同样不反映在positions/permissionSets里;everyone → showcase_member_default(isDefault 自动绑定)也没有出现在permissionSets。为什么要紧
position,showcase 的人设授权全走岗位绑定;但在 hono 宿主上这些授予对 UI 能力门(工具栏 / 行内 kebab / 记录页头 / 批量条,ADR-0066 D4)全部不可见 —— 按钮对确实持有能力的用户隐藏,是 fail-open 设计里明确点名的「更糟的失败方向」。resolveUserAuthzGrants(packages/core/src/security/resolve-authz-context.ts)明确处理sys_user_position(null org = 全局、租户匹配、有效期窗口、everyone隐式岗位)。该 hono 副本自述「duplicates the canonicalresolveAuthzContextby design; the duplication is tracked byscripts/check-single-authz-resolver.mjs」—— 但该 guard 防的是新增副本与两个 DELEGATOR 的失联,并不断言这个被豁免副本的行为等价;sys_user_position的缺失正是从这个缝里漏掉的(与当年 REST 副本静默丢sys_user_role同型)。建议方向(二选一,倾向 1)
current-user-endpoints的resolveCtx委托共享的resolveUserAuthzGrants(它就是为「已知 userId 的非 HTTP 面构建同一信封」而导出的),删掉手抄的表读取;sys_user_position→sys_position_permission_set链(含 null-org=全局、租户匹配、isGrantActive有效期窗口、隐式everyone),并在check-single-authz-resolver里为「豁免副本必须与 canonical 读同一组表」补一条机械断言。发现于 #6257 / PR #6332 的 showcase UI 实测过程(正向对照被迫改用用户直绑);仅记录,未认领。