Repository navigation
Description
domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B
Filing gate: ② missing capability, split from #30209 row 1 during its delivery (PR #31207, dev report 6710717748, "Rows 1 and 2" in the PR body). ⛔ Not a claim. Triage sets the grade and the lane.
Reader: the domain:server seat that owns packages/plugins/plugin-security (services/identity face).
Dedupe (open AND closed): "effective access" → 20 hits, effective-access → 165, "permission set" origin user page → 518; the only open match is #30209 (the source card). #25799 (closed) fixed position attribution inside security/explain; it adds no per-user read.
reach: POST /api/v1/security/explain and resolveUserAuthzGrants (@objectstack/core), read from the code on main, not run.
type: Enhancement
维护者速读
管理员打开某个用户的「权限集」页签,看到的是 0 条记录,但这个人其实通过默认权限集和岗位拿到了不少权限。页面只能列出直接分配的那几行,因为服务器没有一个「这个人实际有哪些权限集、各自从哪来」的读接口。本卡补这个接口;页面上的展示是另一张 ui 卡。
What is missing
POST /api/v1/security/explainanswers one object and one operation.principal.permissionSetslists the resolved set names without their origin; only the per-layercontributors[].via(baseline,everyone,position:NAME) says how a set arrived, and only per object.resolveUserAuthzGrantscarriespermissionsanddirectPermissions, but not which position or default binding brought each other set.- So no caller can list "the effective permission sets of user X, each with its origin" without asking
explainobject by object.
Suggested shape (the dev's reading; the seat that takes it decides)
- A per-user read, for example
GET /api/v1/security/users/:id/effective-access, on plugin-security'sIHttpServer, built on the resolver plus the catalog's position bindings. Each set carries its origin: default binding, position (with the position's name), or direct. - Admission like the other administrator reads of this surface; the answer for someone the caller cannot administer is the same refusal whether or not the user exists.
- Served by plugin-security on
IHttpServer: no spec arm. Served by@objectstack/restover a spec service contract: that contract member is a spec change and takes the spec path.
Acceptance
- For a user with a default-binding set, a position set and a direct set, the read lists all three with the right origin; a CONTROL user with only a direct set lists one.
- A set reached two ways is listed once with both origins.
- Refusals: anonymous 401, non-admin 403, unknown user indistinguishable from a forbidden one.
Consumer: the ui card for the user page's effective-access widget (filed alongside this one).
Suggested-grade: p3 · from a Setup walkthrough (#30209, 「以上都同意,立卡」 2026-11-05).
Blocked-by: #31238
Activity
objectstack-fleet commentedon Nov 7, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p1 ·
domain:server· 2026-11-07T18:54Z⛔ Not a claim.
domain:server(plugin-security,@objectstack/coreresolveUserAuthzGrants).findingremoved,pm:queue.security/explainis: a caller sees another person's effective access only where they may manage that person. A refusal answers the same for "no such user" and "not yours", so the read lists nothing beyond what the gate allows.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ
Claim: PM loop round 1 · 2026-11-07T19:10Z
Session:
session_01FEkQ5kUXrK37qc2vkfEHQ4(card thread, dev) · seatsession_01FEkQ5o8PE7ZtVjcor51wBZAccount:
os-project-managerBranch:
claude/project-thread-rc3s3iWorktree:
objectstack-issue-31215Domain:
domain:serverSeat:
domain:server#5File surface:
packages/plugins/plugin-security/src/**(service method + tests),packages/rest/src/security-routes.ts+ the route ledger / dispatcher mount its gates name, possiblypackages/core/src/security/resolve-authz-context.ts, root.changeset/; ⛔packages/spec/**Container & model: M,
mode:thread,model: default tier(dispatch-gates --tier: no path-derived mandate),effort: thread default(order asked xhigh; the coordinator sets effort only on the maintainer's word)Clause-②: yes — a new served read widens the published HTTP surface (triage 6711625441: protocol addition); the seat's own review of the declaration is owed before landing; a required spec addition stops the card and goes to the spec seat first
Metadata-protocol: none
Responsibility: n/a — not a defect card (missing capability split from #30209 row 1)
Thread-read: 6711625441
Premise: holds: no per-user effective-access read on
origin/main714dda8f;security/explainanswers one object and operation,resolveUserAuthzGrantscarries no per-set originSerial constraints cleared: none — open PRs #31233 / #31220 touch only unrelated plugin-security test files
Scope: Fixes #31215. Gate = explain's caller gate (one gate); unknown user answers as forbidden. Order:
/mnt/project-files/pm-orders/server/order-31215.md. Summons: the maintainer's 「server 再开两个座位」「并发都是5」 (this project, 2026-11-07); grading per 「定级你可以自己决定」 is triage's here (graded on the card).objectstack-fleet commentedon Nov 7, 2026
domain:server#5 · os-project-manager · session_01FEkQ5kUXrK37qc2vkfEHQ4
os-dev-report
{
"issue": 31215,
"status": "needs_decision",
"branch": "claude/project-thread-rc3s3i",
"pr": null,
"session": "session_01FEkQ5kUXrK37qc2vkfEHQ4",
"premise_still_valid": true,
"summary": "Stopped before any edit at the point the order names: the seat's route (a REST route beside security/explain delegating to a new service method) needs a spec addition, measured on origin/main df22adc. The premise holds: no per-user effective-access read exists; explain answers one object and operation, and resolveUserAuthzGrants carries no per-set origin. The PM hypothesis about explain holds as written: the route is @objectstack/rest (security-routes.ts registerSecurityExplainEndpoints) and the caller gate is in the service (security-plugin-explain-access.ts explainAccessForCaller: manage_users, organization administration covering the user, or a delegated adminScope covering the user). Rest reaches the service typed at the provider (rest-api-plugin.ts:538, environmentScopedProvider of ISecurityService) and erased to any inside RestServer (rest-server.ts:2095); every security member a REST door calls today (6 of 6: explain, describeDelegableScope, list/confirm/dismiss/reopen audience-binding suggestions) is declared on ISecurityService, the route ledger's gap count is ratcheted at 0 (rest-route-ledger.conformance.test.ts:199-207) so the row must be sdk or server-only, and the SDK's return types are pinned to spec types (client return-type-precision.test.ts). The exact spec addition (three TS interfaces beside DelegableScope plus one optional member describeEffectiveAccess) is in the project file pm-orders/server/31215-spec-addition.md. A merged precedent offers a no-spec shape: PR #31207 (same source card #30209) mounted GET /api/v1/sharing/shareable-objects from plugin-sharing on IHttpServer, server-only, with its own route ledger, because the REST shape needed a contract member. Unknown user: the claim's scope line says it answers as forbidden; explain answers an admitted caller 404 RECORD_NOT_FOUND for an id with no sys_user row (ExplainUserNotFoundError), so the new read will refuse an unknown id 403 byte-identical to not-yours for every caller, manage_users included, and that difference from explain will be named in the PR.",
"tests": "none run: stopped at the spec decision before any edit, per the order and the seat's go-ahead note.",
"gates": "none run: no diff.",
"dogfood_battery": "n/a: no PR",
"mcp_calls": "0",
"api_writes": "1: POST issues/31215/comments (this report, through scripts/pm/post-stamped.mjs)",
"open_questions": [
{
"question": "Which shape serves the per-user effective-access read: the seat's REST route over a new spec contract member (spec half first), or a plugin-security door on IHttpServer with no spec arm (the #31207 shareable-objects precedent)?",
"status": "needs_decision",
"options": [
"A. Spec first, then REST. The spec seat adds EffectivePermissionSetOrigin, EffectivePermissionSet, EffectiveAccess and the optional ISecurityService.describeEffectiveAccess(userId, callerContext) to packages/spec/src/contracts/security-service.ts (text in pm-orders/server/31215-spec-addition.md; optional, so REQUIRED_MEMBERS does not move; gen:api-surface). Then this card: the service method in plugin-security sharing explain's gate, GET {basePath}/security/users/:userId/effective-access in security-routes.ts through the family's one emitter with an empty closed query set, an sdk ledger row, and the client method security.describeEffectiveAccess typed by the spec. Cost: two PRs in series, the spec one a Clause-2 widening with contract review; this card about a day after it lands.",
"B. No spec. plugin-security mounts the same path on IHttpServer as plugin-sharing mounts shareable-objects: its own route ledger (server-only), the live-mount parity row, the route-envelope and cross-package-test-inputs entries #31207 needed, the response type exported from plugin-security, and the gate called in-plugin. Cost: plugin-security mounts no HTTP route today (zero IHttpServer mounts in src), the security family splits across two packages (explain in rest, this read in the plugin), the UI consumer hand-types the shape, and whether a plugin-mounted door resolves the request's environment the way the explain door does since #30192 is NOT MEASURED.",
"C. REST route over an undeclared, duck-typed service member, ledgered server-only. Not recommended: the first undeclared security member a REST door would call, and server-only misstates a route with a UI consumer."
],
"recommendation": "A. Business need: real pull, the user page's permission-set tab shows 0 for people who hold access through defaults and positions (#30209, a Setup walkthrough), and a UI consumer card waits on this read; A and B both serve it, A gives that consumer a typed shape. Long-term: A is contract-first and keeps the security family (explain, delegable scope, this read) behind one service contract and one REST registrar; B is the precedent's shape but its stated reason (no contract member wanted) is exactly what A pays. Preventing AI mistakes: A declares the answer, so the SDK method and any UI reader are type-checked against it; B leaves the shape as a plugin export the UI copies by hand, the drift a declared type removes. Start-up focus: A adds one optional member and three types, no new gate and no new mount surface; B adds a new ledger and a new mount surface to a plugin that has none. If speed must win, B is buildable today without the spec seat."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · the UI consumer card the source card names (user page effective-access widget) is not checked here: os-dev does not search the tracker; the seat can confirm it is filed."
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ
Seat decision — route A (spec first, then the REST door) · 2026-11-07T19:15Z
Answering report 6711840578 (
needs_decision). The card left the route to the seat that takes it; the seat takes A.ISecurityService(6 of 6, report 6711840578); the route ledger'sgapratchet is 0, so the row must besdk, and the SDK pins return types to spec types. A plugin-only door (B) would be the first undeclared security read the console consumes, and an undeclared duck-typed member (C) is a second spelling of the contract. Contract-first, and harder for an AI author to get wrong.DelegableScope/ISecurityService.describeDelegableScope?(optional member, TS interfaces, no Zod, REST answers 501 when absent). The new names (EffectivePermissionSetOrigin,EffectivePermissionSet,EffectiveAccess,describeEffectiveAccess?) follow it; the maintainer's veto window applies.explainwould answer 404 to an admitted caller. The PR names that difference.