Skip to content

Commit d0fdd3c

Browse files
committed
fix(plugin-security): stop clobbering admin-edited capability scope (#2909 T3)
scope is an admin-editable classification select on sys_capability, but the curated seeder refreshed it every boot — silently reverting admin reclassifications. Make it seed-once (insert only); label/description remain platform-owned and keep refreshing. Lock both sides with tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
1 parent e42b1ea commit d0fdd3c

3 files changed

Lines changed: 35 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): stop clobbering admin-edited capability `scope` on boot (#2909 T3). `scope` is an admin-editable classification select on sys_capability, but the curated seeder refreshed it on every boot alongside label/description — silently reverting admin reclassifications. It is now seed-once: written on insert, never refreshed (a curated scope change in a new platform version requires a data migration; recorded in the ADR-0094 addendum).

packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ describe('bootstrapSystemCapabilities (ADR-0066 D1 back-compat seed)', () => {
6565
expect(ql.rows.find((x: any) => x.name === 'approve_invoice')).toBeDefined();
6666
});
6767

68+
// [#2909 T3] `scope` is an admin-editable classification face — seed-once.
69+
it('does NOT clobber an admin-edited scope on re-seed (label/description still refresh)', async () => {
70+
const ql = makeQl();
71+
await bootstrapSystemCapabilities(ql, []);
72+
const cap = KNOWN_CAPABILITIES[0];
73+
const row = ql.rows.find((x) => x.name === cap.name)!;
74+
// Admin reclassifies the capability and tweaks nothing else.
75+
row.scope = row.scope === 'org' ? 'platform' : 'org';
76+
const adminScope = row.scope;
77+
row.label = 'stale label';
78+
await bootstrapSystemCapabilities(ql, []);
79+
expect(row.scope).toBe(adminScope); // admin's edit survives the boot
80+
expect(row.label).toBe(cap.label); // platform display fields refreshed
81+
});
82+
83+
it('still writes scope on first insert', async () => {
84+
const ql = makeQl();
85+
await bootstrapSystemCapabilities(ql, []);
86+
for (const cap of KNOWN_CAPABILITIES) {
87+
expect(ql.rows.find((x) => x.name === cap.name)?.scope).toBe(cap.scope);
88+
}
89+
});
90+
6891
it('marks manage_org_users as org-scoped and the rest platform', () => {
6992
const org = KNOWN_CAPABILITIES.find((c) => c.name === 'manage_org_users');
7093
expect(org?.scope).toBe('org');

packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,13 @@ export async function bootstrapSystemCapabilities(
100100
for (const def of byName.values()) {
101101
const existing = await tryFind(ql, 'sys_capability', { name: def.name }, 1);
102102
if (existing[0]?.id) {
103-
// Keep label/description/scope fresh, but do NOT clobber admin edits to
104-
// managed_by/active — only platform-owned fields are reconciled.
105-
if (await tryUpdate(ql, 'sys_capability', { id: existing[0].id, label: def.label, description: def.description, scope: def.scope })) {
103+
// Keep label/description fresh, but do NOT clobber admin edits — only
104+
// platform-owned display fields are reconciled. `scope` is an
105+
// admin-editable classification face (plain select on sys_capability),
106+
// so it is seed-once: written on insert, never refreshed (#2909 T3).
107+
// A curated scope change in a new platform version needs a data
108+
// migration — recorded in the ADR-0094 addendum.
109+
if (await tryUpdate(ql, 'sys_capability', { id: existing[0].id, label: def.label, description: def.description })) {
106110
updated += 1;
107111
}
108112
} else {

0 commit comments

Comments
 (0)