Skip to content

app-crm: declare the Pipeline dashboard's audience gate (requiredPermissions on pipeline_dashboard and nav_pipeline_dashboard), so a member who cannot read its data is not offered it #27735

Closed

Description

@objectstack-fleet

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Filing gate: ② a wrong state offered to the user. Filed by the seat on the maintainer's ruling 6508997538 on #27491, item 5: 「C runs beside A, not instead: domain:ui#2 files one S card in the app lane」. ⛔ Not paired with #27491's PR.

Business reason

Who: a CRM member with no sales permission set. What happens today: the CRM landing says "Nothing in CRM is available to you yet", but the sidebar still offers Analytics → Pipeline. Clicking it opens a dashboard every tile of which the server refuses (all six widgets read opportunity_metrics). Once #27491 lands, that member gets a no-access panel instead of a blank canvas. The app still offers them a page they cannot use.

Mechanism (read at origin/main)

Acceptance

  • pipeline_dashboard and nav_pipeline_dashboard declare the same requiredPermissions, with a capability that the CRM sales set(s) able to read the dashboard's dataset actually grant.
  • A plain member (no sales set): the CRM sidebar has no Pipeline entry, and a direct link to the dashboard gets dashboard-access-denied.
  • A sales user: sees Pipeline and its tiles as today.
  • Pins in the app's tests (examples/app-crm/test/**, beside the smoke test). Both rows are checked through the server's read gate, not a client guess.

Reader who acts

The lane that owns examples/app-crm (triage names it; app lane). Size S.

reach

Public door: CRM example → sidebar → Analytics → Pipeline, as any member without a sales permission set (the 18.3.0 RC walkthrough's member, #27377 W3-UX).

Dedupe

No existing card asks for the CRM dashboard's audience gate. I read the #27491 ruling 6508997538 (it names this card as not yet filed) and the 100 most recent open domain:ui / app issues by title for requiredPermissions, pipeline_dashboard and audience.

Links back: #27491 (ruling 6508997538, item 5)


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 经营报表 | none | P②

    Triage: p2 · domain:ui · area:reports · 2026-10-29T21:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    Claim: PM loop round 3 · 2026-10-29T23:15Z
    Session: session_01Utp6CY2n1JmZZKrHgtp9jC (seat; implementation by a card thread, mode:thread)
    Account: os-bill
    Branch: claude/project-thread-ch022k (the card thread's harness push branch, self-reported — the measured platform exception to claude/issue-N-slug)
    Worktree: objectstack-issue-27735
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: examples/app-crm/src/dashboards/pipeline.dashboard.ts, examples/app-crm/src/apps/crm.app.ts, examples/app-crm/src/security/** (one capability, granted in crm_sales_user), examples/app-crm/test/**, and — widened by the seat on the dev's stop-and-report — examples/app-crm/objectstack.config.ts (one capabilities: [...] line plus its import, so the capability is DECLARED on the stack: delegated-admin-gate.ts condition (c) refuses an undeclared systemPermissions name); no changeset (the example is private: true; skip-changeset); ⛔ package.json (no new devDependency); ⛔ packages/spec/**, packages/rest/**, every ui/** file
    Container & model: S, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no (expected; the dev measures from the diff)
    Metadata-protocol: none (expected; the dev measures from the diff)
    Responsibility: the CRM example declaring no audience gate on its Pipeline dashboard and nav item | the server's requiredPermissions read gate (ADR-0066) and the nav guard are fixed inputs | every CRM member without a sales permission set who is offered Pipeline today
    Thread-read: 6509200885
    Premise: holds as filed (the seat read origin/main while filing: neither declaration carries requiredPermissions, and crm_sales_user grants no capability)
    Serial constraints cleared: no open PR touches examples/app-crm/** (seat read of open PRs' files); the renderer half is PR #27777 (#27491), not paired; seats 3 and 4 agreed to the split

    Dispatch order: /mnt/project-files/ui-seat2/dispatch-27735.md in the Claude Code project (not on GitHub); its ruling section quotes the triage text above verbatim.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    os-dev-report

    {
      "issue": 27735,
      "status": "done",
      "branch": "claude/project-thread-ch022k",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27805",
      "session": "session_01Utp6CXo8iLQ8vn2fXj6wxp (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)",
      "premise_still_valid": true,
      "summary": "Added crm.view_pipeline (defineCapability, scope org, packageId com.example.crm) on the CRM stack's capabilities, granted it in crm_sales_user (systemPermissions), and declared requiredPermissions ['crm.view_pipeline'] on pipeline_dashboard and nav_pipeline_dashboard. Readers: createMetaItemReadGate dashboard arm (audience-permission 403) and filterAppForUserWithReason nav pruning, both in packages/rest/src/meta-item-read-gate.ts, against the union of the caller's sets' systemPermissions. The declaration (not only the grant) is required: plugin-security's organization boundary condition (c) refuses a set carrying an undeclared or platform-scoped capability. objectstack.config.ts added to the surface by the seat (claim amended). Named gap: the example has no @objectstack/rest dependency, so tests pin every input the gate reads, not the gate run (seat declined the devDependency).",
      "tests": "pnpm --filter @objectstack/example-crm test: exit 0, Test Files 7/7, Tests 50/50 (new test/pipeline-audience.test.ts, 4 cases); typecheck exit 0; objectstack validate exit 0. Ablation (no build/dist path: example reads its own src): scripts/ablation-replace.mjs deleting the crm_sales_user systemPermissions line, anchor hit 1, holders case red (1 failed | 3 passed), restored blob == HEAD 314586f62842, git diff HEAD empty.",
      "gates": "local at 87e10674: dispatch-gates --ran: 39 derived, 38 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, needs every package dist; CI's). check:i18n-coverage first exit 3 (missing connector-slack dist), rerun after its named prerequisite build: exit 0 OK none new. node scripts/check-sdui-manifest.mjs exit 0. CI check-runs: in_progress at report time.",
      "line_budget": "7 files, +~150 (one new test file ~80, new capabilities.ts ~35, declarations ~15)",
      "files_changed": [
        "examples/app-crm/objectstack.config.ts",
        "examples/app-crm/src/apps/crm.app.ts",
        "examples/app-crm/src/dashboards/pipeline.dashboard.ts",
        "examples/app-crm/src/security/capabilities.ts",
        "examples/app-crm/src/security/index.ts",
        "examples/app-crm/src/security/sales-positions.ts",
        "examples/app-crm/test/pipeline-audience.test.ts"
      ],
      "deviations": [
        "objectstack.config.ts outside the dispatched surface: stopped and asked; seat granted and amended claim 6510769461 before PR",
        "PR attribution link names this card thread (session-context requires it verbatim), not the seat thread link the dispatch spelled",
        "first branch push (WIP) went out before the seat's surface answer; it carried the config line",
        "no changeset: @objectstack/example-crm is private; skip-changeset label on pr_create (read back present)",
        "harness post-PR assign/review step skipped per dispatch; PR has no assignee and no reviewer"
      ],
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub calls",
      "api_writes": "2 — relay pr_create (draft, labels skip-changeset) run 40976175747, body read back identical 7309 bytes; this os-dev-report comment. Plus git pushes (not REST).",
      "open_questions": [],
      "out_of_scope_findings": [
        "none filed. Noted: packages/spec/src/ui/dashboard.zod.ts JSDoc near requiredPermissions still says not enforced (already recorded by the #27491 review thread); carrier: #27491 ruling thread · noted, not filed"
      ]
    }
  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    director#1 · os-bill · session_01Utp6CavdFhhfMVhuGjp7tP

    Ruling: the CRM Pipeline entry's audience gate, after the measured cost of a declared capability (project discussion thread 「决定仪表盘全部无权限时的显示」, follow-up to #27491 ruling 6508997538 item 5) · letter G · maintainer 「同意」 2026-10-30T01:36Z

    The maintainer's own ruling, given in the project thread at 2026-10-30T01:34Z on the review thread's six-section brief and its post-measurement addendum (/mnt/project-files/decisions/27735-种子管理员失去Pipeline-呈批.md, recommendation G). Marker: maintainer-ruled, presented by the review thread; not a proxy ruling. It amends the MECHANISM of #27491 ruling 6508997538 item 5; that item's intent (a member who cannot read the Pipeline's data is not offered it) stands.

    What was measured and re-verified

    • PR fix(app-crm): hide the Pipeline nav entry from a member who cannot read opportunities #27805 at 87e10674 (declared capability crm.view_pipeline, granted to crm_sales_user, required on pipeline_dashboard and nav_pipeline_dashboard). The card thread ran packages/qa/dogfood/test/install-local-unmanaged-mode.dogfood.test.ts against it: 11 failed / 9 passed; the control (the CRM at the merge-base, same build) 20/20. PR CI was green only because the dogfood shards were skipped on the PR.
    • 10 failures: a stack-level capabilities declaration makes the CRM package refuse an UNMANAGED install, 422 UNMANAGED_INSTALL_UNCOPYABLE. By design: the capability metadata type is allowRuntimeCreate: false (packages/spec/src/kernel/metadata-plugin.zod.ts L1242, ADR-0131 D3 / ADR-0066 D1: a capability lives in the registry and is never an environment row), and the unmanaged import refuses whole any body carrying a runtime-uncreatable type (packages/metadata-protocol/src/package-unmanaged-install.ts L33). The dogfood uses the CRM as its unmanaged-install specimen. A package that declares capabilities cannot be installed unmanaged; this card does not change that.
    • 1 failure (L460): the seeded admin's GET /meta/dashboard/pipeline_dashboard answers 403 audience-permission. holdsRequiredPermissions (packages/rest/src/meta-item-read-gate.ts L753) has no administrator bypass; the dashboard arm (L1690–1700) judges the audience gate before policy.arms, author or not, so the gate also closes the dashboard in Studio for a caller who lacks the capability. The seeded admin holds ADMIN_FULL_ACCESS_CAPABILITIES (eval-user.zod.ts L115): every platform capability, no app-declared one (system/dev-login.zod.ts L14–27 documents this as by construction).
    • Granting without declaring is not a way out: packages/plugins/plugin-security/src/delegated-admin-gate.ts L875–906, organization boundary condition (c): a systemPermissions name that is not a capability declared scope: 'org' puts the set outside the boundary, so an organization administrator could no longer assign crm_sales_user or the three sales positions that distribute it (ADR-0090 D12).
    • The nav guard already binds current_user.can(object, verb) from the member's permissions payload (objectui#4421; ui/packages/app-shell/src/hooks/useNavMemberGates.ts, ExpressionProvider.tsx), and BaseNavItemSchema.visible is a CEL predicate. opportunity_metrics is bound to crm_opportunity (examples/app-crm/src/datasets/opportunity.dataset.ts L17), the object whose read refusal is the six tiles' 403. visible is not judged by the server's read gate (meta-item-read-gate.ts L918), so the dogfood's admin reads are untouched.

    Governing text

    The ruling: G

    The CRM example hides the Pipeline entry from a member who cannot read opportunities with a visibility predicate on the navigation item. No capability is declared or granted, and the dashboard itself carries no requiredPermissions.

    Fixed here:

    1. examples/app-crm/src/apps/crm.app.ts, nav_pipeline_dashboard: add visible: "current_user.can('crm_opportunity', 'read')". Nothing else on the entry changes.
    2. PR fix(app-crm): hide the Pipeline nav entry from a member who cannot read opportunities #27805 reverts its other changes: src/security/capabilities.ts (delete), src/security/index.ts, objectstack.config.ts (capabilities), src/security/sales-positions.ts (systemPermissions), src/dashboards/pipeline.dashboard.ts (requiredPermissions).
    3. examples/app-crm/test/pipeline-audience.test.ts pins two things, read off the parsed stack: the Pipeline nav entry carries a visible predicate of the form current_user.can('<object>', 'read'), and that <object> equals the object of the dataset every pipeline_dashboard widget reads. The capability-centred cases are retired with the capability.
    4. Acceptance for app-crm: declare the Pipeline dashboard's audience gate (requiredPermissions on pipeline_dashboard and nav_pipeline_dashboard), so a member who cannot read its data is not offered it #27735 reads: a member who cannot read crm_opportunity sees no Pipeline entry in the CRM sidebar; a sales user and the seeded administrator see it as today; a direct link for the refused member shows the Dashboard: a viewer whose analytics queries are all refused 403 sees a blank canvas under the title (CRM Pipeline Dashboard as a plain member), with no per-widget refusal #27491 no-access panel (PR fix(plugin-dashboard): a viewer whose every tile is refused sees one no-access panel, not a blank canvas #27777), not a blank. The earlier "direct link gets dashboard-access-denied" acceptance is withdrawn.
    5. Verification before push: the example-crm package tests, and install-local-unmanaged-mode.dogfood.test.ts run locally once more, expected back to 20/20; both recorded in the PR body.
    6. ⛔ Not: a declared capability on the CRM stack (A — makes the package un-installable unmanaged); a grant with no declaration (A′ — condition (c)); a platform administrator bypass in holdsRequiredPermissions (D — changes ADR-0066's meaning); demoStaff / devLogins for this card (not needed under G; the seeded admin keeps the entry); any change under packages/qa/dogfood.

    Why G, on the four axes (business standpoint)

    • ① Long-term soundness (≥50%). The platform has two keys for two jobs, and this card's job is the hide. Using the refuse key cost the example its unmanaged installability and closed the dashboard to its own author in Studio; the hide key costs nothing the platform has not already paid for type: 'object' entries (objectui#12109 prunes those on the same payload). The data refusal stays the one enforcement.
    • ② Measured business pull. The 18.3.0 RC walkthrough member ([sweep] v18.3.0 release test sweep #27377 W3-UX) sees the landing say nothing is available while the sidebar offers Pipeline; G closes that gap for every member without opportunity read.
    • ③ AI-error prevention. G's failure direction: a frame with no permissions payload withholds the entry and reports once (objectui#12700), never shows it to everyone; a wrong object name in the predicate is caught by the new pin against the dataset's object. A and A′ failed loud too, but in a required CI gate that the PR's own CI had skipped.
    • ④ Startup no-sprawl. Two files in examples/app-crm, S. A was M across packages/qa; C left the inconsistency in place.

    Only-① pick: G. Do ②③④ flip it: no. Effort for the chosen letter: S.

    ① 长远:藏用 visible、拒用 requiredPermissions(ADR-0066 D4 原话),本卡要的是藏;声明能力让 CRM 不能非托管安装、作者在 Studio 也打不开,代价不是这张卡该付的;对象类导航条目早已按同一权限载荷裁剪(objectui#12109)。
    ② 拉动:RC 走查成员(#27377 W3-UX)首页说没东西、侧栏却挂 Pipeline;G 对每个读不到机会的成员都关上这个口。
    ③ 防错:无权限载荷的框架按 objectui#12700 收起条目不报错;谓词里的对象名由新钉对照数据集 object;A/A′ 的红出现在 PR CI 跳过的必需门里,是 dogfood 实跑才看见的。
    ④ 不扩散:两个文件,S;A 要跨 packages/qa 换样本(M);C 留着不一致。
    Prior rulings read: #27491 6508997538 item 5(本裁修订其机制)、ADR-0066 D1/D4、ADR-0131 D3、ADR-0090 D12 条件 (c)、objectui#4421 / #12109 / #12700、hotclm#28 D(demoStaff,本裁不用);实测:卡线程 dogfood 11/9 与对照 20/20。

    Execution after the ruling


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    os-dev-report

    Revision 2 (Ruling-ref: 6512784198), head a1cd6d8.

    {
      "issue": 27735,
      "status": "done",
      "branch": "claude/project-thread-ch022k",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27805",
      "session": "session_01Utp6CXo8iLQ8vn2fXj6wxp (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)",
      "premise_still_valid": true,
      "summary": "Revision 2 per ruling 6512784198 (letter G). nav_pipeline_dashboard gains visible current_user.can('crm_opportunity', 'read'); the first revision's capability, stack capabilities, crm_sales_user systemPermissions and both requiredPermissions are reverted, so the branch diff vs main is crm.app.ts (one entry) plus the new test. Earlier finding answered: seeded admin and unmanaged install unaffected, since the server read gate does not judge visible. PR retitled and body rewritten with Ruling-ref.",
      "tests": "pnpm --filter @objectstack/example-crm test exit 0, 7/7 files, 48/48 tests (pipeline-audience.test.ts: 2 cases, read off the parsed stack, OBJECT derived from the widgets' dataset); typecheck exit 0. Dogfood install-local-unmanaged-mode.dogfood.test.ts exit 0, 20/20 at a1cd6d8d (was 11 failed / 9 passed at 87e10674). Ablation: predicate object crm_opportunity to crm_account via scripts/ablation-replace.mjs, anchor hit 1, second case red, restored blob == HEAD, git diff HEAD empty.",
      "gates": "local at a1cd6d8d: dispatch-gates --ran 39 derived, 38 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads exit 3 prerequisite, CI's). Re-run after the build settled: check:dts-closure (first exit 1, mid-build tree and the newly merged driver-odata unlinked until pnpm install) exit 0; check:i18n-coverage (first exit 3) exit 0; check:type-check-debt (first exit 3, driver-odata build) exit 0. check-sdui-manifest exit 0. CI check-runs: in_progress at report time.",
      "line_budget": "2 files vs main: crm.app.ts +6/-1, test +60",
      "files_changed": [
        "examples/app-crm/src/apps/crm.app.ts",
        "examples/app-crm/test/pipeline-audience.test.ts"
      ],
      "deviations": [
        "PR title and body edited by issue_patch through the relay (ruling's execution step asks the card thread to retitle and amend); not a PATCH outside a ruling",
        "PR attribution link names this card thread (accepted by the seat)",
        "not run: the console nav guard that evaluates current_user.can (no renderer harness in the example), named in Acceptance notes"
      ],
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one dogfood file the seat named was run: 20/20",
      "mcp_calls": "0",
      "api_writes": "4 total on this card thread: pr_create (rev 1), os-dev-report 6511354813 (rev 1), issue_patch #27805 title+body run 40989829672 (read back identical 7347 bytes), this report. Plus git pushes.",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  6. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    ACCEPT → PR #27805 comment 6513357745 (head a1cd6d8d), done to ruling G (6512784198). Contract review not owed (Clause-②: no, Metadata-protocol: none). Landing through the merge queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    Landed · 2026-10-30T02:38Z

    probe reading
    PR #27805 merged through the queue at 2026-10-30T02:38Z, merge commit da94c11337fdb4c37ee809e9748a76791a5199cc
    this card closed completed by the PR's Fixes line

    State: pm:dispatched removed; the assignee stays as the record of who carried it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:uipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions