Repository navigation
Commit 6f017e2
feat(plugin-sharing): list the objects a sharing rule can take effect on (#31207)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiFMQNAUTGWk1GeD14KvBfaa)_
Refs #30209 (row 5 B, server half; rows 1 and 2 measured below). The
card stays open for row 1 and the console picker.
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:plugin-sharing 新增只读接口 `GET
/api/v1/sharing/shareable-objects`,返回「共享规则能生效的对象」清单(业务对象在前、系统对象在后,各组按标签排序);判定直接调用保存时拒绝用的同一个判断(#30621
的 `sharingRuleObjectRefusal`),不另写一份;会话、账号、API 密钥、密钥等凭据类对象都不在清单里。
- 为什么改:新建共享规则时对象选择器列出 86 个对象,大多是
`sys_*`,业务对象被埋住,还能选到存凭据和会话的对象(保存才报错)。按裁决 6670358877 的第 5 行
B,服务端先给出清单,控制台下一张卡据此只列这些对象。
- 风险与回滚:纯新增、只读,不改存储、不改任何已有接口和元数据;只有共享规则管理员(`manage_sharing`,或旧的
`manage_platform_settings`,且有当前组织)能调用,其他人 403、匿名 401、带查询参数 400。回滚即撤销本
PR。
-
先看:`packages/plugins/plugin-sharing/src/shareable-objects.ts`(清单与排序,判定由调用方传入)、`src/sharing-rule-service.ts`
的
`listShareableObjects`(准入与判定接线)、`src/shareable-objects-routes.ts`(拒绝顺序:身份→受限登录→匿名→查询参数→管理员)。
- 两分钟验证:`pnpm --filter @objectstack/plugin-sharing exec vitest run
src/shareable-objects.test.ts`,预期 16 个用例全过,其中「每个不在清单里的对象,defineRule 都以
SHARING_NOT_ENABLED 拒绝」逐个对象核对。
- 需要你做的:无,席位落地。
## What changes
- **A read-only list, mounted by plugin-sharing itself.**
`registerShareableObjectsRoutes` (`src/shareable-objects-routes.ts`)
mounts `GET /api/v1/sharing/shareable-objects` on `http.server` (alias
`http-server`) at `kernel:ready`, the way the share-link routes are
mounted. The rule administration routes (`/api/v1/sharing/rules*`)
belong to `@objectstack/rest` over the `sharingRules` spec contract; a
list there would need a new contract member, so this door reads the
plugin's own `SharingRuleService` instead and touches neither
`packages/rest` nor `packages/spec`. No HTTP server: one `warn` naming
the route.
- **What is listed** (`src/shareable-objects.ts`): every object in the
engine's registry (`getConfigs()`) for which
`SharingService.sharingRuleObjectRefusal(object)` answers `null`, the
check both save doors (#30621) already ask, handed in as a function and
never restated. Each entry is `{ name, label, system }`; `system` is
`isSystem` or a `sys_` name. Business objects first, then platform
objects, each group by label, then name. Response: `{ success: true,
data: { objects } }`.
- **Who may ask** (`SharingRuleService.listShareableObjects(context)`,
new): the sharing-rule surface's own admission, `assertCanManageRules`
(ADR-0111 D6: `manage_sharing` or the legacy `manage_platform_settings`,
plus an active organization unless platform authority). The route's
refusal order, each before the list is read: caller resolution through
`resolveRequestExecutionContext` (session or API key; a permission-store
outage and an API key lifecycle refusal relayed as declared), held
sign-in `403` with the policy code, anonymous `401 UNAUTHENTICATED`, any
query parameter `400 VALIDATION_ERROR` naming it (closed set: empty),
then a non-administrator `403 PERMISSION_DENIED` with the surface's
sentence.
- **Never an empty list in place of an answer**: no rule service running
(`enforce: false`) answers `501 NOT_IMPLEMENTED`, as the REST rule
routes do; an engine that cannot enumerate its objects answers `500
INTERNAL_ERROR` with the error in the fault log.
- **Ledger and guards**: `src/sharing-route-ledger.ts` (one
`server-only` row) with its package-local conformance test; the
live-mount parity dogfood test imports it, and
`scripts/cross-package-test-inputs.mjs` / `turbo.json` declare it as
that test's input; `scripts/check-route-envelope.mjs` declares the new
door in `MODULES` at `{ responses: 0, ok: 0, err: 0 }` (every body goes
through `sendOk` / `sendError` / `sendServerFault`). These four one-line
wirings are the ones #31004's amendment named.
- **One spelling of "platform object"**: `isPlatformObjectSchema`
(`isSystem` or a `sys_` name) is extracted in `src/sharing-service.ts`
from `effectiveSharingModel`'s own test, and the list reuses it for its
`system` flag (no behaviour change; the system-context census keys that
read to this file).
- **Changeset**: `.changeset/30209-sharing-shareable-objects.md`,
`@objectstack/plugin-sharing: patch`, both declaration lines at line
start.
## Row 5's security note: credential and session objects
Measured, then pinned. The platform's credential and session objects as
their packages declare them (`sys_session`, `sys_account`,
`sys_verification`, `sys_api_key`, `sys_two_factor`,
`sys_oauth_application`, `sys_oauth_access_token`, `sys_oauth_consent`,
`sys_jwks`, `sys_secret`) are none of them listed, and `defineRule`
refuses a rule on each with `SHARING_NOT_ENABLED` / `422` (each is
owner-less or public). So, after #30621, a rule on one of them is
refused at save and never shares anything; none remains listable. A
wider probe registered every object definition
`@objectstack/platform-objects` (root, identity, security, audit) and
plugin-sharing export, 65 objects: none was listable.
## Rows 1 and 2: readings (no code)
**Row 2 is done on `main` `8ef4d309`** (PR #30496), both halves:
- Condition in words: `describeFilterGroup` in
`ui/packages/fields/src/widgets/FilterConditionField.tsx`, 2 hits; its
pin `FilterConditionField.readWords-30209.test.tsx` (3 cases) is on
main.
- Recipient by name: the record page hands the record to the field face
(`dependentValues={data}` in
`ui/packages/plugin-detail/src/DetailSection.tsx`, 2 sites), so
`recipient-picker` reads `recipient_type`; pinned by
`recordFace.siblingValues-30209.test.tsx` (2 cases) on main.
**Row 1: the cheapest honest shape, and which arm needs spec.**
- The tab today is `record:related_list` over `sys_user_permission_set`
(`packages/platform-objects/src/pages/sys-user.page.ts`), which can only
list stored rows: sets from the default (`everyone`) binding and from
positions are never rows, so no related-list source can show them. No
existing page component shows effective access.
- **The page component needs no spec change.** The spec's component
`type` is an open string outside the namespaces its enum claims (`ai`,
`app`, `element`, `global`, `nav`, `page`, `record`), and
`mcp:authorized-assistants` on the same page is the precedent: a console
widget whose zod arm lives in `ui/packages/types`, placed by the page
with no `packages/spec` edit. A `security:effective-access` widget (any
unclaimed namespace) follows it. The ui seat's reading (6659268469) that
row 1 needs a spec vocabulary entry holds only for a type inside a
claimed namespace such as `record:`.
- **The data is the missing piece, and that arm is server.** `POST
/api/v1/security/explain` answers one object and one operation; its
`principal.permissionSets` lists the resolved names without their
origin, and only its per-layer `contributors[].via` says how a set
arrived (`baseline`, `everyone`, `position:NAME`), per object.
`resolveUserAuthzGrants` (`@objectstack/core`) carries `permissions` and
`directPermissions` but not which position or default binding brought
each other set. So "effective sets with their origin (default set,
position, direct)" needs a per-user read, for example `GET
/api/v1/security/users/:id/effective-access`, built on the resolver plus
the catalog's position bindings. If it is served by `@objectstack/rest`
over a spec service contract, that contract member is the one spec arm;
served by plugin-security on `IHttpServer` (this PR's shape), none.
- Proposed split for the seat: (1) server card: the per-user
effective-access read with origins; (2) ui card: the widget plus the
one-line page edit in `sys-user.page.ts` (platform-objects). No spec
card unless the read goes through the REST contract.
## Tests
- `src/shareable-objects.test.ts` (new, 16 cases) on real `ObjectQL` +
`SqlDriver` (better-sqlite3), with the real `SharingService` /
`SharingRuleService` and the #30621 save guard bound:
- the list: admitted objects only, business before platform, each by
label; four admitted shapes (private, read, public-with-owner, a
platform object under enforcement) and four refused (owner-less, the
plugin's bypass `sys_user`, `controlled_by_parent`, `sys_sharing_rule`
itself);
- **one verdict**: for every registered object, "not listed" equals
"`defineRule` refuses with `SHARING_NOT_ENABLED` 422";
- the credential and session objects above: absent, and each refused at
`defineRule`;
- admission: a member without `manage_sharing` and an organization-less
`manage_sharing` holder are refused `PERMISSION_DENIED`; CONTROL the
legacy `manage_platform_settings` is admitted;
- the door, through the real platform resolver (a fake auth session, a
grant store bound to a catalog whose set confers `manage_sharing`): an
administrator is served the list and the service receives the resolved
context; a member is refused `403 PERMISSION_DENIED` by the real
service; anonymous `401`, held sign-in `403 PASSWORD_EXPIRED`, a query
parameter `400` naming it, each asserting the envelope and that the list
was never read; `501` when no rule service runs and `500` when the list
cannot be read, neither with data.
- `src/sharing-route-ledger.conformance.test.ts` (new): registrar
enumeration against the ledger, both directions.
## Acceptance notes
- **Deviation from the order: no widget hint on
`sys_sharing_rule.object_name` in this PR.** The field already declares
`widget: 'object-ref'`, which draws today's object picker.
`FieldSchema.widget` is one free string and an unregistered value
degrades to the text input, so naming a new widget here before the
console registers it would turn the picker into a text box until the ui
card lands. #31004's fields had no widget, so its hint degraded to what
they already were; this field does not. The hint and the registration
should land together, in the ui card (one line in `plugin-sharing`'s
object file beside the `ui/` widget). Raised for the seat as an open
question.
- **Labels are the object's declared `label`**, not localized: the door
withholds locale like its #31004 sibling. The console localizes object
names by key as it does elsewhere.
- The door is mounted unscoped only
(`/api/v1/sharing/shareable-objects`), like the share-link and messaging
doors on `IHttpServer`; the REST rule routes also have an
environment-scoped twin, this one does not.
- A public object with an owner column is listed: the save door admits
it, because the reconcile still grants its external organization members
(#25549). The list says what the save does, nothing narrower.
## Verification
All readings on head ``59c76e787e`
(`59c76e787e7b21dd493272fdd133042ed846a902`)` unless a line says
otherwise.
<details><summary>Package suite, typecheck, lint</summary>
```
pnpm --filter @objectstack/plugin-sharing test :: exit 0 (Test Files 70 passed, Tests 1464 passed)
pnpm --filter @objectstack/plugin-sharing typecheck :: exit 0 (tsc, scripts tsc, check:test-typecheck OK, debt unchanged)
pnpm --filter @objectstack/dogfood exec vitest run test/route-ledger-live-mount-parity.dogfood.test.ts :: exit 0 (8 passed; log: "shareable objects list mounted at GET /api/v1/sharing/shareable-objects")
node node_modules/eslint/bin/eslint.js --no-inline-config <the 11 touched .ts/.mjs files> :: exit 0
```
(The package has no `lint` script; the root eslint ran over the touched
files.)
</details>
<details><summary>Negative-pin ablations (each through
scripts/ablation-replace.mjs; anchor hit once; restored blob == HEAD,
git diff HEAD empty)</summary>
- Verdict filter deleted (`if (refusal(name) !== null) continue;`): 4
failed / 12 passed: the list case, the one-verdict case, the credential
case and the door's administrator case.
- Admission removed from `listShareableObjects`
(`this.assertCanManageRules(context)` replaced by `void context`): 3
failed / 13 passed: member refused, organization-less refused, and the
door's member `403`. (A first attempt replaced the call with text the
anchor already contained; the tool refused it as a no-op (`replacement
count moved 1 -> 1`) and restored; rerun with the replacement above.)
- Anonymous branch replaced by `if (false as boolean) {`: 1 failed
(`refuses an anonymous caller 401 UNAUTHENTICATED`).
- Query refusal replaced by `if (false as boolean) {`: 1 failed
(`refuses a query parameter 400 VALIDATION_ERROR naming it`).
- Business-before-platform comparison deleted: 3 failed (the list order,
the platform classification order, the door's list).
</details>
<details><summary>Derived gates</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-stale-fill :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:turbo-task-graph :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vouch-for-reference-callers :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
Reconciliation: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran <the list above> :: exit 0` (89
derived, 89 run, 0 NOT-MEASURED, 0 UNRUN).
Not derived, run as a check on the merged tree: `pnpm --filter
@objectstack/spec run check:generated` exits 1 under the default 2 GB
heap because `tsc -p tsconfig.test.json` aborts out of memory ("failed
without diagnostics", exit 134), and exits 0 with
`NODE_OPTIONS=--max-old-space-size=8192` (CI sets 4096). This diff
touches no `packages/spec` file.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2f62c1GYk6XzbdtCa4
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 065bba7 commit 6f017e2
13 files changed
Lines changed: 950 additions & 4 deletions
File tree
- .changeset
- packages
- plugins/plugin-sharing/src
- qa/dogfood/test
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
0 commit comments