Uh oh!
There was an error while loading. Please reload this page.
fix(example-crm): bind sales positions to crm_sales_user permission set - #8167
Merged
os-zhuang merged 2 commits intoAug 12, 2026
Merged
Conversation
…m_sales_user The CRM example declared three positions and a crm_sales_user permission set that never met -- no sys_position_permission_set seeding, and the set was not isDefault (which would grant every user, not just these three). Every persona assigned one of the three positions resolved only the everyone baseline and was 403'd on every CRM object. Mirrors examples/app-showcase/src/security/bind-position-sets.ts: binds the three positions to crm_sales_user imperatively on kernel:bootstrapped (cannot be a declarative seed -- the seed loader runs before the security bootstrap creates the sys_position/sys_permission_set rows), wired via a new onEnable export in objectstack.config.ts. Measured with `objectstack verify --rls`: the three per-position probe personas go from 18/18 probe-blocked to 3/18 (crm_opportunity_line_item, ungranted by crm_sales_user -- filed separately as #8164, out of scope). Zero RLS holes in either state. Reverse-verified: disabling the hook restores the exact 18/18 pre-fix baseline. Fixes#8060 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
…k deferral The workspace-root tsc program (tsconfig.json at repo root) has no dom/node lib types. examples/app-crm/src/security/bind-position-sets.ts is pulled into that program transitively via scripts/analytics-reconcile/app-crm.ts, so the fallback branch's `setTimeout(...)` (unreachable in practice -- the real runtime PluginContext always provides `.hook`, and no test exercises this branch) was an undeclared global there: TS2304, +1 over the frozen @objectstack/spec-monorepo DEBT entry (80). Replaced with `void Promise.resolve().then(run)` -- a pure-ES microtask deferral needing no ambient global, equivalent in this unreachable branch. Verified in isolation (tsc --ignoreConfig under the root's exact compiler options) and via the full `pnpm check:type-check-debt` re-measure: the spec-monorepo entry is back to exactly 80. Part of #8060 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
os-zhuang
marked this pull request as ready for review
August 12, 2026 19:24
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang
deleted the
claude/issue-8060-crm-bind-positions-to-permission-set
branch
August 12, 2026 19:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#8060
What
examples/app-crmdeclared three positions (sales_rep,sales_manager,finance_approver) and acrm_sales_userpermission set granting CRUD on 5CRM objects, but nothing ever bound them together — no
sys_position_permission_setseeding, andcrm_sales_userwas notisDefault. Any user assigned one of the three positions therefore resolvedonly the platform
everyonebaseline and was 403'd on every CRM object.This mirrors
examples/app-showcase/src/security/bind-position-sets.tsexactly, per the triage ruling: a new
examples/app-crm/src/security/bind-position-sets.tsbinds the threepositions to
crm_sales_userimperatively onkernel:bootstrapped(adeclarative seed can't do this — the seed loader runs before the security
bootstrap creates the
sys_position/sys_permission_setrows), wired via anew
onEnableexport inobjectstack.config.ts.crm_sales_userisdeliberately not marked
isDefault— that would auto-bind it to theeveryoneanchor and grant every user the CRM permissions, changing theexample's security story rather than completing it (explicitly ruled out).
Measured (predict-then-mutate + reverse verification)
Reproduced the issue's own acceptance test with
objectstack verify --app examples/app-crm/objectstack.config.ts --rls:Before (confirmed matches the issue exactly):
After, per persona:
objects:6, probeBlocked:1, memberVisible:5.probeBlocked(no object grant at all — the by-id-write class neverexercised) drops from 18/18 → 3/18. The one remaining
probe-blockedobjectper persona is
crm_opportunity_line_item, whichcrm_sales_userdoes notgrant at all — a separate, pre-existing gap (filed as #8164, out of this
card's scope). Zero RLS holes introduced or found, in either state.
Reverse verification: disabled the
onEnablewiring, re-ran — readoutreturned to exactly the pre-fix 18/18
probeBlockedbaseline, confirming thehook (not something else) is the lever. Re-enabled and re-confirmed the fixed
readout before pushing.
A caveat on "proven": the report's strict
provenfield (⇒rls-consistent) stayed at 0 for the position personas even after the fix —not a defect of this change. All 5 now-granted CRM objects declare
sharingModel: 'public_read_write'("everyone reads+writes"), so once theobject gate opens, the record is visible to any authenticated holder by
design — the report classifies that as
member-visible(inconclusive), whichis correct and expected for this app, not
rls-consistent. Zero holes is thesignal that matters: no security regression.
Sharing rules (PM mechanism assumption #2)
The three
sharedWith: { type: 'position', value: … }sharing rules donot start mattering after this fix — measured, not assumed. They widen
record visibility on top of the object's OWD baseline, but every CRM object
they target is already
public_read_write(org-wide read+write for anyoneholding the object grant), so there is nothing left to widen. The sharing
rules remain inert on top of an already-maximal baseline; this is a property
of the objects' declared
sharingModel, unrelated to the binding fix.Out of scope
packages/plugins/plugin-security/**untouched, per the ruling.crm_opportunity_line_itemhas no object-level CRUD grant in any app-crm permission set — the platform's own build lint already flags it #8164 filed (unassigned, unlabeled) forcrm_opportunity_line_itemhavingno object-level CRUD grant in any app-crm permission set — the platform's
own build-time lint (
security-master-detail-ungranted) already flags thisindependently of the binding gap this card fixes.
Tests
objectstack verify --app examples/app-crm/objectstack.config.ts --rls(see above) — the issue's own acceptance test, run before/after/reverted.
cd examples/app-crm && npx vitest run— 27/27 passing (unchanged; no testfile added, following the showcase's own precedent of proving the binding
via the live
verify --rlsprobe rather than a unit test).cd examples/app-crm && npx tsc --noEmit— clean.pnpm --filter '@objectstack/example-crm...' build— clean, only thepre-existing (now separately filed)
security-master-detail-ungrantedwarning on
crm_opportunity_line_item.pnpm check:changeset-gate-self-tests,check:i18n-coverage(afterrebuilding the showcase closure to clear a stale-dist false positive
unrelated to this diff),
check:objectui-changeset,check-changeset-no-major.mjs,check:nul-bytes— all green. Notecheck:objectui-pin-fresh/check-objectui-pin-fresh.mjsare red, butpre-existing and unrelated (ambient objectui pin staleness, independent of
this diff's content).
Generated by Claude Code