Build review (changes 8) and security review (changes 4): - P1-1: an owner-approved fire no longer writes the revoke before the apply (decision-stale). The fire stops the copy's runs, the fire is written, then the retire revoke starts for the fired instance. - P2 (both): archive and delete from the CLI and the Console pass the organization's managed Gitea remote step (copyRemoteFor). A remote step that was not attempted or failed keeps the record revoking with failed remote, so the bus finishes it after restore; no done tombstone over a live token. - Security P2-1: a bound server that cannot be read is a failed remote step, not not-managed. updateOrg refuses project-repo-bound when a project a copy binding grants loses or changes its remote or git. - P3-1 (security): a delete whose organization file cannot be read says copiesUnreadable, never an empty copy list. - P3-2 (security): the remote step requires the record's incarnation to equal the registry's live one. - P3-5 (build): the archive's slot discard checks each parent directory (no symlink, own uid, private mode). - P3-6 (build): resume keeps credential-revoke-failed as the gate code while a failed op runs again. - P3-4 (build): a test pins the refused-begin reopen guard.
36 lines
2.6 KiB
JavaScript
36 lines
2.6 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { chmodSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fixture, orgFixture, tmp } from './helpers.mjs';
|
|
import { busStart, busStop, busStopForDelete, copyRemoteFor } from '../src/org-bus.mjs';
|
|
import { createOrg, deleteOrg, templateRender } from '../../organization/src/org-service.mjs';
|
|
|
|
test('deletion stops the recorded running bus before moving its runtime', { timeout: 60000 }, async t => {
|
|
const root = tmp(t, 'd-'), f = fixture(root), dir = join(root, '.config', 'mosaic-test');
|
|
mkdirSync(join(root, '.config'), { mode: 0o700 }); renameSync(join(root, 'config'), dir); chmodSync(dir, 0o700); chmodSync(join(dir, 'businesses'), 0o700);
|
|
writeFileSync(join(f.dataRoot, 'instance.json'), JSON.stringify({ instanceVersion: 1, name: 'test' }), { mode: 0o600 });
|
|
const env = { ...f.env, HOME: root, MOSAIC_CONFIG: join(dir, 'config.json') };
|
|
const ctx = { configDir: dir, dataRoot: f.dataRoot, instance: 'test', env, principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
|
|
await createOrg(ctx, 'a', await templateRender('a'));
|
|
const started = await busStart(ctx, 'a', { detach: true, devLocalOwner: true, noLaunch: true });
|
|
t.after(async () => { try { await busStop(ctx, 'a'); } catch {} });
|
|
const receipt = await deleteOrg(ctx, 'a', { busStop: busStopForDelete, confirm: 'a', ifRevision: 1 });
|
|
assert.equal(receipt.bus.stopped, true); assert.equal(receipt.bus.pid, started.pid);
|
|
assert.ok(receipt.moves.some(move => move.from === started.root));
|
|
});
|
|
|
|
// #1873 CB4: archive and delete from the CLI get the organization's managed Gitea remote step. With no
|
|
// admin slot and no bound repository it never claims a revoke: a malformed grant fails, another
|
|
// service is not managed, and a grant whose bound projects have no server fails (the copy stays fenced).
|
|
test('CB4 copyRemoteFor gives the managed remote step, which never claims a revoke it did not make', async t => {
|
|
const { root, f } = await orgFixture(t, 'a');
|
|
const ctx = { configDir: join(root, '.config', 'mosaic-test'), dataRoot: f.dataRoot, instance: 'test' };
|
|
const remote = await copyRemoteFor(ctx);
|
|
assert.equal(typeof remote, 'function');
|
|
const record = (grantId) => ({ orgIncarnation: 'x', copyId: 'c', services: { gitea: { grantId, resources: { projects: ['work'] } } } });
|
|
assert.equal(await remote('a', 'rev', record('nope'), 'gitea'), 'failed');
|
|
assert.equal(await remote('a', 'rev', record('gitea:1:2'), 'other'), 'not-managed');
|
|
assert.equal(await remote('a', 'rev', record('gitea:1:2'), 'gitea'), 'failed');
|
|
});
|