Skip to content

console: a member refused the Setup app still gets /apps/setup/OBJECT routes rendered inside another app's shell — the alias spelling skips the app verdict #27507 added #28988

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWJDYbbtH6dhJNoboN

立卡门: ① 产品缺陷(用户可见),具名落点 + 复现(reach: 界面,普通成员打开 /_console/apps/setup/sys_user,实测两次)
业务理由: 普通成员点到旧链接 /apps/setup/system/users(清单项自己列的旧地址,书签和文档里也有)会被转到 /apps/setup/sys_user,结果看到的是 Showcase 的顶栏和侧栏套着系统用户列表,地址栏却是 setup,像是自己进了管理后台;而规范写法 /apps/com.objectstack.setup/sys_user 正确显示「无权访问此应用」。#27507 修过 /apps/setup/system/* 这一类,别名拼写的对象路由漏了。服务端读权限本身按设计放行(sys_user_org_members 策略),这是界面判定不一致,不是越权。
动手的读者: domain:ui 席,ui/packages/app-shell/src/console/AppContent.tsx 的应用判定:别名(setup)拼写的对象路由也要走与规范 id 相同的「拒绝进入该应用」判定,遵守其「NEVER render a different app」注释。
查重: apps/setup alias object route member 48 命中、No settings registered member 258(均含已关);最近邻 #27507(同类的 system/* 路由,已关 completed),没有覆盖对象路由别名的开卡。

QA-source: #28981 · platform-core.home-admin-cluster-links · observed beside the clauses

What happens

For a plain member (no Setup app access):

Same class under an app the member can open: /_console/apps/com.example.showcase/system/settings shows the settings hub with "No settings registered. Plugins can register settings manifests via the SettingsService." (the server returns 0 manifests to a member), #27507's first symptom on a route its fix did not cover.

Server side is by design: GET /api/v1/data/sys_user → 200 under the sys_user_org_members policy; GET /api/v1/meta/app/setup → 403. Presentation only.

Measured at framework 7fa9873f (main, 2026-11-02), stock showcase boot from a scratch copy, Console built from the same tree.

Reproduction

  1. Boot the showcase; mint a plain member (invite then sign-up, or admin create-user then change-password); form sign-in as that member.
  2. Open /_console/apps/setup/sys_user.
  3. Expected: the "You don't have access to this app" page, as for /_console/apps/com.objectstack.setup/sys_user.
  4. Actual: the Showcase shell with the sys_user list (4 users). Reproduced twice on fresh browser contexts.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:ui · area:access · pm:queue · 2026-11-02T06:02Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Claim: PM loop round 17 · 2026-11-02T06:21Z
    Session: session_01QsR2jTJvw3sfvZA89BdmSR (card thread, os-dev); dispatched by seat session session_01QsR2jUJ19MstFvFhjgiYTu
    Account: zhuangjianguo
    Branch: claude/project-thread-jsplg6 (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-28988
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: ui/packages/app-shell/src/console/AppContent.tsx (the Setup app verdict on alias-spelled object routes), its tests (ui/apps/console/src/__tests__/AppContent.* included), and a root .changeset (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: AppContent asks the Setup app verdict for the metadata, root, settings and marketplace routes (appSurfaceAwaitsVerdict) but not for alias-spelled object routes
    Thread-read: 6570295530
    Premise: holds (at origin/main ab085a04: AppContent.tsx:556-557 appSurfaceAwaitsVerdict covers isMetadataRoute, isSetupRootRoute and isSettingsOrMarketplaceRoute only. Boundary: AppContent.tsx:537-543 leaves /apps/setup/OBJECT/record/ID open as a product decision (objectui#12706), and members reach it from ApprovalsInboxPage.tsx:613,727; record routes stay as they are unless that decision is made)
    Serial constraints cleared: none known.

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jTJvw3sfvZA89BdmSR
    os-dev-report

    {
      "issue": 28988,
      "status": "done",
      "branch": "claude/project-thread-jsplg6",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29082",
      "head": "0259ffcd",
      "session": "session_01QsR2jTJvw3sfvZA89BdmSR",
      "premise_still_valid": true,
      "summary": "Premise held at main ab085a04: the /apps/setup subtree is a pseudo-route (isSetupRoute in isSpecialRoute), so an alias-spelled object list fell back to the default app instead of asking the verdict, while /apps/com.objectstack.setup/OBJECT (an ordinary unmatched app) drew app-access-denied. AppContent.tsx gains isObjectListAppRoute (matchRoutes over APP_ROUTE_METADATA_READS plus the host's route paths, the same ranking preloadAppRouteChunk uses) and an isSetupObjectListRoute arm in appSurfaceAwaitsVerdict: under the setup alias, :objectName, :objectName/view/:viewId and :objectName/data wait on the Setup verdict. Record routes, the OBJECT/ID shorthand, OBJECT/new and host routes (system/approvals, system/users) stay as they were, per the claim's boundary; the legacy system/users forwards onto the list and is refused there. Admins and a server without the app are unchanged.",
      "files_changed": [
        "ui/packages/app-shell/src/console/AppContent.tsx",
        "ui/packages/app-shell/src/console/__tests__/AppContent.setupObjectListVerdict-28988.test.tsx",
        ".changeset/28988-setup-alias-object-list-verdict.md"
      ],
      "line_budget": "391 changed lines (+390 / -1, 3 files) vs merge base ab085a04, under the 3000 threshold; inside the claim's file surface",
      "tests": "From ui/, under os-verify-lock, --maxWorkers=2, at 0259ffcd. New AppContent.setupObjectListVerdict-28988.test.tsx (real ObjectStackAdapter over stubbed fetch) with the 27507 and 12706 verdict files: Test Files 3 passed, Tests 34 passed, VERDICT command-exit 0. Red on main: AppContent.tsx at ab085a04 with the new test: Tests 6 failed / 6 passed (the five list routes and the legacy system/users redirect; package-id case and controls pass), restored via git checkout HEAD, git diff HEAD empty. Ablation via scripts/ablation-replace.mjs: 'isSetupRoute && isObjectListAppRoute(appSubPath, hostRoutePaths);' -> 'isSetupRoute && appSubPath !== '';' (anchor 1->0, blob 9430666f -> 04b61c84) reds 5 across the new file and the 12706 file (both record-page controls, both approvals-inbox controls, legacy redirect); restored blob == HEAD 9430666f, git diff HEAD empty. No dist step: the tests import AppContent from src. Narrowed sweep: every ui test file importing AppContent (git grep -l: 53 app-shell, 16 apps/console, 1 i18n): Test Files 70 passed, Tests 663 passed, VERDICT 0. turbo run type-check --filter=@object-ui/app-shell (script includes tsc -p tsconfig.test.json): 65/65 tasks, VERDICT 0.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands derived 34 at 0259ffcd; all 34 ran, each exit 0 captured before any pipe, incl. check-sdui-manifest.mjs, check-changeset-no-major --base origin/main, check:type-check-debt, check:query-options-erasure, check:published-files, check:nul-bytes, pnpm --dir ui exec vitest run scripts/ (169 passed / 2 skipped files, 4900 tests). --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. PR-body gates on the exact body before pr_create: check-changeset-no-major (GITHUB_EVENT_PATH) exit 0, check-adr-0087-registration exit 0, check-closing-keyword-parity exit 0, check-partof-closing-keyword exit 0. main gained 9 commits since the base, none under ui/packages/app-shell/src/console or ui/apps/console/src; no merge. CI check-runs on #29082: in_progress at report time (Check Changeset, Type Check, Lint legs, Governed Surface Queue Guard); re-runs and landing are the seat's.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x2 (card body, comments), mcp__github__pull_request_read get_check_runs x1; no write tool",
      "api_writes": "2 — (1) fleet-write relay dispatch POST /repos/objectstack-ai/objectstack/dispatches carrying one pr_create (no labels, no assignees; read-back 8059 bytes sent = stored, #29082); (2) this os-dev-report comment through post-stamped. git push x1 is not a REST write.",
      "deviations": [
        "The PR body opens with the harness-required attribution comment line and the Requested-by line, then Fixes #28988; kept as the harness requires.",
        "The shared checkout held the platform-assigned branch, so it was detached (git checkout --detach, no file change) for the worktree objectstack-issue-28988.",
        "Not subscribed to PR activity and no assignee/reviewer set on the PR: os-dev.md puts landing and re-runs with the seat and forbids PR assignees."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: the card's own measurement (界面, a plain member opens /_console/apps/com.example.showcase/system/settings and gets 'No settings registered. Plugins can register settings manifests via the SettingsService.'; the server returns 0 manifests to a member) · evidence: not the same verdict: the member may open Showcase, so the app verdict is granted and #27507 pins that route as rendered unprobed ('the settings route under an app the caller may open renders the page, unprobed' in AppContent.settingsMarketplaceVerdict-27507.test.tsx); the fix site is the host settings hub's empty state (apps/console), not AppContent · dedupe words: settings hub member empty, No settings registered member, system/settings non-admin",
        "carrier: seat · noted, not filed — /apps/setup/OBJECT/new (the create form) still renders under the default app for a refused caller; it is not a list route, so the claim's boundary left it. Same wrong-shell class; joins the objectui#12706 product decision on the rest of the Setup subtree."
      ]
    }
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Landed · 2026-11-02T08:57Z · PR #29082 merged through the merge queue as 692a4e4a at 2026-11-02T08:37Z (GitHub's clock). ACCEPT 6571491824 at head 0259ffcd.

    probe before (692a4e4a~1) now (origin/main)
    isSetupObjectListRoute in ui/packages/app-shell/src/console/AppContent.tsx 0 2
    AppContent.setupObjectListVerdict-28988.test.tsx exists no yes

    Fixes #28988 closed this card. pm:dispatched comes off in the same stroke as this note. /apps/setup/OBJECT/new under the alias is #29102 (seat 2), and the member settings-hub copy is #29085 (seat 2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions