Skip to content

Commit d2b6ede

Browse files
committed
Merge origin/main into claude/issue-20752-stage2-rest-strings
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
2 parents ee1a101 + 454bbb6 commit d2b6ede

35 files changed

Lines changed: 2525 additions & 75 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/service-analytics': minor
4+
'@objectstack/plugin-audit': minor
5+
'@objectstack/objectql': minor
6+
---
7+
8+
fix(security)!: stored metadata bodies are projected or refused at the audit, analytics, realtime and data-door filter/sort exits too
9+
10+
Clause-②: no (narrowing)
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) further read/copy/evaluate exits for a stored metadata body (sys_metadata / sys_metadata_history), each routed through the one shared redactor or refused: the audit/activity write-time copy is projected, a data-door filter or sort on the body column is refused (the sibling of the already-registered-as-not-required groupBy refusal), an analytics query member on the body column is refused, and a data.record.* realtime event body is projected. No authorable key, spelling, export or stored shape moves, and no stored row is read differently by any metadata consumer; the published surfaces gain and lose nothing. The other categories are closed on facts: the packages publish (not `unpublished`); no ADR-0087 id covers a filter/sort target, an analytics member or an event body (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
13+
14+
**BREAKING**: this narrows what three doors accept or serve for the two stored-metadata tables — the generic data door refuses a filter or sort on the body column, the analytics door refuses it as a dimension / measure / filter / sort member, and the realtime event and the audit/activity copy now carry the body as its type's read projection instead of the stored bytes. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
15+
16+
**What changes.**
17+
18+
- **Audit / activity copy (`@objectstack/plugin-audit`).** The audit writer copies a `sys_metadata` / `sys_metadata_history` row into `sys_audit_log.new_value` / `old_value` and `sys_activity.metadata`. That copy now projects the body through the shared redactor, so stored credential material is withheld from the second store too. A new `os migrate audit-metadata-bodies` command rewrites the copies already at rest (dry run by default, `--apply` to write, idempotent).
19+
- **Analytics (`@objectstack/service-analytics`).** A query naming the stored body column of these objects as a dimension, measure, filter or sort is refused with `400 INVALID_FIELD`, before any strategy runs — the posture analytics already takes for a member it will not evaluate.
20+
- **Realtime (`@objectstack/objectql`).** A `data.record.*` event projects its `after` / `changes` body through the same redactor, so a subscriber to these objects' events receives no stored credential.
21+
- **Data door filter / sort (`@objectstack/metadata-protocol`).** A filter or sort on the body column is refused with `400 INVALID_FIELD`, the same family and shape as the existing groupBy refusal.
22+
23+
**What stays answerable.** Every scalar column of these objects — `type`, `name`, `scope`, `state`, timestamps — is still grouped, filtered, sorted, counted and served; only the body column is affected. Every other object is unchanged.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
---
5+
6+
feat(spec,cli): shared seam for projecting stored metadata bodies, and the audit rewrite command
7+
8+
Clause-②: no
9+
10+
`@objectstack/spec/kernel` gains the family-wide primitives for the
11+
stored-metadata-body security invariant, beside the per-type redactor registry
12+
they build on: `STORED_METADATA_BODY_OBJECTS` / `isStoredMetadataBodyObject`,
13+
the `STORED_METADATA_BODY_COLUMN` / `STORED_METADATA_TYPE_COLUMN` names, and
14+
`redactStoredMetadataBody` / `redactStoredMetadataRow` / `redactStoredMetadataRows`.
15+
These project a stored row's body through the one `getMetadataTypeRedactor`
16+
definition, so every surface that serves, copies or evaluates such a body shares
17+
one rule rather than a copy per package. Additive — no existing export changes.
18+
19+
`@objectstack/cli` gains `os migrate audit-metadata-bodies`, the one-off rewrite
20+
of at-rest metadata-body copies in `sys_audit_log` / `sys_activity` (dry run by
21+
default, `--apply` to write, idempotent).
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(rest,runtime): writing `datasource` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability the datasource admin door already requires (#21124)
7+
8+
Clause-②: no
9+
10+
- A write of a `datasource` definition through `/api/v1/meta` (and its plural spelling) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`POST /api/v1/datasources`, `PATCH` and `DELETE /api/v1/datasources/:name`) already requires for the same create, update and remove. Every write verb is judged alike: the save (`PUT /meta/datasource/:name`, a draft save included), the reset (`DELETE`), `/publish` and `/rollback`.
11+
- A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. Nothing is written. The answer is the same whether or not the named item exists.
12+
- The write doors' own authoring admission is unchanged and still applies, so a datasource write needs `manage_platform_settings` and `manage_metadata` both. Platform administrators hold both through `admin_full_access`. Every other metadata type, and every read route, is unchanged. `external_catalog` writes are unchanged: that type's own write door requires `manage_metadata`.
13+
- Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by.
14+
- If you write datasource definitions through `/api/v1/meta` with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or write through the datasource admin door with a caller that already holds it.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **67 of the 218 sites are spelled that way**. A
125+
forwarding shim cannot, and **67 of the 219 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **218** |
190+
| 175 write call sites | quoted in the merged changeset | **219** |
191191
| 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **34** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **144 of 218** decidable, **74** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 99 decidably elevated, 0 decidably not, 102 undecidable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **144 of 219** decidable, **75** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 100 decidably elevated, 0 decidably not, 102 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -207,11 +207,11 @@ would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
99 of 218 (45%) as decidably elevated, with 102 more whose elevation is a
210+
100 of 219 (46%) as decidably elevated, with 102 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `9 / 218`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `9 / 219`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217217
without tenant context" — **34 further sites** have an options argument this
@@ -223,31 +223,31 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **218** |
226+
| write call sites on the application surface | **219** |
227227
| …whose object name is statically decidable | 144 |
228-
| …whose object name is chosen at run time | 74 |
228+
| …whose object name is chosen at run time | 75 |
229229
| …against an object with tenancy ENABLED | 143 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 134 |
231+
| threading a tenant context | 135 |
232232
| PROVABLY carrying none (options read, no context key) | **17** |
233233
| …of those, against a decidably tenancy-enabled object | **9** |
234234
| options argument UNREADABLE — may or may not carry one | 67 |
235235
| …of those, against a decidably tenancy-enabled object | 34 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 99 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 100 |
237237
| threading a context that is decidably NOT elevated | 0 |
238238
| threading a context whose elevation is a run-time fact | 102 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 174 |
242+
| receiver carried a readable engine type | 175 |
243243
| receiver erased, placed by the object NAME | 24 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 15 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246246

247247
| object name spelled inline | 101 |
248248
| object name spelled through a `const` | 43 |
249249
| object name is an `object: string` parameter | 17 |
250-
| object name is some other run-time expression | 57 |
250+
| object name is some other run-time expression | 58 |
251251

252252
### Subtractions the census could NOT defend — enforced
253253

@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-09-25 at `62171c25e`.
300+
Measured on 2026-10-01 at `752173845`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 572 |
305-
| engine-shaped types recognised | 61 |
304+
| tracked non-test sources scanned | 586 |
305+
| engine-shaped types recognised | 64 |
306306
| declared objects in the registry | 115 |
307-
| same-named calls subtracted as non-engine | 144 |
307+
| same-named calls subtracted as non-engine | 145 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3333

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 218 |
36+
| Write call sites | 219 |
3737
| Object name statically decidable | 144 |
38-
| Object name chosen at run time | 74 |
38+
| Object name chosen at run time | 75 |
3939
| Against a tenancy-enabled object | 143 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 134 |
41+
| Threading a tenant context | 135 |
4242
| Provably carrying none | 17 |
4343
| …and decidably tenancy-enabled | 9 |
4444
| Options argument unreadable | 67 |
4545
| …and decidably tenancy-enabled | 34 |
46-
| Threading a decidably elevated context | 99 |
46+
| Threading a decidably elevated context | 100 |
4747
| Threading a decidably non-elevated context | 0 |
4848
| Threading a context of undecidable elevation | 102 |
4949

@@ -90,14 +90,14 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-09-25 at `62171c25e`.
93+
Measured on 2026-10-01 at `752173845`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 572 |
98-
| engine-shaped types recognised | 61 |
97+
| tracked non-test sources scanned | 586 |
98+
| engine-shaped types recognised | 64 |
9999
| declared objects in the registry | 115 |
100-
| same-named calls subtracted as non-engine | 144 |
100+
| same-named calls subtracted as non-engine | 145 |
101101

102102
## Every site
103103

@@ -116,6 +116,7 @@ Measured on 2026-09-25 at `62171c25e`.
116116
| `packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts` | `update` | `objectPlan.object` | undecidable | context, elevation undecidable | 1 |
117117
| `packages/plugins/plugin-audit/src/auth-event-audit.ts` | `insert` | `sys_audit_log` | enabled | options unreadable | 1 |
118118
| `packages/plugins/plugin-audit/src/read-audit.ts` | `insert` | `sys_audit_log` | enabled | options unreadable | 1 |
119+
| `packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts` | `update` | `object` | undecidable | elevated | 1 |
119120
| `packages/plugins/plugin-auth/src/admin-import-users.ts` | `insert` | `sys_audit_log` | enabled | options unreadable | 1 |
120121
| `packages/plugins/plugin-auth/src/admin-import-users.ts` | `update` | `sys_user` | enabled | options unreadable | 2 |
121122
| `packages/plugins/plugin-auth/src/admin-set-user-manager.ts` | `update` | `sys_user` | enabled | context, elevation undecidable | 1 |

0 commit comments

Comments
 (0)