Skip to content

Commit 119c2db

Browse files
revert: PR #30222 (main red #30255) (#30262)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgFMKmBZQVKU38CQ6a3dqyHp)_ domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL ## 维护者速读 - **改了什么:** 回退 PR #30222(`490d136f`):`member_default` 去掉 `sys_comment` 和 `sys_activity` 两行,连同它的测试和 changeset,文件恢复到合并前的原样。 - **为什么改:** 合入后 main 的 `Dogfood Regression Gate (2/3)` 连红两次。没装审计插件的启动里,这两个对象没有注册,克隆 `member_default` 被保存门以 422 拒收。按 main 红的约定先止血。 - **风险与回滚:** 只撤落地,不撤裁决:普通成员暂时回到改动前(没有评论和动态),等前向修复。这个 changeset 还没发布,所以回退不发布任何东西。 - **先看:** `packages/plugins/plugin-security/src/objects/default-permission-sets.ts` 的差异。 - **两分钟验证:** `git diff 314ad95 HEAD -- packages/plugins/plugin-security .changeset` 为空。 - **需要您做的:** 无,席位落地。 Refs #30255 Refs #30108 Clause-②: no Metadata-protocol: none ## What this does Reverts the squash commit `490d136f` (PR #30222, #30108's landing): the two `member_default` grants (`sys_comment` read, create and edit; `sys_activity` read), the comment block above them, the integration test file, the flipped pins in `member-default-explicit-allow.test.ts` and `comment-reaction-own-record-floor.test.ts`, and the changeset `.changeset/30108-member-default-feed-grants.md`. The five paths are byte-identical to their state in `314ad955`, the commit before the landing: `git diff 314ad95 HEAD` over them is empty. No later commit on `main` touched them. #30108's ruling A stands. Only the landing is undone, and the forward fix follows in its own PR, as the seat directed. ## Why main went red Measured in run 41961254209 (`797f9257`), `Dogfood Regression Gate (2/3)`, case `permission-set-clone-door.dogfood.test.ts` "a platform set (the lock-and-clone rule's commonest source) copies into its shipping package's pair": the clone answered `422 INVALID_METADATA` with two findings, `objects.sys_comment` and `objects.sys_activity`, each "no package registers an object of that name and none is saved here". `@objectstack/verify`'s `bootStack` constructs no `AuditPlugin` (fact 3 of `packages/cli/src/commands/serve-audit-registration.contract.test.ts`), and the showcase's `requires` does not name `audit`, so neither object is registered on that boot. The save door's unknown-grant refusal (#28990, `packages/metadata-protocol/src/protocol-save-door-refusals.ts`) judges every grant of a new item, and a clone is a new item. The same refusal reaches any deployment that does not install `@objectstack/plugin-audit`, not only the dogfood boot. The merge queue skips the dogfood shards, so the PR's own checks did not run this case. ## Verification See the seat hand-off for the local run of the dogfood case on this head. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CatT2ajve5PJf1Hhzj)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d0da880 commit 119c2db

5 files changed

Lines changed: 13 additions & 607 deletions

File tree

‎.changeset/30108-member-default-feed-grants.md‎

Lines changed: 0 additions & 16 deletions
This file was deleted.

‎packages/plugins/plugin-security/src/comment-reaction-own-record-floor.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@
3333
* ## Who grants the bits
3434
*
3535
* The object-level create / read / delete bits come from an ordinary
36-
* position-distributed set an application ships (as an app grants the
37-
* `sys_comment` delete bit; the baseline grants comments' read, create and
38-
* edit, never delete), never from the `everyone`-bound baseline, which
36+
* position-distributed set an application ships (as an app grants
37+
* `sys_comment` today), never from the `everyone`-bound baseline, which
3938
* ADR-0090 D5 keeps free of delete bits. Which shipped set — if any — carries
4039
* them is an open decision on #22566; the floor pinned here holds whatever set
4140
* hands the delete bit out.

‎packages/plugins/plugin-security/src/member-default-explicit-allow.test.ts‎

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
2828
import type { PermissionSet } from '@objectstack/spec/security';
2929
import { PermissionEvaluator } from './permission-evaluator.js';
3030
import { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';
31-
import { defaultPermissionSets, BETTER_AUTH_MANAGED_OBJECTS, EXTERNAL_DEFAULT_PERMISSION_SET } from './objects/default-permission-sets.js';
31+
import { defaultPermissionSets, BETTER_AUTH_MANAGED_OBJECTS } from './objects/default-permission-sets.js';
3232

3333
const evaluator = new PermissionEvaluator();
3434
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -267,10 +267,13 @@ describe('[#7344] the personal inbox is readable by a member, scoped to their ow
267267
expect(rls.compileFilter(policiesFor(object, 'select') as any, {} as any)).toEqual(RLS_DENY_FILTER);
268268
});
269269

270-
it('`sys_activity` rides on its own ruling, not on this one: no `_self` policy scopes it to the caller', () => {
271-
// The inbox ruling left it out because it has no `user_id` to scope by. Its
272-
// grant (the `[#30108]` block below) is scoped by its parent-record gate, so
273-
// no inbox-shaped `_self` carve-out may appear for it.
270+
it('`sys_activity` is deliberately NOT granted — the ruling excludes it', () => {
271+
// Named as an explicit negative so a future "while we are here" sweep has to
272+
// argue with the ruling rather than quietly widen past it. It is not a
273+
// per-user-scoped shape; a separate question if it ever matters.
274+
for (const { operation } of AXES) {
275+
expect(allows(operation, [MEMBER_DEFAULT], 'sys_activity'), `sys_activity ${operation}`).toBe(false);
276+
}
274277
const names = (MEMBER_DEFAULT.rowLevelSecurity ?? []).map((p: any) => p.name);
275278
expect(names).not.toContain('sys_activity_self');
276279
});
@@ -288,60 +291,6 @@ describe('[#7344] the personal inbox is readable by a member, scoped to their ow
288291
});
289292
});
290293

291-
// [#30108 — maintainer ruling 6633054352, 「同意 A」] A record's comments and its
292-
// activity timeline, for every member who can read the record. The baseline
293-
// opens the two OBJECTS and nothing more; which rows is plugin-audit's
294-
// parent-record gates' answer, driven on a real stack in
295-
// `member-default-feed-grants.integration.test.ts`. What is pinned here is the
296-
// shape of the opening: the part that would quietly widen if it moved.
297-
const FEED_OBJECTS = ['sys_comment', 'sys_activity'] as const;
298-
299-
describe('[#30108] the baseline opens comments and activity, and leaves the rows to the parent gates', () => {
300-
it('sys_comment: a member reads, posts and edits — and holds no delete bit', () => {
301-
expect(allows('find', [MEMBER_DEFAULT], 'sys_comment'), 'read').toBe(true);
302-
expect(allows('insert', [MEMBER_DEFAULT], 'sys_comment'), 'create').toBe(true);
303-
expect(allows('update', [MEMBER_DEFAULT], 'sys_comment'), 'edit').toBe(true);
304-
expect(allows('delete', [MEMBER_DEFAULT], 'sys_comment'), 'delete').toBe(false);
305-
});
306-
307-
it('sys_activity: a member reads — the audit mirror is its only writer', () => {
308-
expect(allows('find', [MEMBER_DEFAULT], 'sys_activity'), 'read').toBe(true);
309-
for (const operation of ['insert', 'update', 'delete']) {
310-
expect(allows(operation, [MEMBER_DEFAULT], 'sys_activity'), operation).toBe(false);
311-
}
312-
});
313-
314-
it.each(FEED_OBJECTS)('%s: no read or create row policy of the baseline stands in for the parent gate', (object) => {
315-
// A select- or insert-class policy here would answer the gate's question with
316-
// a column predicate neither object can answer (no owner or user column, and
317-
// a different parent object on every row).
318-
expect(policiesFor(object, 'select').map((p: any) => p.name), 'select').toEqual([]);
319-
expect(policiesFor(object, 'insert').map((p: any) => p.name), 'insert').toEqual([]);
320-
});
321-
322-
it('sys_comment: an edit stays under the ownership floor — a member edits the comments they wrote', () => {
323-
// The moderation carve-out is the DELETE limb only; the edit limb is the
324-
// wildcard floor, so a member's edit reaches their own comments alone.
325-
expect(policiesFor('sys_comment', 'update').map((p: any) => p.name)).toEqual(['owner_only_writes']);
326-
});
327-
328-
it.each(FEED_OBJECTS)('%s: no export, view-all or modify-all rides along', (object) => {
329-
const perm = (MEMBER_DEFAULT.objects as any)[object];
330-
expect(perm, `${object} is named`).toBeTruthy();
331-
expect(perm.allowDelete ?? false).toBe(false);
332-
expect(perm.allowExport ?? false).toBe(false);
333-
expect(perm.viewAllRecords ?? false).toBe(false);
334-
expect(perm.modifyAllRecords ?? false).toBe(false);
335-
});
336-
337-
it.each(FEED_OBJECTS)('%s: the external baseline does not name it — the grant reaches no external member', (object) => {
338-
const external = defaultPermissionSets.find((p) => p.name === EXTERNAL_DEFAULT_PERMISSION_SET)!;
339-
expect(external, `${EXTERNAL_DEFAULT_PERMISSION_SET} is shipped`).toBeTruthy();
340-
expect(allows('find', [external], object)).toBe(false);
341-
expect(Object.keys(external.objects ?? {})).not.toContain(object);
342-
});
343-
});
344-
345294
describe('[#5491] the admin sets keep their wildcards (this is a BASELINE change only)', () => {
346295
it.each(['admin_full_access', 'organization_admin', 'viewer_readonly'])(
347296
'%s still carries a `*` entry',

0 commit comments

Comments
 (0)