|
| 1 | +--- |
| 2 | +"@objectstack/plugin-security": major |
| 3 | +--- |
| 4 | + |
| 5 | +<!-- adr-0087: not-required (no-migration-prescription) what this change removes is a VALUE in one platform-seeded sys_permission_set row, not an authorable key. No spec schema key is retired: object_permissions['*'] stays fully authorable, and admin_full_access / organization_admin / viewer_readonly still ship one. Nothing an app authored becomes invalid, nothing stored fails to parse, and the seeded row itself is rewritten by the boot seeder, so there is no stored shape for `objectstack migrate meta` to rewrite and nothing for the ledger to carry. The Migration section below prescribes a DEPLOYMENT action -- declare the object access you were relying on -- not a consumer code or metadata rewrite. --> |
| 6 | + |
| 7 | +fix(plugin-security)!: `member_default` no longer grants a `*` wildcard — the platform baseline is explicit-allow (#5491) |
| 8 | + |
| 9 | +**This is a deliberate, breaking narrowing of the default security posture. |
| 10 | +Deployments that relied on the implicit wildcard lose that access. That is the |
| 11 | +intended behaviour change, not a side effect — read the migration below before |
| 12 | +upgrading.** |
| 13 | + |
| 14 | +`member_default` is the additive `everyone` baseline: it resolves for **every** |
| 15 | +authenticated member, in addition to whatever else they hold. It carried |
| 16 | +`object_permissions["*"] = {allowCreate: true, allowRead: true, allowEdit: true, |
| 17 | +allowDelete: false}`, and object permissions merge most-permissively — so that |
| 18 | +entry was not a default, it was a **floor no application could get under**. An |
| 19 | +app's explicit-allow object gate was erased on three of the four axes; only |
| 20 | +delete stayed profile-driven, because the baseline never granted it. |
| 21 | + |
| 22 | +HotCRM's 17.0 GA sweep measured the consequence across 5 profiles × 17 objects |
| 23 | +(188 probes, each user with their own bearer token): |
| 24 | + |
| 25 | +- **21 of 21 create-DENIAL probes returned `201`** — every profile created on |
| 26 | + every object once validation passed, including objects the profile explicitly |
| 27 | + denied; |
| 28 | +- a `service_agent` profile that declares no edit anywhere edited its own |
| 29 | + `crm_account`; |
| 30 | +- on `public_read` objects the wildcard yielded **`200` with ALL rows** for |
| 31 | + non-holders — real unauthorized reads, not the documented "200 with 0 rows" |
| 32 | + empty-set pattern; |
| 33 | +- `security/explain` stated it outright for a profile carrying an all-false |
| 34 | + deny: *"create on 'crm_opportunity' is granted by [member_default]"*. |
| 35 | + |
| 36 | +Because app-side authorization suites validate the app's *declarations*, CI |
| 37 | +stayed green while the runtime posture was default-open — `declared ≠ enforced` |
| 38 | +inside the security layer itself. |
| 39 | + |
| 40 | +**The change.** The wildcard is removed on all three live axes. The platform |
| 41 | +baseline narrows to explicit-allow: object access now comes from OWDs plus |
| 42 | +profile / permission-set **declarations** only. Deny-precedence merge semantics |
| 43 | +were considered and rejected — permission sets remain additive capability |
| 44 | +containers (ADR-0090); the fix is to stop the platform shipping a grant nobody |
| 45 | +asked for, not to invent a veto. |
| 46 | + |
| 47 | +What `member_default` still declares, it still enforces, and nothing here is |
| 48 | +newly granted: read on the better-auth identity tables (their writes stay |
| 49 | +denied — that door is better-auth), self-service on `sys_user_preference` (now |
| 50 | +an explicit entry rather than an implicit one; the effective access for a member |
| 51 | +is byte-identical, and its `sys_user_preference_self` RLS carve-out already |
| 52 | +declared exactly that intent), and every row-level policy it shipped before — |
| 53 | +`owner_only_writes`, `owner_only_deletes` and the identity `_self` carve-outs |
| 54 | +are untouched. The set stays anchor-safe, so its `everyone` binding is |
| 55 | +unaffected. `admin_full_access`, `organization_admin` and `viewer_readonly` keep |
| 56 | +their wildcards: those are granted deliberately to a principal, which is exactly |
| 57 | +what the baseline was not. |
| 58 | + |
| 59 | +## Migration |
| 60 | + |
| 61 | +After upgrading, a member holding **no** application profile has no access to |
| 62 | +application objects. Restore access by declaring it, in one of two places: |
| 63 | + |
| 64 | +1. **Ship an app default profile.** Mark a permission set `isDefault: true` and |
| 65 | + the CLI wires it as the additive per-request baseline (ADR-0056 D7 / |
| 66 | + ADR-0090 D5). This is the recommended route and what the bundled showcase app |
| 67 | + already does — list the objects members legitimately touch, with the axes |
| 68 | + they need. |
| 69 | +2. **Grant per position / per user.** Bind an ordinary permission set through |
| 70 | + `sys_position_permission_set` or `sys_user_permission_set`. |
| 71 | + |
| 72 | +To find what a deployment was silently relying on, ask |
| 73 | +`GET /api/v1/security/explain?object=<name>&operation=<op>` for a |
| 74 | +representative member before upgrading: any answer attributing the grant to |
| 75 | +`[member_default]` on an application object is access that will stop. An app |
| 76 | +whose own profiles already declare everything its users do is unaffected. |
0 commit comments