Skip to content

Commit 40622bd

Browse files
committed
test(plugin-auth,cli): pin the sys_organization insert half and the ceremony's attribution of a NULL admin_full_access grant
Claude-Session: https://claude.ai/code/session_01ERm1aSqQLiSN87PFmwRfUV Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4be9398 commit 40622bd

2 files changed

Lines changed: 125 additions & 15 deletions

File tree

‎packages/cli/src/utils/organization-ownership-apply.test.ts‎

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { createHash } from 'node:crypto';
2222
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
2323
import { tmpdir } from 'node:os';
2424
import { join } from 'node:path';
25-
import { permissionSetDefinitionFromRow } from '@objectstack/core';
25+
import { permissionSetDefinitionFromRow, readDefaultOrganizationId } from '@objectstack/core';
2626
import { anonymousFormIntakeCandidates, anonymousFormIntakeWithdrawnIn, hashSpec } from '@objectstack/metadata-core';
2727
import { buildOrganizationOwnershipPlan, inventoryDigest } from './organization-ownership-plan.js';
2828
import {
@@ -83,7 +83,7 @@ function executor(opts: { failOnceOn?: RegExp } = {}): ApplyExecutor {
8383
* Like the registry, it serves a saved definition only from the next boot on
8484
* ({@link boot}), never the moment it is saved.
8585
*/
86-
function catalog(): CatalogPort & MetadataDoorPort & TemplatePort & {
86+
function catalog(opts: { adminSetDeclared?: boolean } = {}): CatalogPort & MetadataDoorPort & TemplatePort & {
8787
saved: Map<string, Record<string, unknown>>;
8888
templates: Map<string, Record<string, unknown>>;
8989
/** Environment `view` saves, in order, with the package each was saved into. */
@@ -92,6 +92,11 @@ function catalog(): CatalogPort & MetadataDoorPort & TemplatePort & {
9292
} {
9393
const declared = new Map<string, Record<string, unknown>>([
9494
['permission|member_default', permissionSetDefinitionFromRow(MEMBER_DEFAULT_ROW)],
95+
// The composed security plugin ships it; `adminSetDeclared: false` is a
96+
// catalog composed without that plugin.
97+
...(opts.adminSetDeclared === false
98+
? []
99+
: [['permission|admin_full_access', permissionSetDefinitionFromRow(ADMIN_FULL_ACCESS_ROW)] as [string, Record<string, unknown>]]),
95100
['position|org_admin', { name: 'org_admin', label: 'Org Admin', delegatable: false, permissionSets: ['member_default'] }],
96101
]);
97102
const saved = new Map<string, Record<string, unknown>>();
@@ -276,6 +281,14 @@ const MEMBER_DEFAULT_ROW = {
276281
system_permissions: '[]',
277282
};
278283

284+
/** The platform set the `single` first-user promotion grants (plugin-security ships it). */
285+
const ADMIN_FULL_ACCESS_ROW = {
286+
name: 'admin_full_access',
287+
label: 'Full Access',
288+
object_permissions: '{}',
289+
system_permissions: '[]',
290+
};
291+
279292
/** A 17.x-shaped database carrying every fate, on the post-stage-8 shape. */
280293
function seed(): void {
281294
db.exec(`
@@ -290,7 +303,8 @@ function seed(): void {
290303
('ps_null', NULL, 'member_default', 'Member', 'platform', 1, '{"crm_deal":{"allowRead":true}}', '[]'),
291304
('ps_org', 'org_default', 'member_default', 'Member', 'package', 1, '{"crm_deal":{"allowRead":true}}', '[]'),
292305
('ps_auth', 'org_default', 'field_auditor', 'Field Auditor', 'admin', 1,
293-
'{"crm_deal":{"allowRead":true,"allowEdit":true}}', '["export_reports"]');
306+
'{"crm_deal":{"allowRead":true,"allowEdit":true}}', '["export_reports"]'),
307+
('ps_admin_mirror', NULL, 'admin_full_access', 'Full Access', 'platform', 1, '{}', '[]');
294308
CREATE TABLE sys_position (id TEXT PRIMARY KEY, organization_id TEXT, name TEXT, label TEXT, managed_by TEXT,
295309
delegatable INTEGER, active INTEGER);
296310
INSERT INTO sys_position VALUES
@@ -334,7 +348,11 @@ function seed(): void {
334348
('grant_1', 'org_default', 'usr_1', 'ps_org', NULL),
335349
('grant_2', NULL, 'usr_2', 'ps_null', NULL),
336350
('grant_3', 'org_default', 'usr_3', 'ps_auth', NULL),
337-
('grant_4', 'org_default', 'usr_4', 'ps_null', 'member_default');
351+
('grant_4', 'org_default', 'usr_4', 'ps_null', 'member_default'),
352+
-- the first-user promotion's grant as an earlier release wrote it: no
353+
-- organization (ADR-0131 D5 — the ceremony attributes it to the Default
354+
-- Organization, the owner the resolver reads the anchor by)
355+
('grant_promoted', NULL, 'usr_admin', 'ps_admin_mirror', NULL);
338356
CREATE TABLE sys_business_unit (id TEXT PRIMARY KEY, organization_id TEXT, parent_business_unit_id TEXT);
339357
INSERT INTO sys_business_unit VALUES ('bu_b', 'org_b', NULL);
340358
CREATE TABLE sys_user_position (id TEXT PRIMARY KEY, organization_id TEXT, user_id TEXT, position TEXT, business_unit_id TEXT);
@@ -397,6 +415,21 @@ function accessBefore(): string[] {
397415
return [...out].sort();
398416
}
399417

418+
/**
419+
* The fixture database as an engine answers `find` — the one-argument
420+
* spelling the authorization resolver's reads use — so a reader that takes an
421+
* engine reads these same rows.
422+
*/
423+
const sqliteEngine = () => ({
424+
async find(object: string, query: { where?: Record<string, unknown>; limit?: number }) {
425+
const where = query.where ?? {};
426+
const keys = Object.keys(where);
427+
return db.prepare(
428+
`SELECT * FROM "${object}"${keys.length ? ` WHERE ${keys.map((k) => `"${k}" = ?`).join(' AND ')}` : ''} ORDER BY id LIMIT ${query.limit ?? 100}`,
429+
).all(...keys.map((k) => where[k] as string)) as Record<string, unknown>[];
430+
},
431+
});
432+
400433
/** Effective access AFTER — the v18 reading, on the next boot: every reference is a name, resolved in the catalog. */
401434
async function accessAfter(c: ReturnType<typeof catalog>): Promise<string[]> {
402435
c.boot();
@@ -519,6 +552,7 @@ describe('ADR-0131 D10 --apply — the acceptance fixture', () => {
519552
it('comes through with effective access identical, mirrors gone, owners written, the orphan reported and holding certification, counts reconciling', async () => {
520553
const before = accessBefore();
521554
const countsBefore = counts();
555+
const defaultOrganizationBefore = await readDefaultOrganizationId(sqliteEngine());
522556
const c = catalog();
523557
const l = ledger();
524558

@@ -582,6 +616,7 @@ describe('ADR-0131 D10 --apply — the acceptance fixture', () => {
582616
{ id: 'grant_2', permission_set: 'member_default' },
583617
{ id: 'grant_3', permission_set: 'field_auditor' },
584618
{ id: 'grant_4', permission_set: 'member_default' },
619+
{ id: 'grant_promoted', permission_set: 'admin_full_access' },
585620
]);
586621
expect(rows(`SELECT name FROM pragma_table_info('sys_user_permission_set') WHERE name = 'permission_set_id'`)).toEqual([]);
587622
expect(writes).toContain('ALTER TABLE sys_user_permission_set DROP COLUMN permission_set_id');
@@ -591,6 +626,14 @@ describe('ADR-0131 D10 --apply — the acceptance fixture', () => {
591626
expect(rows(`SELECT organization_id FROM sys_attachment WHERE id = 'att_1'`)).toEqual([{ organization_id: 'org_b' }]);
592627
expect(rows(`SELECT organization_id FROM crm_deal WHERE id = 'deal_2'`)).toEqual([{ organization_id: 'org_default' }]);
593628
expect(rows(`SELECT organization_id FROM sys_user_permission_set WHERE id = 'grant_2'`)).toEqual([{ organization_id: 'org_default' }]);
629+
// [ADR-0131 D5] The promotion's organization-less `admin_full_access` grant
630+
// is owned by the organization the resolver reads the anchor by — the
631+
// answer of `readDefaultOrganizationId` (`@objectstack/core`) over this
632+
// same database — so the grant that anchored platform standing before the
633+
// ceremony is the one that anchors it after.
634+
expect(defaultOrganizationBefore).toBe('org_default');
635+
expect(rows(`SELECT organization_id FROM sys_user_permission_set WHERE id = 'grant_promoted'`))
636+
.toEqual([{ organization_id: defaultOrganizationBefore }]);
594637

595638
// The unattributable row is still present, still NULL — never guessed,
596639
// never deleted — reported by id, and held with its count and remedy.
@@ -922,7 +965,9 @@ describe('ADR-0131 D10 --apply — idempotent, resumable, gated', () => {
922965
expect(rows(`SELECT organization_id FROM sys_file WHERE id = 'file_1'`)).toEqual([{ organization_id: 'org_b' }]);
923966
expect(c.saved.size).toBe(0);
924967
expect(report.steps.find((s) => s.step === 'mirror-deletion')!.status).toBe('not-reached');
925-
expect(rows('SELECT COUNT(*) AS n FROM sys_permission_set')).toEqual([{ n: 3 }]);
968+
expect(rows('SELECT COUNT(*) AS n FROM sys_permission_set')).toEqual([{ n: 4 }]);
969+
// The promotion's organization-less grant is not attributed under a wall.
970+
expect(rows(`SELECT organization_id FROM sys_user_permission_set WHERE id = 'grant_promoted'`)).toEqual([{ organization_id: null }]);
926971
// The promotion runs under every posture; nothing is deleted before mirror deletion.
927972
expect([...c.templates.keys()].sort()).toEqual(['auth.reset_password|en-US', 'crm.memo|en-US', 'crm.note|en-US', 'crm.renewal|zh-CN']);
928973
expect(rows('SELECT COUNT(*) AS n FROM sys_email_template')).toEqual([{ n: 6 }]);
@@ -2102,7 +2147,7 @@ describe('ADR-0131 D10 --apply — a name only the uncomposed security plugin de
21022147
('grant_nobypass', 'org_default', 'usr_admin', NULL, 'organization_admin_no_bypass'),
21032148
('grant_unknown', 'org_default', 'usr_9', NULL, 'zz_unknown_set');
21042149
`);
2105-
const report = await apply(catalog(), ledger(), { securityPlugin: NO_SECRET });
2150+
const report = await apply(catalog({ adminSetDeclared: false }), ledger(), { securityPlugin: NO_SECRET });
21062151

21072152
const rewrite = step(report, 'rewrite');
21082153
expect(rewrite.status).toBe('blocked');
@@ -2128,7 +2173,7 @@ describe('ADR-0131 D10 --apply — a name only the uncomposed security plugin de
21282173
('grant_admin_legacy', 'org_default', 'usr_admin', 'ps_admin', NULL),
21292174
('grant_gone_legacy', 'org_default', 'usr_9', 'ps_gone', NULL);
21302175
`);
2131-
const report = await apply(catalog(), ledger(), { securityPlugin: NO_SECRET });
2176+
const report = await apply(catalog({ adminSetDeclared: false }), ledger(), { securityPlugin: NO_SECRET });
21322177

21332178
const rewrite = step(report, 'rewrite');
21342179
expect(rewrite.status).toBe('blocked');
@@ -2145,7 +2190,7 @@ describe('ADR-0131 D10 --apply — a name only the uncomposed security plugin de
21452190

21462191
it('authored-conversion: the seeded everyone binding gets the composition, never removal; an undeclared position keeps its remedy', async () => {
21472192
db.exec(v17Bindings);
2148-
const report = await apply(catalog(), ledger(), { securityPlugin: NO_SECRET });
2193+
const report = await apply(catalog({ adminSetDeclared: false }), ledger(), { securityPlugin: NO_SECRET });
21492194

21502195
const conversion = step(report, 'authored-conversion');
21512196
expect(conversion.status).toBe('blocked');
@@ -2161,7 +2206,7 @@ describe('ADR-0131 D10 --apply — a name only the uncomposed security plugin de
21612206

21622207
it('under a wall, the rewrite gate: the everyone binding gets the composition; an undeclared position gets the walled remedy', async () => {
21632208
db.exec(v17Bindings);
2164-
const report = await apply(catalog(), ledger(), { posture: 'isolated', securityPlugin: NO_SECRET });
2209+
const report = await apply(catalog({ adminSetDeclared: false }), ledger(), { posture: 'isolated', securityPlugin: NO_SECRET });
21652210

21662211
expect(step(report, 'authored-conversion').status).toBe('done');
21672212
const rewrite = step(report, 'rewrite');
@@ -2181,7 +2226,7 @@ describe('ADR-0131 D10 --apply — a name only the uncomposed security plugin de
21812226
['not said', undefined],
21822227
])('control (%s): authored-conversion keeps the remedy — the composition is the boot\'s answer, never inferred', async (_label, securityPlugin) => {
21832228
db.exec(v17Bindings);
2184-
const conversion = step(await apply(catalog(), ledger(), { securityPlugin }), 'authored-conversion');
2229+
const conversion = step(await apply(catalog({ adminSetDeclared: false }), ledger(), { securityPlugin }), 'authored-conversion');
21852230
expect(blockerOf(conversion, 'position everyone ')).toMatch(/or remove the bindings/);
21862231
expect(conversion.blockers.join('\n')).not.toContain(COMPOSITION);
21872232
});
@@ -2194,7 +2239,7 @@ describe('ADR-0131 D10 --apply — a name only the uncomposed security plugin de
21942239
INSERT INTO sys_user_permission_set VALUES ('grant_admin', 'org_default', 'usr_admin', NULL, 'admin_full_access');
21952240
${v17Bindings}
21962241
`);
2197-
const rewrite = step(await apply(catalog(), ledger(), { posture: 'isolated', securityPlugin }), 'rewrite');
2242+
const rewrite = step(await apply(catalog({ adminSetDeclared: false }), ledger(), { posture: 'isolated', securityPlugin }), 'rewrite');
21982243
expect(blockerOf(rewrite, 'the permission set admin_full_access,')).toMatch(/or remove the grants/);
21992244
// Not the pointer. This fake catalog declares no `everyone` (a composed boot declares it, sealed), so which
22002245
// walled remedy follows is not this control's question.

‎packages/plugins/plugin-auth/src/last-admin-guard.default-organization.test.ts‎

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
* that ends the last one.
1313
* 2. **The guard judges the organization writes that move the anchor.** The
1414
* Default Organization is the one with `slug = 'default'`, else the only
15-
* one — so re-slugging it while another organization exists, or deleting
16-
* it, takes the standing away with no grant row touched.
15+
* one — so re-slugging it while another organization exists, deleting it,
16+
* or creating a second organization beside a sole one not slugged
17+
* `default`, takes the standing away with no grant row touched.
1718
*
1819
* Same method as `last-admin-guard.config-anchor.test.ts`: a REAL ObjectQL
1920
* engine over better-sqlite3 `:memory:`, so the engine dispatches the hooks.
@@ -104,7 +105,9 @@ afterEach(async () => {
104105
* writes — owned by the Default Organization — and nothing else (no
105106
* membership, so the organization-owner half of the enumeration counts nobody).
106107
*/
107-
async function promotedSingle(opts: { secondOrganization?: boolean; grantOwner?: string } = {}): Promise<ObjectQL> {
108+
async function promotedSingle(
109+
opts: { secondOrganization?: boolean; grantOwner?: string; defaultSlug?: string } = {},
110+
): Promise<ObjectQL> {
108111
const engine = new ObjectQL();
109112
engines.push(engine);
110113
engine.registerDriver(
@@ -117,7 +120,7 @@ async function promotedSingle(opts: { secondOrganization?: boolean; grantOwner?:
117120
bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] });
118121
guard = registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, { packageId: 'test.last-admin-guard.default-org' });
119122

120-
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, SYSTEM);
123+
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: opts.defaultSlug ?? 'default' }, SYSTEM);
121124
if (opts.secondOrganization) {
122125
await engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM);
123126
}
@@ -175,6 +178,68 @@ describe('[ADR-0131 D5] the organization writes that move the anchor off that gr
175178
});
176179
});
177180

181+
describe('[ADR-0131 D5] creating an organization that moves the anchor off that grant is judged', () => {
182+
// A sole organization is the Default Organization whatever its slug, so the
183+
// fixture's `acme` organization owns the anchoring grant until a second
184+
// organization arrives: then none is the Default Organization (the second
185+
// one has another slug), or the second one is (it is slugged `default`).
186+
const organizationIds = async (engine: ObjectQL) =>
187+
((await engine.find('sys_organization', { where: {}, ...SYSTEM })) as Array<Record<string, unknown>>)
188+
.map((row) => row.id)
189+
.sort();
190+
191+
it('a second organization beside a sole one not slugged `default` is refused, and nothing is written', async () => {
192+
const engine = await promotedSingle({ defaultSlug: 'acme' });
193+
await expect(engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM))
194+
.rejects.toMatchObject({ ...refusal, object: 'sys_organization' });
195+
expect(await organizationIds(engine)).toEqual(['org_default']);
196+
// The grant still anchors: its holder is still the last administrator.
197+
await expect(ban(engine, 'usr_admin')).rejects.toMatchObject(refusal);
198+
});
199+
200+
it('a new organization slugged `default` beside a sole one with another slug is refused', async () => {
201+
const engine = await promotedSingle({ defaultSlug: 'acme' });
202+
await expect(engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'default' }, SYSTEM))
203+
.rejects.toMatchObject({ ...refusal, object: 'sys_organization' });
204+
expect(await organizationIds(engine)).toEqual(['org_default']);
205+
});
206+
207+
it('the refusal names the remedy that keeps the anchor: slug the current one `default` first, then the insert is admitted', async () => {
208+
const engine = await promotedSingle({ defaultSlug: 'acme' });
209+
const err = await engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM)
210+
.then(() => undefined, (e: unknown) => e as Error);
211+
expect(err?.message).toContain('organization creation');
212+
await expect(engine.update('sys_organization', { id: 'org_default', slug: 'default' }, SYSTEM)).resolves.toBeDefined();
213+
await expect(engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM)).resolves.toBeDefined();
214+
await expect(ban(engine, 'usr_admin')).rejects.toMatchObject(refusal);
215+
});
216+
217+
it('CONTROL — a `default`-slugged Default Organization stays put, so a second organization is admitted', async () => {
218+
const engine = await promotedSingle();
219+
await expect(engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM)).resolves.toBeDefined();
220+
await expect(ban(engine, 'usr_admin')).rejects.toMatchObject(refusal);
221+
});
222+
223+
it('CONTROL — an administrator left standing elsewhere admits the insert: the guard is break-glass, not standing preservation', async () => {
224+
const engine = await promotedSingle({ defaultSlug: 'acme' });
225+
await engine.insert('sys_member', { id: 'mem_owner', user_id: 'usr_admin', organization_id: 'org_default', role: 'owner' }, SYSTEM);
226+
await expect(engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM)).resolves.toBeDefined();
227+
});
228+
229+
it('CONTROL — under a wall no grant anchors, so the insert is admitted without a read', async () => {
230+
const engine = await promotedSingle({ defaultSlug: 'acme' });
231+
process.env[POSTURE_ENV] = 'isolated';
232+
const reads: string[] = [];
233+
const find = engine.find.bind(engine);
234+
(engine as unknown as { find: typeof engine.find }).find = ((object: string, ...rest: unknown[]) => {
235+
reads.push(object);
236+
return (find as (...a: unknown[]) => unknown)(object, ...rest);
237+
}) as typeof engine.find;
238+
await expect(engine.insert('sys_organization', { id: 'org_b', name: 'B', slug: 'b' }, SYSTEM)).resolves.toBeDefined();
239+
expect(reads).toEqual([]);
240+
});
241+
});
242+
178243
describe('[#24948] the same question about deleting the Default Organization, asked before anything is deleted', () => {
179244
const members = (engine: ObjectQL) =>
180245
engine.find('sys_member', { where: {}, ...SYSTEM }) as Promise<Array<Record<string, unknown>>>;

0 commit comments

Comments
 (0)