Skip to content

Commit 692a4e4

Browse files
fix(console): refuse alias-spelled Setup object lists for a caller the Setup app refuses (#29082)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbLjuGcBHnXMKngAM2U7tGSa)_ Fixes #28988 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:普通成员(没有 Setup 应用权限)用别名地址打开对象列表(`/apps/setup/sys_user`、`/apps/setup/showcase_task`、`…/view/ID`、`…/data`,以及会跳转过去的旧地址 `/apps/setup/system/users`)时,现在看到与规范地址 `/apps/com.objectstack.setup/sys_user` 相同的「无权访问此应用」页面。 - 为什么改:以前这些别名地址会套着默认应用(如 Showcase)的顶栏和侧栏显示列表,地址栏却写着 setup,像是进了管理后台;规范地址早已正确拒绝,两种拼写判定不一致。 - 风险与回滚:只改界面判定,服务端读权限不变;管理员不受影响(不发起探测)。记录页 `/apps/setup/OBJECT/record/ID` 按 objectui#12706 的未决产品决定保持可达,审批收件箱的链接不受影响。回滚即还原本 PR。 - 先看:`ui/packages/app-shell/src/console/AppContent.tsx`(`isObjectListAppRoute` 与 `appSurfaceAwaitsVerdict` 新增的 `isSetupObjectListRoute` 一支)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/console/__tests__/AppContent.setupObjectListVerdict-28988.test.tsx`,预期 12 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes - `AppContent` asks the Setup app's access verdict on the object **list** routes under the `setup` alias: `/apps/setup/OBJECT`, `/apps/setup/OBJECT/view/ID` and `/apps/setup/OBJECT/data`. They join the routes that already waited on it (`appSurfaceAwaitsVerdict`: metadata, Setup's root, settings, marketplace). A `denied` verdict draws `app-access-denied`, and nothing of the list mounts. Every other verdict, and an admitted caller (Setup in the list, so nothing is probed), render as before. - A list route is found by route rank (`isObjectListAppRoute`), the way `preloadAppRouteChunk` and `metadataReadsForAppRoute` already match: the sub-path is matched against `APP_ROUTE_METADATA_READS` plus the host's routes, and only `:objectName`, `:objectName/view/:viewId` and `:objectName/data` count. So a static route (`search`, `reports`) or a host route under the Setup segment (`system/approvals`, `system/users`) is never read as an object list. - The arm applies only under the `/apps/setup` pseudo-route (`isSetupRoute`). An ordinary unmatched app already reaches the verdict through `requestedAppMissing`, and its flow is unchanged. ## Why For a member the server refuses the Setup app, `/apps/setup` is a pseudo-route (`isSpecialRoute`), so an unmatched `setup` segment fell back to the launcher's default app instead of asking the verdict. The canonical spelling is an ordinary unmatched app and already drew the refusal. The legacy `/apps/setup/system/users` forwards onto `/apps/setup/sys_user`, so the member reached the list from a documented URL. ## Tests - New `AppContent.setupObjectListVerdict-28988.test.tsx`, a real `ObjectStackAdapter` over a stubbed `fetch`, as in the #27507 and objectui#12706 files: - A refused caller gets `app-access-denied`, no `ConsoleLayout` and no page mounted, on `/apps/setup/sys_user`, `sys_organization`, `showcase_task`, `sys_user/view/all_users` and `sys_user/data`. The one by-name probe is Setup's, and the URL is left as typed. - The legacy `/apps/setup/system/users` forwards onto `/apps/setup/sys_user`, which is refused. - The package-id spelling draws the same screen. - Controls: an admitted caller gets the list under Setup, unprobed; a server without the app (404) keeps the default-app fallback; the record page the approvals inbox links to stays reachable for a refused caller; the host's `system/approvals` route is not read as a list; a list under an app the caller may open renders, unprobed. - Red on `main`: with `AppContent.tsx` at `ab085a04`, 6 of the 12 cases fail (the five list routes and the legacy redirect). The package-id case and the controls pass there. - Ablation through `scripts/ablation-replace.mjs` (anchor 1 to 0, blob restored to HEAD, `git diff HEAD` empty): the route-rank match replaced by `appSubPath !== ''` (every Setup sub-route refused) reds 5 cases across this file and the objectui#12706 file: both record-page controls, both approvals-inbox controls, and the legacy redirect. ## Acceptance notes - **The boundary is the claim's.** Record routes under the Setup segment (`OBJECT/record/ID`, its `/edit` and `/page/NAME`, and the `OBJECT/ID` shorthand that redirects to one) stay outside the gate, because the approvals inbox links members there and objectui#12706 left that open as a product decision. `OBJECT/new` (the create form) is not a list route and is also left as it was; it still renders under the default app for a refused caller. Noted, not filed. - **The card's second symptom is not the same verdict.** `/apps/com.example.showcase/system/settings` shows a member the empty settings hub because the member may open Showcase: the app verdict is `granted` there, and #27507 pins that route as rendered, unprobed (`the settings route under an app the caller may open renders the page, unprobed`). It is reported to the seat as an out-of-scope finding, not fixed here. ## Verification <details><summary>Local runs</summary> All at head `0259ffcd` (base `ab085a04`; the 9 commits `main` gained since touch nothing under `ui/packages/app-shell/src/console` or `ui/apps/console/src`). - Prerequisite build under the verify lock: `turbo run build --filter='@object-ui/app-shell^...'` plus formula, sdui-parser, lint and client, 64 of 64 tasks. - The new file with the #27507 and objectui#12706 verdict files: Test Files 3 passed, Tests 34 passed. - Red on `main` (`AppContent.tsx` at `ab085a04`, new test unchanged): Tests 6 failed, 6 passed. - Every ui test file that imports `AppContent` (`git grep -l`, 53 in `packages/app-shell`, 16 in `apps/console`, 1 in `packages/i18n`): Test Files 70 passed, Tests 663 passed. A narrowing of app-shell's suite; CI's Test UI shards run the rest. - `turbo run type-check --filter=@object-ui/app-shell` (its script runs `tsc -p tsconfig.test.json` too, so the new test is compiled): 65 of 65 tasks. - `node scripts/pm/dispatch-gates.mjs --commands`: 34 derived commands, each exit 0, among them `check-sdui-manifest.mjs`, `check-changeset-no-major.mjs --base origin/main`, `check:type-check-debt`, `check:published-files`, `check:nul-bytes` and `pnpm --dir ui exec vitest run scripts/` (Test Files 169 passed, 2 skipped; Tests 4900 passed, 2 skipped). `--ran` reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. The PR-body gates were run again against this body before the pull request was opened. - Not measured: a browser run of the built Console as a real member. The card's reproduction is the server's own (`GET /api/v1/meta/app/setup` answers 403 to a member), and the test drives the real adapter's probe over that envelope. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: not applicable. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 section is the Chinese summary the repo asks for). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jTJvw3sfvZA89BdmSR --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTJvw3sfvZA89BdmSR)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c749198 commit 692a4e4

3 files changed

Lines changed: 390 additions & 1 deletion

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(console): a caller the Setup app refuses gets "You don't have access to this app" on an object list spelled with the `setup` alias, as on the package-id spelling, instead of the list inside another app's shell
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`/apps/setup/OBJECT`, `/apps/setup/OBJECT/view/ID` and `/apps/setup/OBJECT/data` (and the legacy `/apps/setup/system/users`, which forwards onto the first) now wait on the Setup app's access verdict, the way Setup's root, metadata, settings and marketplace routes already did. A member the server refuses the Setup app sees the access-denied screen there, as on `/apps/com.objectstack.setup/OBJECT`; before, they saw the object list under the default app's header and sidebar while the address bar read `setup`. The server's data reads are unchanged, and so is everything an admitted caller sees. Record pages under the Setup segment (`/apps/setup/OBJECT/record/ID`, the address the approvals inbox links to) stay reachable as before.

‎ui/packages/app-shell/src/console/AppContent.tsx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,27 @@ function preloadAppRouteChunk(subPath: string, hostPaths: readonly string[] = []
201201
if (chunk) void chunk.preload().catch(() => {});
202202
}
203203

204+
/**
205+
* objectstack#28988 — the route paths that draw an object's LIST: the default
206+
* list, a saved list view and the bare data surface. Record-scoped paths
207+
* (`:objectName/record/...`, the `:objectName/:maybeRecordId` shorthand) and
208+
* the create form are deliberately absent; see `appSurfaceAwaitsVerdict`.
209+
*/
210+
const OBJECT_LIST_ROUTE_PATHS: ReadonlySet<string> = new Set([':objectName', ':objectName/view/:viewId', ':objectName/data']);
211+
212+
/**
213+
* Whether the route at `subPath` (the path below `/apps/:appName`) draws an
214+
* object's list. Matched the way `<Routes>` ranks paths, beside every path
215+
* `AppContent` and the host declare, so a static route or a host route that
216+
* shadows `:objectName` (`search`, `reports`, `system/...`) is not one.
217+
*/
218+
function isObjectListAppRoute(subPath: string, hostPaths: readonly string[] = []): boolean {
219+
const routes = [...APP_ROUTE_METADATA_READS.map(({ path }) => ({ path })), ...hostPaths.map((path) => ({ path }))];
220+
const matches = matchRoutes(routes, `/${subPath.replace(/^\/+/, '')}`);
221+
const path = matches?.[matches.length - 1].route.path;
222+
return path !== undefined && OBJECT_LIST_ROUTE_PATHS.has(path);
223+
}
224+
204225
interface AppContentProps {
205226
/**
206227
* Extra <Route> elements appended to the inner /apps/:appName/* router.
@@ -550,12 +571,24 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
550571
// settings registered"), the raw capability error on a settings page, the
551572
// marketplace's admin-only notice. Each now reads as the app root does; the
552573
// pages' inner states stay for a caller the app lets in.
574+
//
575+
// objectstack#28988 — and the object LIST routes under the `setup` alias
576+
// (`/apps/setup/OBJECT`, `…/view/ID`, `…/data`; the legacy
577+
// `/apps/setup/system/users` forwards onto one). The `/apps/setup` subtree is
578+
// a pseudo-route, so a member got the default app's chrome around the list
579+
// while the canonical `/apps/com.objectstack.setup/OBJECT`, an ordinary
580+
// unmatched app, already drew the refusal. The record routes the approvals
581+
// inbox links to stay outside this gate, as above; the list paths are
582+
// matched by route rank (`isObjectListAppRoute`), so they never reach one.
553583
const isSetupRootRoute = location.pathname === '/apps/setup' || location.pathname === '/apps/setup/';
554584
const [appSubRoot, appSubPage] = appSubPath.split('/');
555585
const isSettingsOrMarketplaceRoute =
556586
appSubRoot === 'system' && (appSubPage === 'settings' || appSubPage === 'marketplace');
587+
const isSetupObjectListRoute = isSetupRoute && isObjectListAppRoute(appSubPath, hostRoutePaths);
557588
const appSurfaceAwaitsVerdict =
558-
!!appName && !matchedApp && (isMetadataRoute || isSetupRootRoute || isSettingsOrMarketplaceRoute);
589+
!!appName &&
590+
!matchedApp &&
591+
(isMetadataRoute || isSetupRootRoute || isSettingsOrMarketplaceRoute || isSetupObjectListRoute);
559592

560593
// Post-publish readiness: when a requested app isn't in the loaded metadata,
561594
// re-check ONCE (the registry can lag a beat behind a publish) before

0 commit comments

Comments
 (0)