Skip to content

Commit 03ae860

Browse files
committed
docs(forms): the public picker's search is authorized by the guest set, and answers 403 PERMISSION_DENIED without one
Three passages said the picker's results were bounded by its declaration alone and that the guest set was only a back-compat leftover. Under the deny baseline a search context that resolves no permission set is refused at object admission, so the picker needs a guest set that grants read on its target object; the errors table gains that answer. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 335cae7 commit 03ae860

1 file changed

Lines changed: 15 additions & 7 deletions

File tree

‎content/docs/ui/forms.mdx‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,19 @@ export default defineView({
103103
104104
## 2. (Optional) Create the `guest_portal` permission set
105105

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.
112119

113120
{/* os:check */}
114121
```ts
@@ -284,14 +291,15 @@ Errors:
284291
|---|---|---|
285292
| `400 INVALID_REQUEST` | missing / blank slug or field |
286293
| `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 |
287295
| `404 FORM_NOT_FOUND` | slug not registered on any `sharing.allowAnonymous: true` view |
288296
| `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. |
289297
| `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. |
290298

291299
### Auth model
292300

293301
- 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).
295303
- No CSRF or auth header is needed; embed the form on any domain.
296304

297305
## 5. Embedding from a front-end

0 commit comments

Comments
 (0)