Part of objectstack-ai/hotcrm#707 (never mirrored until now). Measured on @objectstack/*17.0.0 GA during HotCRM's GA close-out sweep (hotcrm#1150 / #1152), 40 export probes, 5 principals x 8 objects, real Bearer tokens.
The good half — the gate works
For every non-admin principal the 17.0 export inversion is exact. Every declared-allowed cell returns 200 with read-derived rows; every declared-denied cell returns 403 EXPORT_NOT_PERMITTED. modifyAllRecords does not substitute for allowExport. This is not asserted from a single call — the gate is shown to be ENFORCING three ways:
- C1 the same token that is refused on
crm_quote exports crm_lead 200 on the same route (so the 403 is the gate, not the token); - C2 granting
allowExport on crm_quote at runtime flips that 403 to 200; - C3 revoking it flips it back to 403.
The hole
The org owner (holding admin_full_access + organization_admin_no_bypass) exports crm_quote (9 rows), crm_campaign (13 rows) and crm_task (15 rows) with 200 and full data — objects on which no app permission set grants allowExport.
Attribution, measured rather than guessed
hotcrm#707 guessed a canExport admin bypass. It is not that. Both platform admin sets ship a wildcard export grant:
admin_full_access['*'].allowExport = true
organization_admin_no_bypass['*'].allowExport = true
member_default['*'] = undefined
and a wildcard export grant is honoured — proven on a principal with no admin status at all, crm_quote, one grant shape per row:
| grant shape on a plain org member | READ | EXPORT |
|---|
S1 '*'.allowExport = true | 200 | 200 |
S2 crm_quote.allowExport = true | 200 | 200 |
S2b '*'.allowExport = true + crm_quote.allowExport = false | 200 | 403 |
So the admin's 200 is the wildcard doing exactly what it says, not a bypass — and S2b shows the platform already honours a more-specific per-object false over the wildcard, i.e. a supported opt-out shape exists.
Why the app cannot use it
- The wildcard lives in code-package metadata. Editing it answers
403 [not_overridable] Metadata item 'permission/admin_full_access' is provided by a code package. - The org admin holds no app-authored permission set, so the app has nowhere to author the per-object
false that S2b proves would win.
Consequence: an app can declare "this object is exportable by nobody" and the platform will still export it for any org admin, with no supported way to opt out. That is the same shape hotcrm#703 raised about the old member_default create/read/edit wildcard — which was resolved by removing the wildcard (#5491 / PR #6684). The export axis kept its wildcard.
The card's own open question, now closed
hotcrm#707 recorded a caveat that a pure app system_admin-set holder was not constructible in that run, so the finding might be "org-owner bypasses" rather than "the admin set bypasses". Constructed here by binding the app's system_admin set to a plain member through sys_user_permission_set:
crm_quote declared allowExport=false -- export 403 EXPORT_NOT_PERMITTED
crm_campaign declared allowExport=false -- export 403 EXPORT_NOT_PERMITTED
crm_task declared allowExport=false -- export 403 EXPORT_NOT_PERMITTED
crm_account declared allowExport=true -- export 200 rows=19
crm_lead declared allowExport=true -- export 200 rows=25
An app admin set gets exactly the declared posture. The hole is specific to the platform org-admin sets and the wildcard they ship.
Suggested direction
Either drop allowExport from the '*' entry of the shipped admin sets (matching what #5491 did for the CRUD axes), or give apps a supported way to author a per-object export denial that reaches an org admin. A design decision either way, which is why this is filed rather than patched.
Evidence: hotcrm#1152 probes 40-707.mjs, 45-707-decisive2.mjs, 46-707-sysadmin.mjs; readings posted on hotcrm#707.
Generated by Claude Code
Part of objectstack-ai/hotcrm#707 (never mirrored until now). Measured on
@objectstack/*17.0.0 GA during HotCRM's GA close-out sweep (hotcrm#1150 / #1152), 40 export probes, 5 principals x 8 objects, real Bearer tokens.The good half — the gate works
For every non-admin principal the 17.0 export inversion is exact. Every declared-allowed cell returns 200 with read-derived rows; every declared-denied cell returns 403
EXPORT_NOT_PERMITTED.modifyAllRecordsdoes not substitute forallowExport. This is not asserted from a single call — the gate is shown to be ENFORCING three ways:crm_quoteexportscrm_lead200 on the same route (so the 403 is the gate, not the token);allowExportoncrm_quoteat runtime flips that 403 to 200;The hole
The org owner (holding
admin_full_access+organization_admin_no_bypass) exportscrm_quote(9 rows),crm_campaign(13 rows) andcrm_task(15 rows) with 200 and full data — objects on which no app permission set grantsallowExport.Attribution, measured rather than guessed
hotcrm#707 guessed a
canExportadmin bypass. It is not that. Both platform admin sets ship a wildcard export grant:and a wildcard export grant is honoured — proven on a principal with no admin status at all,
crm_quote, one grant shape per row:'*'.allowExport = truecrm_quote.allowExport = true'*'.allowExport = true+crm_quote.allowExport = falseSo the admin's 200 is the wildcard doing exactly what it says, not a bypass — and S2b shows the platform already honours a more-specific per-object
falseover the wildcard, i.e. a supported opt-out shape exists.Why the app cannot use it
403 [not_overridable] Metadata item 'permission/admin_full_access' is provided by a code package.falsethat S2b proves would win.Consequence: an app can declare "this object is exportable by nobody" and the platform will still export it for any org admin, with no supported way to opt out. That is the same shape hotcrm#703 raised about the old
member_defaultcreate/read/edit wildcard — which was resolved by removing the wildcard (#5491 / PR #6684). The export axis kept its wildcard.The card's own open question, now closed
hotcrm#707 recorded a caveat that a pure app
system_admin-set holder was not constructible in that run, so the finding might be "org-owner bypasses" rather than "the admin set bypasses". Constructed here by binding the app'ssystem_adminset to a plain member throughsys_user_permission_set:An app admin set gets exactly the declared posture. The hole is specific to the platform org-admin sets and the wildcard they ship.
Suggested direction
Either drop
allowExportfrom the'*'entry of the shipped admin sets (matching what #5491 did for the CRUD axes), or give apps a supported way to author a per-object export denial that reaches an org admin. A design decision either way, which is why this is filed rather than patched.Evidence: hotcrm#1152 probes
40-707.mjs,45-707-decisive2.mjs,46-707-sysadmin.mjs; readings posted on hotcrm#707.Generated by Claude Code