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
11 changes: 11 additions & 0 deletions .changeset/24652-report-v18-upgrade-text.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
);
}
Expand Down
Loading