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 1346052
Browse filesBrowse the repository at this point in the historyBrowse files
fix(lint): security-owd-unset's fix line on a writable external object names security-external-owner-unmapped's ways out, not 'private' (#27639)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiVy7F2atKWpo5rp3YPrqddh)_Fixes#27472
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:** 可写外部对象(`external.writable: true`)没写 `sharingModel`
时,`security-owd-unset` 的修复提示不再推荐 `private`,改为给出
`security-external-owner-unmapped` 的两条出路;`os explain security-owd-unset`
补了一段说明这个例外。
- **为什么改:** 旧提示对所有自定义对象一律推荐
`private`,而可写外部对象照做后,另一条警告又说成员每次编辑都会被拒,两条提示互相打架。
- **风险与回滚:** 只改提示文字,报错级别和报错正文不变,元数据契约不变;普通对象的提示一字未动。回滚即 revert 本 PR。
- **先看:**
`packages/lint/src/validate-security-posture.ts`(`SECURITY_OWD_UNSET`
分支的
hint)、`packages/lint/src/validate-external-owner-sharing.ts`(导出复用的两条出路措辞与判定)、`packages/lint/src/validate-external-owner-sharing.test.ts`(末尾新
describe)。
- **两分钟验证:** `pnpm --filter @objectstack/lint exec vitest run
src/validate-external-owner-sharing.test.ts`,预期全绿,其中
`security-owd-unset's fix line on a writable external object` 五条通过。
- **需要你做的:** 无,席位落地。
## What changes
- **`security-owd-unset` fix line (`validate-security-posture.ts`).**
For a custom object with no `sharingModel` that
`security-external-owner-unmapped` would judge, the hint now names that
rule's two ways out instead of recommending `'private'`. Those objects
are a writable binding (`external.writable: true`), not on a datasource
this stack declares with `external.allowWrites: false`, and declaring no
ownership column a remote row serves (`owner_id`, or `created_by` on an
object without one). The new hint reads, for `spike_owned`:
`declare sharingModel: 'public_read_write' and decide who may edit in a
beforeUpdate hook, or declare owner_id in fields and have the driver
return the remote owner under that name (a built-in SQL driver does it
through external.columnMap; a plugin driver must return it itself) and
declare sharingModel: 'private' or 'public_read'; 'controlled_by_parent'
for master-detail children`
Severity (`error`), the verdict line and `path` are unchanged. Every
other object keeps today's hint byte for byte.
- **One phrasing, not three (`validate-external-owner-sharing.ts`).**
`PUBLIC_READ_WRITE_WAY_OUT` and the declaration clause
(`declareOwnershipColumnWayOut(column)`) are now exported from the rule
that owns them. The judgement "does this object have an ownership gap"
moves into `externalOwnershipGap(obj, readOnlyDatasources)`, and the
datasource set into `readOnlyExternalDatasources(stack)`. Both rules
call the same predicate, so the hint swaps on exactly the objects the
warning fires on. These are module exports, not barrel exports: the
package's public surface is unchanged. The external rule's own findings
are byte-identical, and its existing whole-hint pins stay green.
- **`os explain security-owd-unset` (`rule-explanations.ts`).** A new
paragraph explains the writable-external exception to the `'private'`
recommendation, names the two ways out and points at `os explain
security-external-owner-unmapped`.
- Changeset: `@objectstack/lint` patch.
## Tests
- `validate-external-owner-sharing.test.ts` adds the describe block
`security-owd-unset's fix line on a writable external object`:
- `spike_owned` (the spike stack, verbatim) gets the external hint (both
shared clauses, no `recommended`), with severity, path and message
unchanged;
- a managed custom object (`leave_request`) keeps the old hint, pinned
whole;
- an `ownership: 'none'` external object is told to declare
`created_by`;
- a read-only binding, a declared `owner_id` and an `allowWrites: false`
datasource keep the old hint;
- either way out, once taken (`public_read_write`, or `private` plus a
declared `owner_id`), clears both rules.
- `validate-security-posture.test.ts`: the explanation pin also asserts
the external paragraph. The receiver meta-test (#5017) excuses
`ownershipGap`, which is the helper's own return value and not metadata.
- Ablation: with the fix committed, `scripts/ablation-replace.mjs`
replaced `const ownershipGap = externalOwnershipGap(obj,
readOnlyDatasources);` with `undefined` (anchor 1 → 0, blob
`ce9dd0f8e857` → `f178c04309b6`). Two of the new pins went red
(`spike_owned …`, `names created_by …`), 24 stayed green. The tool
restored the file (blob == HEAD, `git diff HEAD` empty). The test
imports `src/` by relative path, so no `dist/` rebuild was involved.
## Acceptance notes
- **The hint swaps only where the warning fires, which is narrower than
the card's literal "`external.writable: true`".** An external object
that already declares `owner_id` (or `created_by` on an object without
`owner_id`), or that sits on an `allowWrites: false` datasource, keeps
the `'private'` recommendation. There `security-external-owner-unmapped`
is silent and `'private'` is not the refused shape: the owner edits, and
since #27476 reads are scoped by the owner it proves. This follows the
card's first clause ("names the ways out
`security-external-owner-unmapped` names"). Where that rule names none,
there is nothing to name.
- The guide (`content/docs/data-modeling/virtual-objects.mdx`, the
"Choose the sharing model on purpose" callout) still reads true and is
not edited.
- `packages/cli/test/explain-rule-id.test.ts` pins the managed-object
hint, which is unchanged. It is declared to CI: the CLI's dependency
closure is not built in this container (the file fails to load on
`@objectstack/types` resolution, before any assertion runs).
## Verification
<details><summary>Local runs (head 4e9b79c)</summary>
The tests ran on the code tree of `c4b4bd65`. The head `4e9b79ca` adds
only the changeset, so its code is the same. The gate commands ran at
`4e9b79ca`.
- `pnpm --filter '@objectstack/lint^...' build` under
`os-verify-lock.sh`: VERDICT command-exit 0.
- `pnpm --filter @objectstack/lint typecheck && pnpm --filter
@objectstack/lint test` under the lock: VERDICT command-exit 0; `Test
Files 177 passed (177)`, `Tests 7604 passed | 5 skipped (7609)`. The
typecheck ran `tsc --noEmit` and `check:test-typecheck`.
- Targeted: `vitest run src/validate-external-owner-sharing.test.ts
src/validate-security-posture.test.ts src/rule-explanations.test.ts
src/validate-security-posture.runtime-surface.test.ts`: `Test Files 4
passed (4)`, `Tests 228 passed (228)`.
- Ablation (`scripts/ablation-replace.mjs`, see Tests): VERDICT
command-exit 1, `Tests 2 failed | 24 passed (26)`, restored `blob ==
HEAD (ce9dd0f)`, `git diff HEAD` empty.
- `node scripts/pm/dispatch-gates.mjs --commands`: 58 commands derived;
all 58 were run with their exit codes recorded. `--ran`: "58 derived
famil(ies) accounted for — 55 run, 3 NOT-MEASURED (3 DERIVED from a
recorded exit 3)". 55 exited 0, including `check:docs-transcript-drift`
("4 declared transcript value(s) across 471 page(s) … equal what the
registry derives today"), `check:doc-authoring`, `check:nul-bytes`,
`check:cross-package-test-inputs` and `check:type-check-debt`.
- NOT MEASURED, exit 3 (prerequisite), left to CI:
- `node scripts/check-plugin-teardown-shape.mjs --self-test`: a shallow
clone cannot reach its pinned positive-control commit;
- `pnpm check:dual-build-cjs-loads` and `pnpm check:lean-entry-closure`:
both read built `dist/` of packages outside this closure.
- NOT MEASURED, declared to CI:
`packages/cli/test/explain-rule-id.test.ts`. The CLI closure is not
built here, so the file fails to load (`@objectstack/types` entry
unresolved) before any assertion runs. It pins the managed-object hint,
which this diff leaves unchanged.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2c5z3q4h8inrn5kwqV
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(lint): `security-owd-unset`'s fix line on a writable external object names the ways out `security-external-owner-unmapped` names, instead of recommending `'private'`
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`security-owd-unset` refuses a custom object with no `sharingModel`, and its fix line recommended `sharingModel: 'private'` for every such object. On a writable external object (`external.writable: true`) whose ownership column no remote row serves, `'private'` is the shape `security-external-owner-unmapped` warns refuses every member edit: the author followed one rule's advice into the other's warning.
11
+
12
+
-**Now, on a writable binding.** For exactly the objects `security-external-owner-unmapped` judges (`external.writable: true`, with no declared `owner_id`, or `created_by` on an object without `owner_id`), the fix line reads: declare `sharingModel: 'public_read_write'` and decide who may edit in a `beforeUpdate` hook, or declare the ownership column in `fields` and have the driver return the remote owner under that name, and declare `'private'` or `'public_read'`; `'controlled_by_parent'` for master-detail children. Both clauses are that rule's own wording, including its shorter one when an `external.columnMap` entry already targets the column.
13
+
-**Now, on a read-only binding.** An external object that no member writes (`external.writable` not `true`, or a datasource this stack declares with `external.allowWrites: false`) and whose `owner_id` no declared field or `external.columnMap` entry names is told to declare `sharingModel: 'public_read'`: on `'private'` a member reads none of its rows.
14
+
-**Unchanged.** The severity (`error`) and the verdict line. Every other custom object, and an external object whose owner is provable, keeps the `'private'` recommendation.
15
+
-`os explain security-owd-unset` explains both external cases and points at `os explain security-external-owner-unmapped`.
16
+
17
+
The same fix line reaches the runtime publish gate's 422 issue for an `object` write. That gate sees no `datasources`, so a writable binding on an `allowWrites: false` datasource gets the writable-binding line there; neither door recommends `'private'` for it.
0 commit comments