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 91ca7f3
Browse filesBrowse the repository at this point in the historyBrowse files
docs(forms,app-crm): forms.mdx says what the guest_portal name does; crm_lead.status says which default the write uses
forms.mdx said the guest_portal set and the anonymous flag are attached
"for back-compat" with object hooks, that hooks may branch on the
guest_portal permission, and that a registered set must be kept
INSERT-only. No hook sees either key, and a registered set's object bits
are not read on the submit path; only its field permissions feed the
result masker over the record the route gets back, and the visitor gets
the id only. Section 2 is retitled and rewritten to say that, and drops
the INSERT-only snippet whose object bits nothing reads.
The crm_lead.status comment called a select option's `default` "only a
UI preselect"; the engine falls back to it on write when no defaultValue
is declared. The comment now says the declared defaultValue is what the
write uses and why it takes precedence.
Text only; no behaviour changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
Copy file name to clipboardExpand all lines: content/docs/ui/forms.mdx
+26-30Lines changed: 26 additions & 30 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,9 +47,10 @@ Only the spec'd whitelist of form fields is accepted; everything else (status, o
47
47
> exactly that object, never anything else and never the anonymous fall-open. So
48
48
> public forms work under the unconditional anonymous-deny posture (v17
49
49
> retired the `requireAuth: false` opt-out, #3963) with **no**
50
-
> `guest_portal` permission set. The `guest_portal` permission set + `anonymous` flag
51
-
> are still attached for **back-compat** (object hooks that detect a guest via a
52
-
> falsy `ctx.user?.id`), but they are no longer the authorization mechanism.
50
+
> `guest_portal` permission set. The submit's context also carries
51
+
> `permissions: ['guest_portal']` and `anonymous: true`. Neither authorizes
52
+
> anything and neither reaches an object hook; section 2 says the one thing the
53
+
> `guest_portal` name does.
53
54
54
55
## 1. Declare the form view
55
56
@@ -102,32 +103,27 @@ export default defineView({
102
103
> - Multiple form views per object are fine — only the one(s) with `sharing.enabled === true` and `sharing.allowAnonymous === true` are exposed.
103
104
> - On a **walled** tenancy posture (`group` or `isolated` in force), a form whose object is walled by an organization column is **not offered**. An anonymous submission carries no organization, and an insert without one into a walled object is refused, so both anonymous endpoints answer the form exactly as they answer a withdrawn one (`404 FORM_NOT_FOUND`). The administrator is told why, at the form's `sharing`: the view's read (`GET /api/v1/meta/view/:name`) carries it in `_diagnostics.warnings`, and a save or publish of the view answers success with a `public-form-intake-unavailable` warning in `advisories`. If the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it and the form is offered again.
104
105
105
-
## 2. (Optional) Create the `guest_portal` permission set
106
-
107
-
The submit's authorization no longer depends on this set — the
108
-
declaration-derived `publicFormGrant` (see the note above) is what permits the
109
-
insert. For the submit you only need a `guest_portal` permission set if you rely
110
-
on the legacy back-compat path (e.g. an older runtime, or object hooks that
111
-
branch on the `guest_portal` permission). When present the set is attached to
112
-
the anonymous context, so keep it INSERT-only on the form's target object — the
// Guest-safe capability: INSERT-only on the intake objects. (`isProfile` no
121
-
// longer exists — the Profile concept was removed by ADR-0090 D2.)
122
-
exportdefaultdefinePermissionSet({
123
-
name: 'guest_portal',
124
-
label: 'Public Form Submitters',
125
-
objects: {
126
-
lead: { allowCreate: true }, // no read/edit/delete
127
-
case: { allowCreate: true },
128
-
},
129
-
});
130
-
```
106
+
## 2. No permission set to create — what the `guest_portal` name does
107
+
108
+
The submit is authorized by the declaration-derived `publicFormGrant` (see the
109
+
note above). The grant is decided before any permission set resolves, so the
110
+
form works the same whether or not a `guest_portal` permission set exists. The
111
+
submit's execution context still names `guest_portal` in `permissions`, and that
112
+
name reaches one thing only:
113
+
114
+
-**The result masker, where a deployment registers a set of that name.** Only
115
+
the set's field permissions (`fields`, such as `readable: false`) and the
116
+
capabilities it holds against a field's `requiredPermissions` are read, to mask
117
+
the created record the server hands back to the route. The set's object
118
+
permissions (`allowCreate`, `allowRead`, …) are not read on this path, so they
119
+
neither grant nor limit the insert.
120
+
-**The visitor never sees that record.** The submit answers with the new
121
+
record's `id` only (section 4). Register a `guest_portal` set only if you want
122
+
fields masked on the record the route gets back; nothing else needs one.
123
+
-**No object hook can read it.** A hook's `ctx.session` and `ctx.user` carry
124
+
neither `permissions` nor `anonymous`, and on a public submission both are
125
+
unset because the context carries no user. Tell a public submission apart by
126
+
the missing `ctx.user` (section 3), not by a permission.
131
127
132
128
## 3. Stamp server-controlled defaults in a hook
133
129
@@ -247,7 +243,7 @@ lookup route.
247
243
### Auth model
248
244
249
245
- Neither route calls `enforceAuth`, so both work under the always-on anonymous-deny default (there is no `requireAuth` knob to configure since v17).
250
-
- 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.
246
+
- The execution context the **submit** route hands to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin admits on `publicFormGrant`alone — a create + read-back grant scoped to exactly the declared object, decided before any permission set resolves — so no `guest_portal`set is needed. Neither `permissions` nor `anonymous` authorizes anything or reaches an object hook; where a set named `guest_portal` is registered, the result masker reads its field permissions (section 2).
251
247
- No CSRF or auth header is needed; embed the form on any domain.
0 commit comments