Skip to content

Commit d0514e6

Browse files
fix(plugin-security): the platform-admin promotion WITHHELD warning says what 18.0 supports for a v17 database (#24718)
Fixes #24709 Clause-②: no ## What this changes The boot warning "[security] platform-admin promotion WITHHELD: an unscoped grant exists whose permission-set name (sys_user_permission_set.permission_set) was never written" ended by sending the operator to the organization-ownership migration: "The organization-ownership migration's id-to-name rewrite (ADR-0131 D10) names it; the next pass after that reads it by name." 18.0 refuses that migration on every database, so an operator booting a v17-shaped database with an unnamed grant was pointed at a refusal. The warning now ends with the sentence the boot refusal and the two sibling release-text rows already carry (PR #24635 and PR #24679 landed it): "18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path." Text only. The withholding does not move: `findPlatformAdminGrantHolder`, the `unnamedHolder` guard, the returned `adminPromoted: false` / `admin_grant_unnamed` result, the `logger.warn` / `logger.info` fallback and every other clause of the warning (what is withheld, why, "this pass will not mint a second one") are unchanged. Files: `packages/plugins/plugin-security/src/bootstrap-platform-admin.ts` (the string only, 2 lines), `packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts` (one new case), `.changeset/24709-platform-admin-withheld-18-text.md` (`@objectstack/plugin-security` patch). ## Pin One case in `grant-readers-unnamed-grant.test.ts`, beside the existing unnamed-holder cases, which already build the state (an unscoped `admin_full_access` grant written with a NULL name, an older account the age rule would otherwise promote). It runs `bootstrapPlatformAdmin` with a capturing logger and asserts: - the pass took the unnamed-holder branch (`adminPromoted: false`, `reason: 'admin_grant_unnamed'`) and logged the WITHHELD warning exactly once (anti-vacuity); - the whole warning text equals the kept clauses followed by the 18.0 sentence, and ends with it; - the text names none of `organization-ownership`, `os migrate`, `--out`, `--apply`, `--plan-file`, `ceremony`, `id-to-name`. ## Evidence (head `7357dcca56`) - Closure build: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build`, under the verify lock, VERDICT command-exit 0. - Pin file alone: `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/grant-readers-unnamed-grant.test.ts` is 8 passed (7 existing plus the new case), VERDICT command-exit 0, run again after the ablation was restored: 8 passed. - Typecheck: `pnpm --filter @objectstack/plugin-security typecheck` exit 0, including `check:test-typecheck` (the test layer compiles, 0 errors). - Ablation (the diff changes a test-pinned string): the old clause put back through `scripts/ablation-replace.mjs` (anchor hit once, blob `eac7d8f87baa` to `f9123b5ffce1` on disk). The test imports `./bootstrap-platform-admin.js` from `src`, so no rebuild leg applies. Result: 1 failed, 7 passed, the new case red with the old clause as the received text, the expected direction (red). Restore proven on disk: blob equals the HEAD blob and `git diff HEAD` is empty. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 68 commands; each was run with its exit captured before any pipe, then `--ran` reconciled: 68 derived, 67 run (all exit 0), 1 NOT-MEASURED, 0 UNRUN. `pnpm check:i18n` first exited 3 (prerequisite not met), was re-run after building its stated closure, and exited 0. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`, reason: it reads the built output of every package and exits 3 (prerequisite not met) without a whole-workspace build, which CI's `Build Core` owns; this diff changes no export, `package.json` or build config. - Left to CI: the full plugin-security suite, `test:repo`, the type-check lanes, and the lint workflow's repo-wide gates. ## Acceptance notes - Not changed, by the ruling that only the warning string moves: the doc comment in `bootstrap-platform-admin.ts` (the "By name, and the grant that names nothing" section), the comment above the `unnamedHolder` guard, the header of `grant-readers-unnamed-grant.test.ts` and a comment in `cleanup-package-permissions.ts` all still describe the v18 ceremony's rewrite in the future tense. They are developer-facing comments, not operator text; noted, not filed. - The second `promotion WITHHELD for ...` warning in the same file (the Default Organization one) is a different condition and names no migration; unchanged. - Order note from the card: #24681 (the 18.x steps that reopen the ceremony) re-states the surfaces #24635 taught, and this line joins them when it lands. #24681 remains open and this PR does not touch its files. - #24637 remains open: this is its `domain:services` row 2, and other rows of that family are outside this PR. - `origin/main` moved by five commits while this was open (none touches plugin-security or this changeset), so main was not merged in. --- _Generated by [Claude Code](https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec2694d commit d0514e6

3 files changed

Lines changed: 50 additions & 2 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): the platform-admin promotion WITHHELD warning says what 18.0 supports for a v17 database, not a migration 18.0 refuses
6+
7+
Clause-②: no
8+
9+
- **The boot warning "[security] platform-admin promotion WITHHELD: an unscoped grant exists whose permission-set name (sys_user_permission_set.permission_set) was never written"** used to end with a way on: "The organization-ownership migration's id-to-name rewrite (ADR-0131 D10) names it; the next pass after that reads it by name." 18.0 refuses that migration on every database, so the operator was sent to a refusal. The warning now ends with the sentence the boot refusal and the drop-gate refusal of `os migrate apply --allow-destructive` carry: "18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path."
10+
- **Unchanged:** the withholding itself (an unscoped grant that names no set is still read as an administrator who may already exist, and no second platform-admin grant is minted), the facts the warning states (which column was never written, that the grant predates it, that the set it holds can no longer be read), the log level and the `info` fallback when the logger has no `warn`, and the returned `admin_grant_unnamed` result.
11+
- **Removed from the warning:** the pointer to the organization-ownership migration's rewrite and the promise that the next pass reads the grant by name afterwards. Both described the 18.x upgrade path, which 18.0 does not carry.

‎packages/plugins/plugin-security/src/bootstrap-platform-admin.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,8 @@ export async function bootstrapPlatformAdmin(
880880
'[security] platform-admin promotion WITHHELD: an unscoped grant exists whose permission-set name '
881881
+ '(sys_user_permission_set.permission_set) was never written — a grant from a release before that '
882882
+ 'column existed. Which set it holds can no longer be read, and it may be the platform administrator\'s, '
883-
+ 'so this pass will not mint a second one. The organization-ownership migration\'s id-to-name rewrite '
884-
+ '(ADR-0131 D10) names it; the next pass after that reads it by name.';
883+
+ 'so this pass will not mint a second one. 18.0 supports fresh installs only and does not upgrade a v17 '
884+
+ 'database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path.';
885885
if (logger?.warn) logger.warn(withheld);
886886
else logger?.info?.(withheld);
887887
return {

‎packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,43 @@ describe('[ADR-0131 D4] a grant that names nothing — the platform-admin bootst
221221
await unname(engine, { id: 'g_admin' });
222222
expect(await findExistingPlatformAdmin(engine, defaultPermissionSets)).toBeUndefined();
223223
});
224+
225+
// The one wording pin on the withheld-promotion warning. 18.0 supports fresh
226+
// installs only and its `os migrate organization-ownership` refuses plan and
227+
// apply on every database, so the warning an operator reads at boot must not
228+
// send them to it; it ends with the sentence the unnamed-grant line of the
229+
// catalog reference report and the boot refusal carry, in the same terms.
230+
// Everything before the sentence is the warning's own: what is withheld, why,
231+
// and that this pass mints no second grant.
232+
const V17_SENTENCE = '18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a '
233+
+ '17.x release until the 18.x release that carries the upgrade path.';
234+
235+
it('unnamed, the withheld warning keeps what is withheld and why, ends with the 18.0 sentence, and prescribes no migration', async () => {
236+
const engine = await standingAdminUnnamed();
237+
await unname(engine, { id: 'g_admin' });
238+
// An existing database: the platform-bucket rows an earlier release seeded.
239+
await seedLegacyPlatformBucket(engine, defaultPermissionSets);
240+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
241+
const report = await bootstrapPlatformAdmin(engine, defaultPermissionSets, { logger });
242+
243+
// Anti-vacuity: the pass took the unnamed-holder branch and logged its warning exactly once.
244+
expect(report).toMatchObject({ adminPromoted: false, reason: 'admin_grant_unnamed' });
245+
const withheld = logger.warn.mock.calls
246+
.map(([message]) => String(message))
247+
.filter((message) => message.startsWith('[security] platform-admin promotion WITHHELD'));
248+
expect(withheld).toHaveLength(1);
249+
const [message] = withheld;
250+
251+
expect(message).toBe(
252+
'[security] platform-admin promotion WITHHELD: an unscoped grant exists whose permission-set name '
253+
+ '(sys_user_permission_set.permission_set) was never written — a grant from a release before that '
254+
+ 'column existed. Which set it holds can no longer be read, and it may be the platform administrator\'s, '
255+
+ 'so this pass will not mint a second one. ' + V17_SENTENCE,
256+
);
257+
expect(message.endsWith(V17_SENTENCE), 'the sentence closes the warning').toBe(true);
258+
// The refused command and the rewrite it performed are gone, not merely qualified.
259+
expect(message).not.toMatch(/organization-ownership|os migrate|--out|--apply|--plan-file|ceremony|id-to-name/);
260+
});
224261
});
225262

226263
describe('[ADR-0131 D4] a grant that names nothing — the organization-administrator reconcile', () => {

0 commit comments

Comments
 (0)