Part of objectstack-ai/hotcrm#706
Found during HotCRM's 17.0 GA acceptance sweep on @objectstack/* 17.0.0-rc.2. This is a regression against runtime-verified behavior: HotCRM #547 added two RLS update-wideners and its PR body records live verification (member insert as marketing_user → 201).
Measured on rc.2 (marketing_user, own token)
| Widener |
Object |
rc.2 behavior |
marketing_campaign_member_updates |
crm_campaign_member (no sharing rules of its own) |
✅ works — PATCH admin-owned member → 200 (control: sales_rep → 403) |
marketing_campaign_updates |
crm_campaign (carries sharing rules) |
❌ PATCH not-own campaign → 403 "FORBIDDEN: insufficient privileges to update crm_campaign" |
The error shape is the tell: the failing denial is the sharing-middleware FORBIDDEN, not the row-gate's "[Security] … (row-level security)" shape every other write denial in the sweep produced. The failing layer answers before RLS evaluation on objects that carry sharing rules — the widener is never consulted on crm_campaign, while on rule-less crm_campaign_member it is.
Reproduction
# as marketing_user
PATCH /api/v1/data/crm_campaign/ {"description":"x"} → 403 FORBIDDEN (middleware shape)
PATCH /api/v1/data/crm_campaign_member/ {"status":"responded"} → 200
Impact
Marketing can move members but cannot edit campaigns they didn't create — the exact posture the wideners were written to fix, failing subtly (half the mechanism works).
App declarations unchanged since they were verified working; attribution is the ordering of sharing middleware vs RLS widener evaluation on objects with sharing rules.
Evidence: HotCRM acceptance run w4/results.jsonl (M5). Related sibling: objectstack-ai/hotcrm#705 (write gate ignores share access levels — a different layer, same subsystem).
Blocked-by: #6735
(2026-08-08 identity PM note: maintainer ruled Q1=A / Q2=A1 — see comment 5226389104. Step 1 = #6735 (ISecurityService authored-row-write verdict, contract + plugin-security implementation, spec-seat transfer). This card is step 2: plugin-sharing consumption + the substance-bearing flip of the #5493 control in row-write-widener-composition.test.ts, dispatched by the identity lane after #6735 merges. Bulk half tracked as #6736. Prior line "Blocked-by: #5492" resolved via PR #6684, superseded by this one. Mechanism refinement from the round-2 dev: the discriminator is "does record sharing enforce on this object at all" (checkEdit deny vs abstain), not "carries sharing rules" — see comment 5226364929.)
Part of objectstack-ai/hotcrm#706
Found during HotCRM's 17.0 GA acceptance sweep on
@objectstack/*17.0.0-rc.2. This is a regression against runtime-verified behavior: HotCRM #547 added two RLS update-wideners and its PR body records live verification (member insert as marketing_user → 201).Measured on rc.2 (marketing_user, own token)
marketing_campaign_member_updatescrm_campaign_member(no sharing rules of its own)marketing_campaign_updatescrm_campaign(carries sharing rules)The error shape is the tell: the failing denial is the sharing-middleware FORBIDDEN, not the row-gate's "[Security] … (row-level security)" shape every other write denial in the sweep produced. The failing layer answers before RLS evaluation on objects that carry sharing rules — the widener is never consulted on
crm_campaign, while on rule-lesscrm_campaign_memberit is.Reproduction
Impact
Marketing can move members but cannot edit campaigns they didn't create — the exact posture the wideners were written to fix, failing subtly (half the mechanism works).
App declarations unchanged since they were verified working; attribution is the ordering of sharing middleware vs RLS widener evaluation on objects with sharing rules.
Evidence: HotCRM acceptance run
w4/results.jsonl(M5). Related sibling: objectstack-ai/hotcrm#705 (write gate ignores share access levels — a different layer, same subsystem).Blocked-by: #6735
(2026-08-08 identity PM note: maintainer ruled Q1=A / Q2=A1 — see comment 5226389104. Step 1 = #6735 (ISecurityService authored-row-write verdict, contract + plugin-security implementation, spec-seat transfer). This card is step 2: plugin-sharing consumption + the substance-bearing flip of the #5493 control in
row-write-widener-composition.test.ts, dispatched by the identity lane after #6735 merges. Bulk half tracked as #6736. Prior line "Blocked-by: #5492" resolved via PR #6684, superseded by this one. Mechanism refinement from the round-2 dev: the discriminator is "does record sharing enforce on this object at all" (checkEdit deny vs abstain), not "carries sharing rules" — see comment 5226364929.)