You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0d5e446
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope, and the security contract says so (#21079)
7
+
8
+
Clause-②: yes (narrowing)
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key, export, published type, stored shape or wire shape is removed or renamed, so objectstack migrate meta has nothing to rewrite. What moves is which callers object admission and the row scope accept: a caller that carries a principal and resolves no permission set is now refused, and the remedy is a grant decision (a permission set the deployment declares and assigns), not a rewrite of anything an author already wrote. -->
11
+
12
+
**BREAKING for callers who resolve no permission set.** Shipped as `minor` under the launch-window convention.
13
+
14
+
**What changed.** ADR-0056 D2 gives an unauthenticated principal the deny baseline, not "no checks", and ADR-0090 D9 gives a guest the `guest` position and nothing else. A non-system caller that carries a principal (a position, a named permission set or a user id) but resolves no permission set was instead admitted to every object no set grants, for reads and writes, and read with the record-sharing predicate as its only row scope. Now an empty set list grants nothing:
15
+
16
+
-**Object admission refuses it.** Every engine operation (find, findOne, count, aggregate, insert, update, delete) is refused with `403 PERMISSION_DENIED`, the same refusal any caller gets for an object its sets do not grant. `ISecurityService.canReadObject` and `canExport`, and the write preview's admission, answer `false` for it.
17
+
-**Its row scope is the deny filter.**`ISecurityService.getReadFilter` answers the filter that matches zero rows for it, as it already did on a resolution failure.
18
+
-**The second principal of a delegated request is held to the same answer.** An agent acting on behalf of a delegator who resolves no permission set was already refused by the engine; `canReadObject`, `canExport` and the write preview now refuse it too.
19
+
20
+
The field answers for this caller (`getReadableFields`, `getQueryableFields`, `getWritableFields`, `getMetadataReadableFields`) are unchanged: they are field-level answers, and the contract now says that object admission is not part of them. The `ISecurityService` docblocks in `@objectstack/spec/contracts` that stated the old zero-set admission (`canReadObject`, `canExport`, the metadata-plane field projection) and the deny cases of `getReadFilter` narrow to match. No method, parameter or return type changes.
21
+
22
+
**Who this reaches.**
23
+
24
+
- An unauthenticated request carried as the guest envelope, on a deployment that grants anonymous callers no permission set.
25
+
- A context that names only permission sets the deployment does not register.
26
+
- A signed-in user on an embedder that switches the baseline off (`fallbackPermissionSet: null`) and grants that user nothing.
27
+
28
+
A context that carries no principal at all (no position, no named set, no user id) is handed through as before; ADR-0096 stages it separately. A caller who resolves at least one permission set is decided by its sets, as before, and so is a system context. The public form submit is unaffected: its declaration-derived grant admits the create and its read-back ahead of object admission. Signed-in users of a stock `objectstack serve` deployment are unaffected: it applies the member baseline to every one of them, so none resolves an empty list.
29
+
30
+
**Migration.** A caller that resolves no permission set is refused object admission and reads nothing. An app-declared anonymous endpoint (`authRequired: false`) can no longer read or write objects until the `guest` anchor's bindings are resolved for anonymous callers (#21158). An embedder that sets `fallbackPermissionSet: null` must grant its signed-in users a set explicitly.
31
+
32
+
**For implementers of `ISecurityService`.** Answer `canReadObject`, `canExport` and the object-admission half of a write `false`, and `getReadFilter` with your deny filter, for a non-system caller that carries a principal and resolves no permission set; admit only the principal-less context.
fix(mcp)!: the MCP stdio transport serves a stored metadata body only as its type's read projection, and refuses to evaluate it
6
+
7
+
Clause-②: no (narrowing)
8
+
9
+
<!-- adr-0087: not-required (no-migration-prescription) the MCP stdio transport's engine-only reader (its query, get and aggregate verbs and its record resource) now serves a stored metadata body through the stored-metadata-body family's one projection, and refuses a group, filter, sort or aggregate member on the stored body column — the posture the generic data door already takes. No authorable key, spelling, export or stored shape moves, and no stored row is read differently by any metadata consumer. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a served body projection or a refused query shape on this door (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10
+
11
+
**BREAKING**: this narrows what one door serves and accepts for the two stored-metadata tables (the stored row and its version history). On the MCP stdio transport, a read now carries the stored body as its type's read projection instead of the stored bytes, and a call that would evaluate the stored body is refused. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12
+
13
+
**What changes.**
14
+
15
+
-**Reads.** The stdio bridge's query and get verbs, and the record resource, serve the stored body through the same projection the generic data door and every metadata read serve: stored credential material is withheld, a body that cannot be judged is omitted, and a credential-free body is served unchanged.
16
+
-**Evaluate shapes.** A group, filter, sort or aggregate member on the stored body column is refused with `400 INVALID_FIELD` before the engine runs — the data door's code and envelope.
17
+
18
+
**What stays answerable.** Every scalar column of the two tables is still served, filtered, sorted, grouped and counted; only the stored body column is affected, and every other object is unchanged. A member's read of these tables is refused as before.
Copy file name to clipboardExpand all lines: content/docs/ui/forms.mdx
+15-7Lines changed: 15 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -103,12 +103,19 @@ export default defineView({
103
103
104
104
## 2. (Optional) Create the `guest_portal` permission set
105
105
106
-
Authorization no longer depends on this set — the declaration-derived
107
-
`publicFormGrant` (see the note above) is what permits the insert. You only need
108
-
a `guest_portal` permission set if you rely on the legacy back-compat path (e.g.
109
-
an older runtime, or object hooks that branch on the `guest_portal` permission).
110
-
When present it is still attached to the anonymous context, so keep it
111
-
INSERT-only on the target object — the guest-safe shape (ADR-0090 D9).
106
+
The submit's authorization no longer depends on this set — the
107
+
declaration-derived `publicFormGrant` (see the note above) is what permits the
108
+
insert. For the submit you only need a `guest_portal` permission set if you rely
109
+
on the legacy back-compat path (e.g. an older runtime, or object hooks that
110
+
branch on the `guest_portal` permission). A form that declares a public picker
111
+
(see *the public picker* below) is different: the picker's search runs under
112
+
this set and nothing else, so without one that grants read on the picker's
113
+
target object the picker answers `403 PERMISSION_DENIED` — the ADR-0056 D2 deny
114
+
baseline refuses a caller that resolves no permission set. When present the set
115
+
is attached to the anonymous context, so keep it INSERT-only on the form's
116
+
target object — the guest-safe shape (ADR-0090 D9) — and remember that any read
117
+
you grant a picker's target object is readable by every anonymous visitor
118
+
through that picker.
112
119
113
120
{/* os:check */}
114
121
```ts
@@ -284,14 +291,15 @@ Errors:
284
291
|---|---|---|
285
292
|`400 INVALID_REQUEST`| missing / blank slug or field |
286
293
|`403 LOOKUP_NOT_PUBLIC`| the field has no `publicPicker` block — the deliberate loud default (#3022); also any server-managed anchor (`owner_id`, `organization_id`, …), which never gets a picker even if one is declared |
294
+
|`403 PERMISSION_DENIED`| the deployment registers no `guest_portal` set that grants read on the picker's target object, so the search context resolves no permission set and is refused at object admission — the ADR-0056 D2 deny baseline. The picker's declaration bounds what is searched and projected; it never grants the read itself |
287
295
|`404 FORM_NOT_FOUND`| slug not registered on any `sharing.allowAnonymous: true` view |
288
296
|`500 LOOKUP_TARGET_MISSING`| the referenced object could not be resolved from either `publicPicker.object` or the field definition — the field names no target object at all (or its object metadata is unreachable). Until #7486 this also fired for a perfectly well-formed field, because the fallback read only the legacy spellings and not the canonical `reference`; declaring `object` was the workaround and is no longer needed. |
289
297
|`500 INTERNAL_ERROR`| the field def **declares** a target this route cannot READ — a stored `reference` holding an object or an array rather than the object name `FieldSchema` declares. ⚠️ Deliberately **not**`LOOKUP_TARGET_MISSING`: "nothing names the target" and "the named target is unreadable" want different fixes from whoever owns the metadata, so they get different answers. The unreadable carrier is named in full in the server log (it is withheld from the response body, as every fault's text is); the picker's search never runs. |
290
298
291
299
### Auth model
292
300
293
301
- None of the three routes calls `enforceAuth`, so they work under the always-on anonymous-deny default (there is no `requireAuth` knob to configure since v17).
294
-
- The execution context the **submit** route hands to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin honors `publicFormGrant` first — a create + read-back grant scoped to exactly the declared object — so authorization holds even without a `guest_portal` profile. `permissions: ['guest_portal']` is retained for back-compat. The **lookup** route's search context is `{ permissions: ['guest_portal'], anonymous: true }` — no `publicFormGrant` (it reads the picker's target object, not the form's), which is why its result set is bounded by the picker declaration instead.
302
+
- The execution context the **submit** route hands to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin honors `publicFormGrant` first — a create + read-back grant scoped to exactly the declared object — so authorization holds even without a `guest_portal` profile. `permissions: ['guest_portal']` is retained for back-compat. The **lookup** route's search context is `{ permissions: ['guest_portal'], anonymous: true }` — no `publicFormGrant` (it reads the picker's target object, not the form's), so the picker declaration bounds what the search matches and projects, and the deployment's `guest_portal` set is what authorizes it: with no such set granting read on the target object, the search context resolves no permission set and is refused at object admission (`403 PERMISSION_DENIED`, the ADR-0056 D2 deny baseline).
295
303
- No CSRF or auth header is needed; embed the form on any domain.
0 commit comments