Skip to content

fix(console): refuse an internal form's write up front when the caller holds no grant - #29407

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-niy7mm
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-niy7mm

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29187

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:成员打开一个自己没有新建权限的表单链接时,页面一加载就直接显示「您没有权限保存这条记录。」,不再画出可填写的表单;编辑表单(带记录 ID)同理按编辑权限判断。两个登录后的表单地址行为一致,匿名公开表单不变。
  • 为什么改:以前只读成员能把整张表单填完,点提交才被拒,填的内容白费。
  • 风险与回滚:风险低,只影响没有对应权限的人;有权限的人看到的表单完全不变。回滚即还原这一个 PR。
  • 先看:ui/apps/console/src/components/FormPage.tsx 新增的拒绝分支;ui/apps/console/src/App.tsx 中 /forms/:name 移入权限布局的那一处。
  • 两分钟验证:用只有读权限的成员打开 /_console/forms/showcase_project.form,应只看到一行红色的无权限提示,没有任何输入框;管理员打开同一链接仍是完整表单。
  • 需要你做的:无,席位落地。

What changes

  • One mechanism, named: /forms/NAME moves under ConsolePermissionsScope. The app-less internal form route was a sibling of the console's permissions layout, under ProtectedRoute alone, while its in-app twin /apps/APP/forms/NAME sat under it. It is now a child of the same pathless layout route as /home and /apps/APP/*, so both form URLs read the one /auth/me/permissions answer the console already holds. The URL is unchanged, deep links keep working, and a hop from Home or an app keeps the layout, so the answer is not asked again. /f/SLUG stays outside it (anonymous, no answer, no request).
  • FormPage refuses up front. Once the form has loaded (it needs the form view to know its object), internal mode asks the affordance map's form row through resolveAffordance from @object-ui/core, the predicate the app's own create and edit forms read: createFormFields (the create grant) for a create form, editFormFields (the update grant) for an edit form (?recordId=), each intersected with the object's effective API operations. Refused, the page draws the submit-time refusal's own sentence (form.noPermissionToSave, the one console FormPage: a refused create shows the raw server JSON envelope to the end user ("Create failed (403): {"error":…}) #29019's PR fix(console): a refused form submit says a localized sentence, never the server's response body #29184 uses) in a role="alert" banner and no field, no Submit. With the grant, nothing changes. No new i18n key, no metadata key, no server change.
  • With no permissions provider above it (the public route, a bare harness), usePermissions() reads every grant open, so /f/SLUG and every existing FormPage test behave as before.

Tests

  • FormPage.upfrontWriteRefusal-29187.test.tsx (8 cases, real MePermissionsProvider fed the served payload shape): create without the create grant is refused with no textbox and no POST, on /forms/NAME and /apps/APP/forms/NAME; create with it renders the form; the update grant alone does not open a create form; an edit form without the update grant is refused even with create; with it the form renders prefilled; /f/SLUG draws its fields both under a no-create answer and with no answer at all.
  • FormPage.routePermissions-29187.test.tsx (4 cases, real App route tree and ConsolePermissionsScope): /forms/NAME renders under the answer, asked once; the loading screen shows before it; /home then a form link asks /auth/me/permissions once in total; /f/SLUG has no permission context and makes no request.
  • Ablation: replacing mode === 'internal' && in the new guard with a never-true test (node scripts/ablation-replace.mjs, restored to git diff HEAD empty) reds exactly the four refusal cases; the four grant and public cases stay green.

Acceptance notes

  • Requests on a cold /forms/NAME deep link. The permission check costs one request, /auth/me/permissions. The same layout also holds LauncherAppsProvider, so a cold deep link now asks /me/apps too, as /home does: the form page's header launcher now narrows the apps it offers the way Home's does, where before it listed every app. A hop from Home or an app asks neither again.
  • The policy half reads the default bucket. FormPage's object payload keeps only name, label, fields and fieldGroups, so resolveAffordance is given no objectSchema and an object-level userActions opt-out or a managed bucket does not refuse here; the grant and the effective API operations do. The card asked for the grant; widening the payload is a separate change.
  • The refusal state carries no button of its own; both routes keep the console header for navigation.

Verification

Live on a fresh showcase (pnpm dev -- --fresh, source Console on Vite), a member created through auth/admin/create-user, holding only the everyone baseline: showcase_project read only, showcase_task read + create, no edit. Before = main at 5780fa49, after = this branch.

Case Before After
member, /forms/showcase_project.form full form, 8 inputs, Submit refusal sentence, 0 inputs, no Submit
member, /apps/showcase_app/forms/showcase_project.form full form, Submit refusal sentence, 0 inputs, no Submit
member, /forms/showcase_task.form (create allowed) form form, unchanged
member, /forms/showcase_task.form?recordId=ID (no edit) edit form, Submit refusal sentence, 0 inputs
admin, same edit link edit form edit form, unchanged
anonymous, /f/contact-us form, Submit form, unchanged, 0 permission requests

Screenshots for every row (before-*.png, after-*.png) are in the project folder issue-29187/.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 33 commands for this diff; each ran locally and exited 0, reconciled with --ran (the list is in the os-dev report on the card). Also: pnpm --filter @object-ui/console run type-check exit 0; eslint on the four touched files, 0 errors; the FormPage, InternalFormRoute and App.* console suites plus affordanceGrantMap-12082.test.tsx (the census that refuses a CRUD grant read outside the map), 51 files / 474 tests green. All of the above reran green on the merged head c9f31cf0 (main with #29322 and #29302): 33/33 derived gates exit 0 (--ran: 33 run, 0 NOT-MEASURED), the console suites 53 files / 484 tests, vitest run scripts/ 169 files. The last merge of main (e8cea601) touched only ui/apps/console/src/main.tsx, vite.config.ts and tsconfig.node.json in this area; on it the console type-check (after rebuilding the @object-ui/mobile dist that merge moved) and the two new files plus App.homePermissions-12700 (17 tests) are green.


Generated by Claude Code

…r holds no grant

The app-less /forms/NAME route moves under the console's permissions
layout beside /home and /apps/APP/*, and FormPage asks the affordance
map's createFormFields / editFormFields row before drawing a field.
Refused, it draws the submit-time refusal sentence and no field.
/f/SLUG is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSZFcoEaXNQkQx4KJp
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB

席位核对 — ACCEPT · domain:ui#5 · head e8cea601 · 2026-11-02T14:28Z

  • 形态: draft PR on main. The body has Fixes #29187 and no other card number next to a closing keyword. 5 files, +485/−13, Tier S: check-governed-merges --pr 29407 finds no governed path and 498 lines, under the 3000 limit. The body carries 维护者速读 and both declaration lines (Clause-②: no, Metadata-protocol: none), copied from claim 6576914927.
  • 前提实测: at base, /forms/:name sat under ProtectedRoute alone and FormPage drew a fillable create form whatever the caller's grant. The server refused only at Submit (seat read, claim; the filing body). The dev's ablation reds exactly the 4 refusal cases. On the live showcase, a read-only member is refused up front on both URLs and on edit, while a create-allowed form, an admin edit and /f/contact-us are unchanged (shots in /mnt/project-files/issue-29187/).
  • diff read: the mechanism is to move /forms/:name under ConsolePermissionsScope (App.tsx, placement only). The URL is unchanged, and /f/:slug stays outside the layout. In internal mode only, FormPage asks resolveAffordance('createFormFields' | 'editFormFields') against usePermissions(), the same row the app's own create and edit forms read. Refused, it draws form.noPermissionToSave (the submit-time sentence) with no field. One /me/permissions answer serves both internal URLs, so the same form behaves the same by URL. No new key.
  • 门禁 check-runs (pinned to head e8cea601fb48cd32ac84b31c26313c0751af9fe3): 44 success, 11 skipped, 0 failed, 0 pending. mergeable_state clean. Dev local gates: 33 derived, all exit 0; --ran 0 NOT-MEASURED. New tests 12/12; console suites and the affordance census 53 files, 484 tests.
  • 接受的偏差: (1) a cold /forms/:name deep link now also asks /me/apps, because the launcher provider shares the scope. That matches what the in-app twin URL already asks, so it is parity rather than a new cost. (2) The policy half of the affordance reads the default bucket, because FormPage's object payload carries no managedBy/userActions. Grants alone decide here, and the server still enforces. Both are declared in the body.
  • 范围外去向: none reported.
  • 落地条款: no contract review owed (Clause-②: no). Seat 4's fix(console): a refused edit-form record read says a localized line, never the response body #29322 on the same file landed first (4e0ff3e6), and this head merged main after it. Land by pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 14:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 14:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 96fb7b5 Nov 2, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-niy7mm branch November 2, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console form link: a user without create right is offered a fillable create form on /forms/NAME, and learns of the refusal only on Submit

1 participant