diff --git a/.changeset/24652-report-v18-upgrade-text.md b/.changeset/24652-report-v18-upgrade-text.md new file mode 100644 index 0000000000..49839d91ac --- /dev/null +++ b/.changeset/24652-report-v18-upgrade-text.md @@ -0,0 +1,11 @@ +--- +'@objectstack/plugin-security': patch +--- + +fix(plugin-security): the boot report's unnamed-grant line says what 18.0 supports for a v17 database, not a command 18.0 refuses + +Clause-②: no + +- **The error line for `sys_user_permission_set` grants whose `permission_set` is empty** used to end with a fix: plan with `os migrate organization-ownership --out plan.json`, then run `os migrate organization-ownership --apply --backup-taken --plan-file plan.json`, restart, with a warning that granting the set again by name before it runs leaves a second grant for the ceremony to report. 18.0 refuses that command on every database, so the operator was sent to a refusal. The line now ends with the sentence the drop-gate refusal of `os migrate apply --allow-destructive` carries: "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." +- **Unchanged:** the count, the "permission_set is empty" fact, the ADR-0131 D4 reason ("each confers nothing"), "Nothing was changed.", the structured meta, the log level and the report's result. Such a grant still confers nothing and is still listed. +- **Removed from the line:** the two command steps and the restart step, and the advice about granting the set again by name. They described the 18.x upgrade ceremony, which 18.0 does not carry. diff --git a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts index c2c619a3fe..20401e850b 100644 --- a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts +++ b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts @@ -14,7 +14,9 @@ * name it stay. * * Every assertion reads the structured result, never the log's wording; the - * wiring pin finds the plugin's lines by their structured meta. + * wiring pin finds the plugin's lines by their structured meta. One pin reads + * wording on purpose: the unnamed-grant line is 18.0 release text an operator + * acts on, and that release says one sentence about a v17 database everywhere. */ import { describe, it, expect, afterEach, vi } from 'vitest'; @@ -320,6 +322,44 @@ describe('[ADR-0131 D4] the catalog reference report — references whose names }); }); +describe('[ADR-0131 D4] the unnamed-grant line says what 18.0 supports for a v17 database', () => { + // The one wording pin in this file. 18.0 supports fresh installs only and its + // `os migrate organization-ownership` refuses plan and apply on every + // database, so the line an operator reads at boot must not send them to it; + // it carries the sentence the drop-gate refusal of `os migrate apply + // --allow-destructive` carries (the boot refusal and the command's own + // refusal say the same in their own terms). Everything else in the line is + // the report's own: the count, the field, the D4 reason, and that nothing was + // changed. + const V17_SENTENCE = '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.'; + + it('names the count, the empty field, the reason and "Nothing was changed", then the 18.0 sentence, and prescribes no command', async () => { + const b = await boot(); + await insertUser(b, 'usr_a'); + await insertUser(b, 'usr_b'); + await insertUnnamedGrants(b, [ + { id: 'g_one', user_id: 'usr_a', organization_id: ORG }, + { id: 'g_two', user_id: 'usr_b', organization_id: ORG }, + ]); + const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + await reportCatalogReferences(b.engine as any, { catalog: b.catalog, logger }); + + // Anti-vacuity: the report logged exactly the unnamed-grant line, found by its meta. + expect(logger.error).toHaveBeenCalledTimes(1); + const [message, , meta] = logger.error.mock.calls[0] as [string, unknown, { count: number }]; + expect(meta.count).toBe(2); + + expect(message).toContain( + '[security] 2 sys_user_permission_set grant(s) name no permission set (permission_set is empty), ' + + 'so each confers nothing (ADR-0131 D4). Nothing was changed. ' + V17_SENTENCE, + ); + expect(message.endsWith(V17_SENTENCE), 'the sentence closes the line').toBe(true); + // The refused command and every step of it are gone, not merely qualified. + expect(message).not.toMatch(/organization-ownership|os migrate|--out|--apply|--plan-file|--backup-taken|plan\.json|ceremony/); + }); +}); + describe('[ADR-0131 D4] a removed declaration is reported; re-adding it clears the report', () => { /** A member granted the auditor set directly and assigned the field-lead position that binds it. */ const seedHolder = async (b: Booted) => { diff --git a/packages/plugins/plugin-security/src/catalog-reference-report.ts b/packages/plugins/plugin-security/src/catalog-reference-report.ts index 8683807b93..14fa2de804 100644 --- a/packages/plugins/plugin-security/src/catalog-reference-report.ts +++ b/packages/plugins/plugin-security/src/catalog-reference-report.ts @@ -433,11 +433,9 @@ function logReport(report: CatalogReferenceReport, logger?: CatalogReferenceRepo logError( logger, `[security] ${unnamed.count} ${GRANT_OBJECT} grant(s) name no permission set (${GRANT_SET_NAME_FIELD} is ` + - 'empty), so each confers nothing (ADR-0131 D4). Nothing was changed. Fix: run the v18 ceremony. Plan it ' + - 'first, os migrate organization-ownership --out plan.json, and review the file it writes; then run ' + - 'os migrate organization-ownership --apply --backup-taken --plan-file plan.json, which executes that plan: ' + - 'it names each legacy grant from its retired id and lists the ones it cannot name. Then restart. Granting ' + - 'the set again by name before it runs makes a second grant the ceremony will report, not name.', + 'empty), so each confers nothing (ADR-0131 D4). Nothing was changed. 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.', { count: unnamed.count, organizations: unnamed.organizations }, ); }