|
| 1 | +--- |
| 2 | +'@objectstack/spec': major |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec)!: retire the two fail-open app-area gates — `app.areas[].visible` and `app.areas[].requiredPermissions` (#4651) |
| 6 | + |
| 7 | +These were **not** inert authoring keys. They were capability gates that **failed |
| 8 | +open**: an author wrote `requiredPermissions: ['sales.admin']` on a navigation |
| 9 | +area, got a clean parse and a stored value, and the area — with everything under |
| 10 | +it — was served and rendered to **every user**. |
| 11 | + |
| 12 | +**This is a breaking change with a real migration.** Both keys are authorable |
| 13 | +metadata keys on a `.strict()` schema, so existing `app` metadata that declares |
| 14 | +either one now **fails to parse** with the prescription below. `authorable-surface.json` |
| 15 | +is net **−2 keys**. This is not the "zero metadata migration" shape of the |
| 16 | +same-window renames (#4661 C8, #4684 C9) — those kept every key. |
| 17 | + |
| 18 | +**The retirement kit:** |
| 19 | + |
| 20 | +| FROM | TO | Fix | |
| 21 | +|---|---|---| |
| 22 | +| `app.areas[].requiredPermissions` | *(removed)* | Delete the key. Gate each of the area's `navigation` items with `requiredPermissions` / `requiresService`, or gate the whole app with `requiredPermissions` on the AppSchema. | |
| 23 | +| `app.areas[].visible` | *(removed)* | Delete the key. Move the same CEL expression onto the area's `navigation` items — a navigation **item**'s `visible` is evaluated per item by the shell. | |
| 24 | + |
| 25 | +The retired alias spellings `visibleWhen` / `visibleOn` / `permissions` carry the |
| 26 | +same prescriptions rather than renaming onto keys that are themselves gone. |
| 27 | + |
| 28 | +Run `os migrate meta --from 16` to rewrite existing sources automatically |
| 29 | +(ADR-0087 D2 conversion `app-area-fail-open-gates-removed`, wired into the |
| 30 | +protocol-17 D3 chain step). |
| 31 | + |
| 32 | +**Why they read alive — and why that made them worse than dead.** The *same key |
| 33 | +names* are genuinely enforced one level up and one level down: |
| 34 | + |
| 35 | +- **app-level** `requiredPermissions` — server-side: an app whose required |
| 36 | + permissions the caller lacks is dropped from `/meta` entirely; |
| 37 | +- **item-level** `requiredPermissions` / `requiresService` — stripped server-side |
| 38 | + from the app's top-level `navigation` tree, and re-checked in the shell; |
| 39 | + item-level `visible` is a real CEL gate in the shell. |
| 40 | + |
| 41 | +Three layers, of which the middle one was theatre — `filterAppForUser` reads the |
| 42 | +app's `requiredPermissions` and then walks **only** `item.navigation`; it never |
| 43 | +touches `item.areas`, and the client renders every area in the switcher. ADR-0078 |
| 44 | +false compliance, the same shape as `capabilities.readOnly` (#4583). |
| 45 | + |
| 46 | +**Removed rather than enforced (ADR-0049), deliberately.** Enforcing area gates |
| 47 | +is not wrong, it is unscoped: it needs semantics settled first — when an area is |
| 48 | +filtered out, do its items disappear everywhere, or still participate in other |
| 49 | +areas? does the server bind `user` for area-level CEL? — and a retirement must |
| 50 | +not invent an authorization mechanism. Removing a gate that never gated is |
| 51 | +strictly safer than shipping a major with it still declared, which would have |
| 52 | +kept authors writing it for all of 17.x. |
| 53 | + |
| 54 | +**One caveat the prescription carries rather than hides:** per-item gating |
| 55 | +*inside* an area is enforced by the shell only, because the server does not walk |
| 56 | +`areas`. Anything that must never reach the browser belongs in the app's |
| 57 | +top-level `navigation` tree, or in its own app. Trading one false belief for a |
| 58 | +weaker one would have repeated the defect this removal exists to end. |
0 commit comments