// 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' }); });