git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
63 lines
3.5 KiB
JavaScript
63 lines
3.5 KiB
JavaScript
// The organization list must not reveal an org to a principal who does not own it, through
|
|
// either an invalid row or the 503 message (polish review F1, F2; P19 #868).
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import * as fs from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { createOrg, showOrg, templateRender } from '../../organization/src/org-service.mjs';
|
|
import { listOrgs, resolveOrg } from '../../organization/src/org-registry.mjs';
|
|
import { organizationConsole } from '../src/organizations.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { createDevOwner } from '../src/dev-owner.mjs';
|
|
|
|
const principal = { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' };
|
|
async function context(t) {
|
|
const root = await fs.mkdtemp(join(tmpdir(), 'orgpriv-'));
|
|
const ctx = { configDir: join(root, 'config'), dataRoot: join(root, 'data'), instance: 'test', principal };
|
|
await fs.mkdir(ctx.configDir, { mode: 0o700 }); await fs.mkdir(ctx.dataRoot, { mode: 0o700 });
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
return ctx;
|
|
}
|
|
const adapter = ctx => organizationConsole({ ctx, listOrgs, showOrg, resolveOrg, orgPaths: () => ({}), busStatus: async () => ({ healthy: true }), options: () => ({ reads: {}, bus: {} }) });
|
|
async function serverFor(t, organizations, session = principal) {
|
|
const sso = { ...createDevOwner(), session: () => ({ ...session, name: 'Test operator' }) };
|
|
const s = await startServer({ port: 0, sso, organizations, log: () => {} });
|
|
t.after(() => new Promise(r => { s.close(r); s.closeIdleConnections(); }));
|
|
return `http://127.0.0.1:${s.address().port}`;
|
|
}
|
|
|
|
test('a foreign org with a broken root stays hidden', async t => {
|
|
const ctx = await context(t);
|
|
await createOrg(ctx, 'private-tenant', await templateRender('private-tenant'));
|
|
const orgRoot = join(ctx.dataRoot, 'org-runtime', 'private-tenant');
|
|
await fs.writeFile(join(orgRoot, 'owner.json'), JSON.stringify({ ...principal, sub: 'different-owner' }));
|
|
const orgs = adapter(ctx);
|
|
assert.deepEqual(await orgs.list(principal), []);
|
|
await fs.unlink(join(orgRoot, '.mosaic-org.json'));
|
|
const response = await fetch(`${await serverFor(t, orgs)}/api/orgs`);
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(await response.json(), []);
|
|
});
|
|
|
|
test('the owner of a broken org sees it as invalid, with paths masked', async t => {
|
|
const ctx = await context(t);
|
|
await createOrg(ctx, 'alpha', await templateRender('alpha'));
|
|
await fs.unlink(join(ctx.configDir, 'businesses', 'alpha.json'));
|
|
const rows = await adapter(ctx).list(principal);
|
|
assert.equal(rows.length, 1); assert.equal(rows[0].id, 'alpha'); assert.equal(rows[0].invalid, true);
|
|
assert.equal(rows[0].reason.includes(ctx.configDir), false);
|
|
});
|
|
|
|
test('a malformed registry gives every caller a fixed 503 message with no file bytes', async t => {
|
|
const ctx = await context(t);
|
|
await createOrg(ctx, 'private-tenant', await templateRender('private-tenant'));
|
|
const outsider = { ...principal, sub: 'outsider' };
|
|
const base = await serverFor(t, adapter(ctx), outsider);
|
|
assert.deepEqual(await (await fetch(`${base}/api/orgs`)).json(), []);
|
|
await fs.writeFile(join(ctx.configDir, 'org-runtime.json'), 'private-tenant');
|
|
const response = await fetch(`${base}/api/orgs`), body = await response.json();
|
|
assert.equal(response.status, 503);
|
|
assert.deepEqual(body, { error: 'org-unavailable', message: 'Organization registry could not be read' });
|
|
});
|