Part of objectstack-ai/hotcrm#706. Successor to #5493, which closed via merged PR #6909 ("fix(sharing): defer to an app-authored RLS widener before hard-refusing a by-id write").
Measured on @objectstack/*17.0.0 GA during HotCRM's GA close-out sweep (hotcrm#1150 / #1152), real Bearer tokens against a running dev server.
What #6909 fixed, and what it did not
#5493's rc.2 symptom is gone: an app-authored RLS update-widener is now consulted on a direct by-id write to an object that carries sharing rules.
What still refuses is the derived write. crm_campaign_member is controlled_by_parent (ADR-0055) with master crm_campaign; its insert/update path first asks whether the caller may edit the MASTER. That check does not consult the master's RLS update policy — it resolves master editability through ownership and sys_record_share only. So the same principal, on the same master record, is simultaneously allowed to write the master and refused to write its children.
The isolation
One variable — who created the campaign. Everything else identical: same principal (marketing_user, which authors marketing_campaign_updates, an operation: 'update' RLS policy using: 'id != null' on crm_campaign), same objects, same payloads, same session.
| step | campaign created by ADMIN | campaign created by MARKETING |
|---|
| 1. PATCH the campaign itself (master, by id) | 200 | 200 |
2. INSERT a child (crm_campaign_member) | 403 | 201 |
| 3. UPDATE an admin-created child | 403 | 200 |
| 4. control: admin updates the same child | 200 | 200 |
5. security/explain update on the master, record-scoped | allowed=true | allowed=true |
Row 1 shows the widener in force in both columns; row 5 is the platform's own verdict agreeing. Rows 2 and 3 nonetheless refuse in the left column with:
[Security] Access denied: insert on 'crm_campaign_member' requires edit access to its
master record (master 'crm_campaign' not editable by this user (record sharing))
So security/explain and the master-editability check disagree about the same record, the same operation, and the same principal — the parenthetical names record sharing as the deciding layer, which is the layer #6909 taught to defer on the by-id path.
Why it matters
For HotCRM this is the app's "Add to Campaign" enrolment path — marketing's core write surface. On rc.2 the child widener worked and the master one did not; on GA the two have swapped, so the user-visible workflow is broken in the mirror direction. The app's declaration is unchanged and correct: an object grant plus an RLS update widener that says "marketing works any campaign".
Reproduction
# marketing_user token; a campaign it neither owns nor created
PATCH /api/v1/data/crm_campaign/:id {"description":"x"} -- 200
POST /api/v1/data/crm_campaign_member {"crm_campaign":":id", "crm_lead":"...", "status":"sent"}
-- 403, master not editable
# then repeat both against a campaign the same principal created -- 200 / 201Suggested direction
Have the ADR-0055 master-accessibility resolution for WRITE consult the same composition #6909 installed on the by-id path, so "may I edit this master" answers identically whether it is asked directly or on behalf of a child.
Evidence: hotcrm#1152 probe 31-706-isolate.mjs, readings posted on hotcrm#706.
Generated by Claude Code
Part of objectstack-ai/hotcrm#706. Successor to #5493, which closed via merged PR #6909 ("fix(sharing): defer to an app-authored RLS widener before hard-refusing a by-id write").
Measured on
@objectstack/*17.0.0 GA during HotCRM's GA close-out sweep (hotcrm#1150 / #1152), real Bearer tokens against a running dev server.What #6909 fixed, and what it did not
#5493's rc.2 symptom is gone: an app-authored RLS update-widener is now consulted on a direct by-id write to an object that carries sharing rules.
What still refuses is the derived write.
crm_campaign_memberiscontrolled_by_parent(ADR-0055) with mastercrm_campaign; its insert/update path first asks whether the caller may edit the MASTER. That check does not consult the master's RLS update policy — it resolves master editability through ownership andsys_record_shareonly. So the same principal, on the same master record, is simultaneously allowed to write the master and refused to write its children.The isolation
One variable — who created the campaign. Everything else identical: same principal (
marketing_user, which authorsmarketing_campaign_updates, anoperation: 'update'RLS policyusing: 'id != null'oncrm_campaign), same objects, same payloads, same session.crm_campaign_member)security/explainupdate on the master, record-scopedallowed=trueallowed=trueRow 1 shows the widener in force in both columns; row 5 is the platform's own verdict agreeing. Rows 2 and 3 nonetheless refuse in the left column with:
So
security/explainand the master-editability check disagree about the same record, the same operation, and the same principal — the parenthetical namesrecord sharingas the deciding layer, which is the layer #6909 taught to defer on the by-id path.Why it matters
For HotCRM this is the app's "Add to Campaign" enrolment path — marketing's core write surface. On rc.2 the child widener worked and the master one did not; on GA the two have swapped, so the user-visible workflow is broken in the mirror direction. The app's declaration is unchanged and correct: an object grant plus an RLS update widener that says "marketing works any campaign".
Reproduction
Suggested direction
Have the ADR-0055 master-accessibility resolution for WRITE consult the same composition #6909 installed on the by-id path, so "may I edit this master" answers identically whether it is asked directly or on behalf of a child.
Evidence: hotcrm#1152 probe
31-706-isolate.mjs, readings posted on hotcrm#706.Generated by Claude Code