/** * D12's reachability policy (hook-process-boundary stage 3, tasks 5.1b/5.2). * * The rules under test, in the order they short-circuit: * 1. a request on the module's OWN credential is allowed — the credential is * the scope; * 2. an explicit non-root user is allowed — the fleet key's authority is * root on fleet systems, and a non-root account trusts it only where an * operator installed it (the cPanel bootstrap); * 3. the target is a system the module or one of its instances provisioned; * 4. or it is a machine in celilo's pool that this module's own config names * — the case a config-only manager module needs, since it provisions * nothing and so can never satisfy rule 3 (celilo#1225); * * and otherwise a refusal names the module, the target, and the capability * route. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { type DbClient, createDbClient } from '../db/client'; import { capabilities, machines, moduleConfigs, moduleInstances, moduleSystems, modules, } from '../db/schema'; import { remoteAccessPolicy } from './remote-access'; const TEST_DB_PATH = './test-remote-access.db'; function seedModule(db: DbClient, id: string): void { db.insert(modules) .values({ id, name: id, version: '1.0.0', manifestData: {}, sourcePath: `/tmp/${id}`, state: 'VERIFIED', }) .run(); } function seedSystem(db: DbClient, moduleId: string, ipv4Address: string, hostname: string): void { db.insert(moduleSystems) .values({ moduleId, name: 'main', hostname, ipv4Address, zone: 'dmz', infraType: 'machine', updatedAt: new Date(), }) .run(); } /** A box the operator admitted to celilo's pool. */ function seedPoolMachine(db: DbClient, ipAddress: string, hostname: string): void { db.insert(machines) .values({ id: `machine-${hostname}`, hostname, zone: 'internal', ipAddress, sshUser: 'root', sshKeyEncrypted: 'x', hardware: { cpu_cores: 1, memory_mb: 512, disk_gb: 8 }, }) .run(); } /** A config value the operator set on a module. */ function seedConfig(db: DbClient, moduleId: string, key: string, value: string): void { db.insert(moduleConfigs).values({ moduleId, key, value }).run(); } describe('remoteAccessPolicy', () => { let db: DbClient; beforeEach(() => { db = createDbClient({ path: TEST_DB_PATH }); seedModule(db, 'caddy'); seedSystem(db, 'caddy', '10.0.10.10', 'www'); seedModule(db, 'homebridge'); seedSystem(db, 'homebridge', '10.0.30.7', 'homebridge'); db.insert(capabilities) .values({ moduleId: 'caddy', capabilityName: 'public_web', version: '1.0.0', data: {} }) .run(); }); afterEach(async () => { db.$client.close(); for (const suffix of ['', '-shm', '-wal']) { const p = `${TEST_DB_PATH}${suffix}`; if (existsSync(p)) await rm(p); } }); test("allows the module's own system", () => { const policy = remoteAccessPolicy('caddy', db); expect(policy.checkTarget({ ipv4_address: '10.0.10.10' }, false)).toEqual({ allowed: true }); }); test("allows an instance's system, one level deep", () => { seedModule(db, 'forgejo-runner-abc123def456'); db.insert(moduleInstances) .values({ moduleId: 'forgejo-runner-abc123def456', parentId: 'homebridge', submodule: 'runner', instanceKey: 'lab', state: 'ready', }) .run(); seedSystem(db, 'forgejo-runner-abc123def456', '10.0.30.9', 'runner-lab'); const policy = remoteAccessPolicy('homebridge', db); expect(policy.checkTarget({ ipv4_address: '10.0.30.9' }, false)).toEqual({ allowed: true }); }); test("refuses another module's system, naming module, target and the capability route", () => { const verdict = remoteAccessPolicy('homebridge', db).checkTarget( { ipv4_address: '10.0.10.10' }, false, ); expect(verdict.allowed).toBe(false); if (verdict.allowed) throw new Error('unreachable'); expect(verdict.message).toContain("Module 'homebridge'"); expect(verdict.message).toContain('10.0.10.10'); expect(verdict.message).toContain("belongs to module 'caddy'"); expect(verdict.message).toContain('public_web'); }); test('refuses an address no deployed system has, with the generic guidance', () => { const verdict = remoteAccessPolicy('homebridge', db).checkTarget( { ipv4_address: '203.0.113.50' }, false, ); expect(verdict.allowed).toBe(false); if (verdict.allowed) throw new Error('unreachable'); expect(verdict.message).toContain("Module 'homebridge'"); expect(verdict.message).toContain('203.0.113.50'); expect(verdict.message).toContain('capability'); }); test('allows any target when the request carries its own credential', () => { const policy = remoteAccessPolicy('homebridge', db); expect(policy.checkTarget({ ipv4_address: '10.0.10.10' }, true)).toEqual({ allowed: true }); }); test('allows an explicit non-root user (the off-fleet account case)', () => { const policy = remoteAccessPolicy('generic-cpanel-hosting-provider', db); expect( policy.checkTarget({ ipv4_address: '198.51.100.7', user: 'peba-hosting' }, false), ).toEqual({ allowed: true }); }); test("an explicit 'root' user gets no such pass", () => { const verdict = remoteAccessPolicy('homebridge', db).checkTarget( { ipv4_address: '10.0.10.10', user: 'root' }, false, ); expect(verdict.allowed).toBe(false); }); describe('a config-only module managing a pool machine (celilo#1225)', () => { // `iptables` is the archetype: it configures a pre-existing firewall and is // deployed onto nothing, so `module_systems` holds no row for it EVER. Rule // 3 is unsatisfiable for it by construction, not by timing. const FIREWALL = '10.226.1.254'; beforeEach(() => { seedModule(db, 'iptables'); }); test("allows a pool machine this module's config names", () => { seedPoolMachine(db, FIREWALL, 'fw-main'); seedConfig(db, 'iptables', 'firewall_ip', FIREWALL); expect( remoteAccessPolicy('iptables', db).checkTarget({ ipv4_address: FIREWALL }, false), ).toEqual({ allowed: true }); }); test('pool membership ALONE is not authorization', () => { // Otherwise any module could reach every box celilo knows about, which is // far wider than the rule this policy states. seedPoolMachine(db, FIREWALL, 'fw-main'); const result = remoteAccessPolicy('iptables', db).checkTarget( { ipv4_address: FIREWALL }, false, ); expect(result.allowed).toBe(false); }); test('config ALONE is not authorization', () => { // A module names its own config keys. If naming an address were enough, a // module could point itself at any box in the world. seedConfig(db, 'iptables', 'firewall_ip', FIREWALL); const result = remoteAccessPolicy('iptables', db).checkTarget( { ipv4_address: FIREWALL }, false, ); expect(result.allowed).toBe(false); }); test("one module's config does not unlock another module's box", () => { seedPoolMachine(db, FIREWALL, 'fw-main'); seedConfig(db, 'iptables', 'firewall_ip', FIREWALL); // caddy has its own systems and no config naming the firewall. const result = remoteAccessPolicy('caddy', db).checkTarget({ ipv4_address: FIREWALL }, false); expect(result.allowed).toBe(false); }); }); });