Skip to content

security: a per-user effective-access read that says where each permission set comes from (default binding, position, direct) #31215

Open

Description

@objectstack-fleet

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/explain answers one object and one operation. principal.permissionSets lists the resolved set names without their origin; only the per-layer contributors[].via (baseline, everyone, position:NAME) says how a set arrived, and only per object.
  • resolveUserAuthzGrants carries permissions and directPermissions, 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 explain object 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's IHttpServer, 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/rest over 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

  1. added
    enhancementNew feature or request
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 7, 2026
  2. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p1 · domain:server · 2026-11-07T18:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

    Claim: PM loop round 1 · 2026-11-07T19:10Z
    Session: session_01FEkQ5kUXrK37qc2vkfEHQ4 (card thread, dev) · seat session_01FEkQ5o8PE7ZtVjcor51wBZ
    Account: os-project-manager
    Branch: claude/project-thread-rc3s3i
    Worktree: objectstack-issue-31215
    Domain: domain:server
    Seat: domain:server#5
    File surface: packages/plugins/plugin-security/src/** (service method + tests), packages/rest/src/security-routes.ts + the route ledger / dispatcher mount its gates name, possibly packages/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/main 714dda8f; security/explain answers one object and operation, resolveUserAuthzGrants carries no per-set origin
    Serial 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).

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 7, 2026
  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • Why A: every security member a REST door calls is declared on ISecurityService (6 of 6, report 6711840578); the route ledger's gap ratchet is 0, so the row must be sdk, 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.
    • Precedent named: 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.
    • Refusal: an unknown user answers exactly as a user the caller may not manage, for every caller (triage 6711625441, card acceptance), even where explain would answer 404 to an admitted caller. The PR names that difference.
    • Next: the spec half goes to the spec lane first (text drafted by the dev in this card's report). This card holds its claim and builds the plugin-security service method + REST door + client method once that spec change lands; it is serial behind it.
  7. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:blockedBlocked by another issue/PR — body carries Blocked-by: #Npm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions