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 03ae860
Browse filesBrowse the repository at this point in the historyBrowse files
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>
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