Skip to content

fix(app-crm): hide the Pipeline nav entry from a member who cannot read opportunities - #27805

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-ch022k
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-ch022k

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread
Fixes #27735
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6512784198

维护者速读

  • 改了什么:CRM 侧栏的 Pipeline 入口加了一个显示条件 current_user.can('crm_opportunity', 'read'),读不到商机的成员看不到它;仪表盘本身、权限集和能力都不动。
  • 为什么改:没有销售权限集的成员原来侧栏有 Pipeline,点进去六个组件全被拒。按裁决 G 用「藏」而不是「拒」:首版声明能力的做法会让 CRM 不能非托管安装,种子管理员也会被拒。
  • 风险与回滚:只改示例的一个导航项和一个测试文件。拒绝仍由数据集查询的 403 负责,未变;种子管理员和销售用户照旧能看到。回滚就是 revert。
  • 先看:examples/app-crm/src/apps/crm.app.ts(一行)、examples/app-crm/test/pipeline-audience.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/example-crm exec vitest run test/pipeline-audience.test.ts,预期 2 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

Per ruling 6512784198 (letter G, which amends the mechanism of #27491 ruling 6508997538 item 5):

  • examples/app-crm/src/apps/crm.app.ts: nav_pipeline_dashboard gains visible: "current_user.can('crm_opportunity', 'read')". Nothing else on the entry changes. opportunity_metrics, the dataset every Pipeline widget reads, is bound to crm_opportunity.
  • The first revision's capability approach is fully reverted: no crm.view_pipeline declaration, no stack capabilities, no systemPermissions on crm_sales_user, and no requiredPermissions on pipeline_dashboard or its nav entry. The branch diff against main is the two files listed here.

visible hides and refuses nothing (ADR-0066 D4). The refusal stays the dataset query's 403 for a caller who cannot read crm_opportunity, unchanged. The server's meta read gate does not judge visible, so the seeded administrator's reads of the dashboard (the dogfood's managed-install lock control) are untouched, and the package stays installable unmanaged.

Acceptance (as amended by the ruling): 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 #27491 no-access panel (PR #27777), not a blank.

Tests

examples/app-crm/test/pipeline-audience.test.ts (new, 2 cases), read off the parsed stack:

  • the Pipeline nav entry carries a CEL visible of the form current_user.can(OBJECT, 'read') (the stack parse normalises the authored string to { dialect: 'cel', source });
  • OBJECT equals the object of the dataset that every pipeline_dashboard widget reads, derived from the definitions, not hard-coded.

Ablation: scripts/ablation-replace.mjs replacing crm_opportunity with crm_account in the predicate (anchor hit 1) turns the second case red (expected [ 'crm_opportunity' ] to deeply equal [ 'crm_account' ]), restored to the HEAD blob with git diff HEAD empty.

Acceptance notes

  • What the tests do not run: the console's nav guard that evaluates current_user.can (ui/packages/app-shell, objectui#4421). The example has no renderer harness; the pins cover the predicate's shape and its object.
  • No changeset: @objectstack/example-crm is private: true; labelled skip-changeset.
  • The first revision (head 87e10674) measured why a declared capability does not fit here: install-local-unmanaged-mode.dogfood.test.ts went 11 failed / 9 passed (422 UNMANAGED_INSTALL_UNCOPYABLE on the stack's capabilities, and 403 audience-permission for the seeded administrator), against 20/20 at the merge-base. Recorded in the ruling.

Verification

Local, at a1cd6d8d (after merging origin/main once more):

  • pnpm --filter @objectstack/example-crm test: exit 0, Test Files 7 passed (7), Tests 48 passed (48); typecheck exit 0 (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/install-local-unmanaged-mode.dogfood.test.ts (closure built first): exit 0, Test Files 1 passed (1), Tests 20 passed (20). At the first revision 87e10674 the same file was 11 failed / 9 passed.
  • node scripts/pm/dispatch-gates.mjs --ran: 39 derived, 38 run, 1 NOT-MEASURED (check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET: needs every package's dist; CI's). Three families first answered from a mid-build tree and were re-run after the build settled: check:dts-closure (exit 1 naming @objectstack/runtime / service-automation while the dogfood closure was rebuilding, then driver-odata, new on main, unlinked until pnpm install) exit 0; check:i18n-coverage exit 0 ("OK (14 config(s), 527 baselined untranslated string(s), none new)"); check:type-check-debt exit 0 ("0 raw tsc error(s) total, none above its recorded number"). The diff touches no package, so none of these read it beyond the example.
  • node scripts/check-sdui-manifest.mjs: exit 0.
Derived gate families, exit codes (final readings)
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:examples-live-imports :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n-coverage :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0

Generated by Claude Code

…ad opportunities

Per ruling 6512784198 (letter G): a visible predicate on the nav entry
instead of a declared capability; the capability, its grant and the
dashboard's requiredPermissions are reverted.

Claude-Session: https://claude.ai/code/session_01Utp6CXo8iLQ8vn2fXj6wxp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s and removed size/m labels Oct 30, 2026
@objectstack-fleet objectstack-fleet Bot changed the title fix(app-crm): gate the Pipeline dashboard and its nav entry on a sales capability fix(app-crm): hide the Pipeline nav entry from a member who cannot read opportunities Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head a1cd6d8d4e737e5a810b17e88714ddeaf2a9a60b · 2026-10-30T02:19Z

  1. Shape: draft → main, Fixes #27735, Ruling-ref: 6512784198 (letter G), attribution block on top, label skip-changeset (examples/app-crm is private and ships nothing). Two files: examples/app-crm/src/apps/crm.app.ts (+6 / −1) and examples/app-crm/test/pipeline-audience.test.ts (+62). No assignee, no reviewer.
  2. Ruling met (seat read the diff):
    • nav_pipeline_dashboard gains visible: "current_user.can('crm_opportunity', 'read')", and nothing else on the entry changes.
    • Every capability change from the first revision is reverted: no capabilities declaration, no systemPermissions, no requiredPermissions. Nothing in packages/qa, and no demoStaff/devLogins change.
    • The two pins read the PARSED stack: the predicate has the form current_user.can(OBJECT, 'read') (CEL), and OBJECT equals the one object behind the dataset of every pipeline_dashboard widget. It is derived, not hard-coded.
    • The dev's ablation (predicate → crm_account) turns the second pin red.
    • A direct link still lands on 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's no-access panel, because the dataset 403 stays the enforcement (ADR-0066 D4).
  3. Gates (check-runs on this head): 28 success, 17 skipped, 0 red.
    • Named runs: Lint & Repo Gates 123849635735, TypeScript Type Check 123849617403, Test Core 123851781344.
    • CI skipped the dogfood shards on this path set. The dev ran install-local-unmanaged-mode.dogfood.test.ts locally at this head: exit 0, 20/20. That is the file the first revision turned red 11/20. The example-crm tests ran 48/48, and check-sdui-manifest exited 0.
    • Derived gates: 38 of 39 exit 0; 1 NOT-MEASURED (check:dual-build-cjs-loads, exit 3 prerequisite, CI's).
  4. Contract review: not owed. Clause-②: no, Metadata-protocol: none (an example app's nav metadata uses an existing key).
  5. Deviations: none from the ruling. The card's acceptance line 2 now reads "a direct link lands on 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 panel", as the ruling directed.
  6. Landing: not governed (check-governed-merges --pr 27805), +68 / −1. The seat lands it: ready + auto-merge now.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 02:21
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 02:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit da94c11 Oct 30, 2026
46 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ch022k branch October 30, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

1 participant