@@ -22,7 +22,7 @@ import { createHash } from 'node:crypto';
2222import { mkdtempSync , readFileSync , rmSync } from 'node:fs' ;
2323import { tmpdir } from 'node:os' ;
2424import { join } from 'node:path' ;
25- import { permissionSetDefinitionFromRow } from '@objectstack/core' ;
25+ import { permissionSetDefinitionFromRow , readDefaultOrganizationId } from '@objectstack/core' ;
2626import { anonymousFormIntakeCandidates , anonymousFormIntakeWithdrawnIn , hashSpec } from '@objectstack/metadata-core' ;
2727import { buildOrganizationOwnershipPlan , inventoryDigest } from './organization-ownership-plan.js' ;
2828import {
@@ -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. */
280293function 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. */
401434async 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 ( / o r r e m o v e t h e b i n d i n g s / ) ;
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 ( / o r r e m o v e t h e g r a n t s / ) ;
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.
0 commit comments