Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(core): scope the legacy platform-admin deprecation pointer to walled postures by claude[bot] · Pull Request #13719 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/great-moons-attack.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
'@objectstack/core': patch
---

Scope the legacy platform-admin deprecation pointer to walled tenancy postures

The request-side notice that tells an operator their unscoped `admin_full_access`
grant row is the OLD anchor — "it is removed in a later release", "re-anchor this
deployment by declaring its administrators in configuration" — was emitted without
regard to the deployment's tenancy posture, so it fired on `single` rigs too.

`single` is the DEFAULT posture, and on a `single` rig that row is not legacy at
all: the boot-time `bootstrapPlatformAdmin` mints it to promote the first human
user, and that promotion is ruled correct and unchanged. Such a deployment was
therefore being told, once per process, to migrate off an anchor that is not
scheduled to go away, toward a variable its own promotion is pinned never to read.

The pointer is now gated on `postureEnforcesWall(resolveTenancyPosture())`, the
same predicate and the same source the boot-side detector already reads, so the
migration window's loudness is scoped to the walled postures actually in it.
Walled rigs are unaffected and still receive the notice.

⛔ Standing is not touched: this is a log-line trigger, not access control. Every
deployment resolves exactly the `PLATFORM_ADMIN` it resolved before.
16 changes: 15 additions & 1 deletion packages/core/src/security/authz-store-unavailable.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -465,8 +465,22 @@ describe('[#13279 option A] the classifier is the RELOCATED one, not a second co
// The ruling's structural half, pinned in source. A local re-spelling of
// the predicate here would pass every behavioural test above and still be
// the duplication-drift the ruling rejected (option B).
//
// ⚠️ [#13667] The positive assertion matches the BINDING LIST, not the whole
// import statement. It used to demand the exact text
// `import { isMissingTableError } from '@objectstack/types';`, which also
// pinned something the ruling never decided: that this symbol is the ONLY
// one core takes from that module. It is not any more — the walled-posture
// gate at §6b-config reads `resolveTenancyPosture` from the same package —
// and the exact-text form went red on a change that did not touch the
// predicate, the classifier, or the dependency edge. What the ruling
// decided is asserted below, undiminished: the predicate arrives by IMPORT
// from `@objectstack/types`, it is not re-spelled locally, and
// `@objectstack/metadata` is not imported here. `[^}]*` cannot cross a
// closing brace, so the binding still has to sit in THAT statement's list.
// ⛔ Do not "restore" the exact-text form: it re-pins the incidental half.
const src = readFileSync(join(REPO_ROOT, 'packages/core/src/security/resolve-authz-context.ts'), 'utf8');
expect(src).toMatch(/import \{ isMissingTableError \} from '@objectstack\/types';/);
expect(src).toMatch(/import \{[^}]*\bisMissingTableError\b[^}]*\} from '@objectstack\/types';/);
expect(src).not.toMatch(/function\s+isMissingTableError/);
// ⛔ core must not IMPORT `@objectstack/metadata` — metadata depends on
// core, and that edge is why the predicate moved rather than being imported.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,17 @@ import {
} from './resolve-authz-context.js';

const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
* [#13667] The two variables `resolveTenancyPosture()` reads, in its order:
* `OS_TENANCY_POSTURE` when set, else `OS_MULTI_ORG_ENABLED` (`true` ⇒
* `isolated`), else `single`. BOTH are driven by this file's harness, never
* just the canonical one — an arm that pinned only the first would inherit
* whatever the ambient environment happened to carry for the second, and the
* default-posture arm below exists precisely to assert what an environment
* carrying NEITHER resolves to.
*/
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
const MULTI_ORG_ENV = 'OS_MULTI_ORG_ENABLED';
const NOW = Date.parse('2026-08-29T00:00:00.000Z');

interface Recorded { object: string; where: unknown }
Expand DownExpand Up@@ -102,11 +113,17 @@ function makeSink(): PlatformAdminConfigSink & { errors: string[]; warns: string
}

let ambient: string | undefined;
let ambientPosture: string | undefined;
let ambientMultiOrg: string | undefined;
let sink: ReturnType<typeof makeSink>;

beforeEach(() => {
ambient = process.env[ENV];
ambientPosture = process.env[POSTURE_ENV];
ambientMultiOrg = process.env[MULTI_ORG_ENV];
delete process.env[ENV];
delete process.env[POSTURE_ENV];
delete process.env[MULTI_ORG_ENV];
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
sink = makeSink();
Expand All@@ -116,6 +133,10 @@ beforeEach(() => {
afterEach(() => {
if (ambient === undefined) delete process.env[ENV];
else process.env[ENV] = ambient;
if (ambientPosture === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = ambientPosture;
if (ambientMultiOrg === undefined) delete process.env[MULTI_ORG_ENV];
else process.env[MULTI_ORG_ENV] = ambientMultiOrg;
resetPlatformAdminEmailMemo();
resetLegacyPlatformAdminGrantReport();
setPlatformAdminConfigSink(undefined);
Expand All@@ -128,6 +149,19 @@ function declare(value: string | undefined): void {
resetPlatformAdminEmailMemo();
}

/**
* [#13667] Declare the deployment's REQUESTED tenancy posture for one arm.
* `undefined` clears BOTH inputs, which is how a rig that has configured no
* tenancy at all is spelled — and that rig resolves `single`, the default.
* There is no memo to drop: `resolveTenancyPosture()` re-reads the environment
* on every call.
*/
function requestPosture(value: 'single' | 'group' | 'isolated' | undefined): void {
delete process.env[MULTI_ORG_ENV];
if (value === undefined) delete process.env[POSTURE_ENV];
else process.env[POSTURE_ENV] = value;
}

describe('[#11663 L2] acceptance criterion — the configured, VERIFIED account', () => {
it('yields PLATFORM_ADMIN with the DECLARED capability set', async () => {
declare('a@b.c');
Expand DownExpand Up@@ -279,19 +313,26 @@ describe('⭐ [#11663 L2 pin P1] the derivation reads the STORED row, never the
});
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});
/**
* A principal whose PLATFORM_ADMIN rests on the LEGACY unscoped
* `admin_full_access` grant row and nothing else — the shape
* `bootstrapPlatformAdmin` mints when it promotes the first human user.
* Hoisted out of the `#11663 L2 / P5` suite so the `#13667` posture suite below
* drives the identical fixture rather than a second copy of it.
*/
const legacyTables = () => ({
sys_user: [{ id: 'usr_1', email: 'legacy@corp.example', email_verified: true }],
sys_member: [],
sys_user_position: [],
sys_position: [],
sys_position_permission_set: [],
sys_user_permission_set: [
{ id: 'ups_1', user_id: 'usr_1', permission_set_id: 'pst_1', organization_id: null },
],
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
});

describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () => {
it('an unscoped admin_full_access grant still confers PLATFORM_ADMIN with no config at all', async () => {
declare(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
Expand All@@ -302,6 +343,10 @@ describe('[#11663 L2 / P5] the legacy grant row is still honoured, loudly', () =

it('logs the deprecation pointer once, naming the holder and the config line', async () => {
declare(undefined);
// [#13667] A WALLED posture — the rigs that really are inside the migration
// window. On the default `single` posture the same fixture is silent; that
// is the suite below.
requestPosture('isolated');
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
Expand DownExpand Up@@ -343,3 +388,205 @@ describe('[#11663 L2] the sys_user read stays CONDITIONAL on config', () => {
expect(grants.posture).toBe('PLATFORM_ADMIN');
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] The deprecation pointer is POSTURE-KEYED — the request side matching
* the boot side.
*
* `bootstrapPlatformAdmin` has always been posture-keyed: under `single` a
* pre-existing unscoped `admin_full_access` holder is `already_have_admin` and
* the boot exits silently, because under Choice 4A that row IS that rig's
* anchor — first-user promotion mints it and is ruled correct and unchanged.
* Only under a walled posture is the same row the LEGACY anchor. The
* request-side pointer carried no such gate, so the default posture — `single`,
* what an unconfigured deployment resolves to — was told once per process to
* migrate off an anchor that is not scheduled to go away, toward a variable its
* own promotion is pinned never to read.
*
* ⚠️ BOTH directions are pinned here, deliberately. Gating the notice is only
* correct if the walled rigs keep hearing it: the migration window's loudness
* is the thing #11663 P5 exists to provide, and a one-sided pin would let a
* later edit switch it off for everyone and stay green.
*
* ⛔ And every arm below asserts STANDING as well as the log. This card changes
* a log trigger, not access control; a `single` rig keeps exactly the
* PLATFORM_ADMIN it had, it merely stops being nagged about it.
*/
describe('[#13667] the legacy-grant pointer fires only on the rigs in the migration window', () => {
it('WALLED rigs still hear it — both walled postures, once per process, holder and config line named', async () => {
for (const walled of ['group', 'isolated'] as const) {
declare(undefined);
requestPosture(walled);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

expect(grants.posture, walled).toBe('PLATFORM_ADMIN');
expect(sink.warns, walled).toHaveLength(1);
expect(sink.warns[0], walled).toContain('usr_1');
expect(sink.warns[0], walled).toContain(`${ENV}=legacy@corp.example`);
}
});

it('a `single` rig is SILENT — and keeps the identical PLATFORM_ADMIN standing', async () => {
declare(undefined);
requestPosture('single');
const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });

// The half this card repairs: no notice…
expect(sink.warns).toEqual([]);
// …and the half it must not disturb: the row still confers, exactly as before.
expect(grants.posture).toBe('PLATFORM_ADMIN');
expect(grants.positions[0]).toBe('platform_admin');
expect(grants.permissions).toContain(ADMIN_FULL_ACCESS);
});

it('the DEFAULT posture is silent too — an unconfigured deployment resolves `single`', async () => {
// The reach of the defect: `OS_TENANCY_POSTURE` and `OS_MULTI_ORG_ENABLED`
// both unset is what a deployment that has configured no tenancy at all
// looks like, and `resolveTenancyPosture()` answers `single` for it. This
// arm is the one that covers most rigs in the field.
declare(undefined);
requestPosture(undefined);
const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });

expect(sink.warns).toEqual([]);
expect(grants.posture).toBe('PLATFORM_ADMIN');
});

it('the legacy-anchor detection itself is untouched: `single` + a CONFIG anchor is silent for the other reason', async () => {
// The control that keeps the arm above honest. Silence under `single` must
// come from the posture gate, not from the fixture having quietly stopped
// resolving through the legacy row. Here the SAME user also matches the
// declared list, so standing no longer rests on the row and #11663 P5's own
// `else if` never runs — silence with a different cause, under both postures.
for (const p of ['single', 'isolated'] as const) {
declare('legacy@corp.example');
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const grants = await resolveUserAuthzGrants(makeQl(legacyTables()), 'usr_1', { nowMs: NOW });
expect(sink.warns, p).toEqual([]);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
}
});

it('adds NO read: the recorded query multiset is identical under both answers of the gate', async () => {
// The in-place claim at the call site — "the row is read only if it was
// already loaded, so this notice never adds a query (and so never moves the
// pinned query multiset)" — re-MEASURED rather than quoted, because this
// card is what put a new call into that branch. `resolveTenancyPosture()`
// asks the ENVIRONMENT, so the reads issued against the engine must be
// identical whichever way it answers.
const reads: Record<string, unknown[]> = {};
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
const ql = makeQl(legacyTables());
await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW });
reads[p] = ql.calls.map((c) => ({ object: c.object, where: c.where }));
}
expect(reads.single).toEqual(reads.isolated);
expect(reads.single.length).toBeGreaterThan(0); // the fixture really did resolve
});

it('…and the notice still costs no sys_user read of its own — it fires with the row never loaded', async () => {
// The other half of the same claim, isolated. Above, `sys_user` IS read —
// for `grants.email` and the `ai_seat` synthesis, neither of which is this
// branch. Seed both of those and NOTHING in the resolution needs the row;
// the notice must still fire under a walled posture, reading `userRow` as
// the undefined it already was and falling back to the generic address
// placeholder. That is what "read only if it was already loaded" means, and
// it is unchanged by the gate.
const seeded = { nowMs: NOW, seedEmail: 'seeded@corp.example', seedPermissions: ['ai_seat'] };
for (const p of ['single', 'isolated'] as const) {
declare(undefined);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
sink.warns.length = 0;

const ql = makeQl(legacyTables());
const grants = await resolveUserAuthzGrants(ql, 'usr_1', seeded);

expect(ql.calls.filter((c) => c.object === 'sys_user'), p).toHaveLength(0);
expect(grants.posture, p).toBe('PLATFORM_ADMIN');
expect(sink.warns, p).toHaveLength(p === 'single' ? 0 : 1);
}
// The walled arm named the holder, and quoted the placeholder rather than an
// address it would have had to issue a read to learn.
expect(sink.warns[0]).toContain('usr_1');
expect(sink.warns[0]).toContain(`${ENV}=<the administrator's verified email address>`);
});
});

// ───────────────────────────────────────────────────────────────────────────
/**
* [#13667] STANDING is invariant under the posture — all four arms of the
* `if (configConfersPlatformAdmin) / else if (hasPlatformAdminGrant)`
* derivation.
*
* The gate this card adds is nested INSIDE the `else if` body, so no arm of
* that chain changes shape. This suite is the measurement of that claim rather
* than an assertion about it: each of the four (config, grant) truth-table
* corners is resolved once under `single` and once under `isolated`, and the
* two envelopes must be deep-equal — same positions in the same order, same
* permissions, same rung.
*
* ⛔ If a future edit moves the posture test up into the `else if` condition, or
* anywhere else it could suppress a branch, one of these four corners changes
* and this suite goes red.
*/
describe('[#13667] standing is byte-identical across postures in all four derivation arms', () => {
const verified = { id: 'usr_1', email: 'a@b.c', email_verified: true };

const ARMS: Array<{ arm: string; env: string | undefined; tables: () => Record<string, Array<Record<string, unknown>>> }> = [
// config=T, grant=T — the config anchor wins and the `else if` is skipped.
{ arm: 'config + legacy grant', env: 'legacy@corp.example', tables: legacyTables },
// config=T, grant=F — config-only standing.
{ arm: 'config only', env: 'a@b.c', tables: () => configOnlyTables(verified) },
// config=F, grant=T — the arm this card gates the NOTICE inside.
{ arm: 'legacy grant only', env: undefined, tables: legacyTables },
// config=F, grant=F — no standing at all.
{ arm: 'neither', env: undefined, tables: () => configOnlyTables(verified) },
];

for (const { arm, env, tables } of ARMS) {
it(`resolves the SAME envelope under \`single\` and under \`isolated\` — ${arm}`, async () => {
const envelopes: Record<string, unknown> = {};
for (const p of ['single', 'isolated'] as const) {
declare(env);
requestPosture(p);
resetLegacyPlatformAdminGrantReport();
envelopes[p] = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
}
expect(envelopes.single).toEqual(envelopes.isolated);
});
}

it('and the four arms are genuinely DISTINCT — the matrix above is not four copies of one answer', async () => {
// Without this control the suite above would pass just as well on four
// fixtures that all resolved to the same thing, proving nothing about the
// arms it claims to cover.
const seen: string[] = [];
for (const { env, tables } of ARMS) {
declare(env);
requestPosture('single');
resetLegacyPlatformAdminGrantReport();
const g = await resolveUserAuthzGrants(makeQl(tables()), 'usr_1', { nowMs: NOW });
seen.push(`${g.posture}|${[...g.permissions].sort().join(',')}`);
}
// Arms 1-3 all confer PLATFORM_ADMIN (by design — that is what makes the
// notice, not the standing, the only thing this card moves); arm 4 does not.
expect(seen[0]).toContain('PLATFORM_ADMIN');
expect(seen[1]).toContain('PLATFORM_ADMIN');
expect(seen[2]).toContain('PLATFORM_ADMIN');
expect(seen[3]).toBe('MEMBER|');
});
});
Loading
Loading