/** * Tests for src/network/acl-compiler.js — DC-148. * * Golden-policy cases, lockout adversarial cases (each hard rule), drift * detection both ways, baseline-section passthrough. 100% pure — no I/O, * no jest mocks needed. */ 'use strict'; /* eslint-disable require-await, no-unused-vars */ // require-await: helper stubs are async to match real signatures without awaiting. // no-unused-vars: fixture records exist to document the people shape. const { compilePolicy, assertNoLockout, detectDrift, compileAndValidate, hashAcls, LockoutError, } = require('../src/network/acl-compiler'); // ── fixtures ────────────────────────────────────────────────────────────── const ADMIN = { id: 'p-admin', providerUserId: '1001', groups: ['Admin'], status: 'active' }; const WIFE = { id: 'p-wife', providerUserId: '1002', groups: ['Family'], status: 'active' }; const KID = { id: 'p-kid', providerUserId: '1003', groups: ['Family'], status: 'active' }; const BLOCKED = { id: 'p-bad', providerUserId: '1004', groups: ['Family'], status: 'blocked' }; const REMOVED = { id: 'p-gone', providerUserId: '1005', groups: ['Admin'], status: 'removed' }; const RESOURCES = [ { name: 'dashcaddy', host: 'dashcaddy.internal', ports: ['80', '443', '8080'] }, { name: 'plex', host: 'plex.internal', ports: ['32400'] }, { name: 'nas', host: 'nas.internal' }, // ports default → ['*'] ]; const HOST_DEVICE = 'dev-host-123'; function grants(...list) { return { grants: list.map(([type, id, resource, allowed]) => ({ principal: { type, id }, resource, allowed })) }; } const BASELINE = { ssh: [{ action: 'check', src: ['autogroup:member'], dst: ['autogroup:self'], users: ['autogroup:nonroot'] }], tagOwners: { 'tag:dc-host': ['autogroup:admin'] }, }; // ── golden-policy: compilePolicy ────────────────────────────────────────── describe('acl-compiler: golden policies', () => { test('single admin with all-resource grants → per-user src entries + host egress', () => { const map = grants( ['person', 'p-admin', 'dashcaddy', true], ['person', 'p-admin', 'plex', true], ['person', 'p-admin', 'nas', true], ); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(policy.acls).toEqual([ { action: 'accept', src: ['1001'], dst: ['dashcaddy.internal:80', 'dashcaddy.internal:443', 'dashcaddy.internal:8080'], proto: ['tcp', 'udp'] }, { action: 'accept', src: ['1001'], dst: ['plex.internal:32400'], proto: ['tcp', 'udp'] }, { action: 'accept', src: ['1001'], dst: ['nas.internal:*'], proto: ['tcp', 'udp'] }, { action: 'accept', src: [HOST_DEVICE], dst: ['*:*'], proto: ['tcp', 'udp'] }, ]); }); test('family group grant expands to each active member (no Tailscale groups)', () => { const map = grants(['group', 'Family', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN, WIFE, KID, BLOCKED], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); const srcs = policy.acls.map(e => e.src[0]); expect(srcs).toContain('1002'); expect(srcs).toContain('1003'); expect(srcs).not.toContain('1004'); // blocked compiles to nothing expect(srcs).not.toContain('1005'); // removed compiles to nothing // No group object is ever emitted in dst (free-plan rule) for (const e of policy.acls) { for (const d of e.dst) expect(d.startsWith('group:')).toBe(false); } }); test('blocked person is absent even with an explicit personal allow grant', () => { const map = grants( ['person', 'p-bad', 'plex', true], ['group', 'Family', 'plex', true], ); const policy = compilePolicy({ accessMap: map, people: [BLOCKED], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(policy.acls).toEqual([ { action: 'accept', src: [HOST_DEVICE], dst: ['*:*'], proto: ['tcp', 'udp'] }, ]); }); test('person-level override beats group grant (deny and allow)', () => { const map = grants( ['group', 'Family', 'plex', true], ['person', 'p-kid', 'plex', false], // kid explicitly denied despite Family ['group', 'Family', 'nas', false], ['person', 'p-wife', 'nas', true], // wife explicitly allowed despite Family deny ); const policy = compilePolicy({ accessMap: map, people: [WIFE, KID], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); const wife = policy.acls.filter(e => e.src.includes('1002')); const kid = policy.acls.filter(e => e.src.includes('1003')); expect(wife.map(e => e.dst[0]).sort()).toEqual(['nas.internal:*', 'plex.internal:32400']); // nas via override + plex via group expect(kid).toEqual([]); // plex denied by override, nas denied by group }); test('default-deny: ungranted resources and people compile to nothing', () => { const map = grants(['group', 'Family', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); // only the host egress rule — no catch-all accept, nothing for ADMIN expect(policy.acls).toEqual([ { action: 'accept', src: [HOST_DEVICE], dst: ['*:*'], proto: ['tcp', 'udp'] }, ]); const flat = JSON.stringify(policy.acls); expect(flat.includes('*:*')).toBe(true); // host device egress only expect(policy.acls.filter(e => e.src.includes('1001'))).toEqual([]); }); test('grant for an unknown resource compiles to nothing', () => { const map = grants(['person', 'p-admin', 'ghost-app', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(policy.acls).toHaveLength(1); // host egress only }); test('person without providerUserId compiles to nothing', () => { const nobody = { ...ADMIN, id: 'p-noid', providerUserId: '' }; const map = grants(['person', 'p-noid', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [nobody], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(policy.acls).toHaveLength(1); // host egress only }); test('baseline ssh + tagOwners pass through untouched; acls replaced', () => { const map = grants(['person', 'p-admin', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE, baseline: BASELINE }); expect(policy.ssh).toEqual(BASELINE.ssh); expect(policy.tagOwners).toEqual(BASELINE.tagOwners); expect(Object.keys(policy).sort()).toEqual(['acls', 'ssh', 'tagOwners']); }); test('baseline acls section is dropped (fully DC-managed)', () => { const withOldAcls = { ...BASELINE, acls: [{ action: 'accept', src: ['stale'], dst: ['x:*'] }] }; const map = grants(['person', 'p-admin', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE, baseline: withOldAcls }); expect(policy.acls.some(e => e.src.includes('stale'))).toBe(false); }); test('input validation throws TypeError', () => { expect(() => compilePolicy()).toThrow(TypeError); expect(() => compilePolicy({ accessMap: {} })).toThrow(TypeError); expect(() => compilePolicy({ accessMap: { grants: [] }, people: [] })).toThrow(TypeError); expect(() => compilePolicy({ accessMap: { grants: [] }, people: [], resources: [] })).toThrow(TypeError); }); }); // ── lockout adversarial ─────────────────────────────────────────────────── describe('acl-compiler: lockout guard (assertNoLockout)', () => { const hostResource = RESOURCES[0]; // dashcaddy test('last-admin-denied throws LockoutError', () => { // admin granted plex only — dashcaddy host resource NOT granted const map = grants(['person', 'p-admin', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(() => assertNoLockout(policy, { adminUserIds: ['1001'], hostResource })).toThrow(LockoutError); expect(() => assertNoLockout(policy, { adminUserIds: ['1001'], hostResource })) .toThrow(/every active admin/); }); test('host-egress-missing throws LockoutError', () => { const map = grants(['person', 'p-admin', 'dashcaddy', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); // simulate a policy that lost the egress rule policy.acls = policy.acls.filter(e => !e.dst.includes('*:*')); expect(() => assertNoLockout(policy, { adminUserIds: ['1001'], hostResource, hostDeviceId: HOST_DEVICE })) .toThrow(/egress/); }); test('two admins, one denied, one allowed → passes (not a lockout)', () => { const admin2 = { id: 'p-admin2', providerUserId: '1006', groups: ['Admin'], status: 'active' }; const map = grants( ['person', 'p-admin', 'plex', true], // admin1: NO dashcaddy ['person', 'p-admin2', 'dashcaddy', true], // admin2: has dashcaddy ); const policy = compilePolicy({ accessMap: map, people: [ADMIN, admin2], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(() => assertNoLockout(policy, { adminUserIds: ['1001', '1006'], hostResource })).not.toThrow(); }); test('empty adminUserIds throws (no admin at all is a lockout)', () => { const map = grants(['person', 'p-wife', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [WIFE], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(() => assertNoLockout(policy, { adminUserIds: [], hostResource })).toThrow(LockoutError); expect(() => assertNoLockout(policy, { adminUserIds: [], hostResource })).toThrow(/no active admin/); }); test('admin allowed via group grant counts (Family granted dashcaddy)', () => { const adminViaGroup = { ...ADMIN, groups: ['Family'] }; const map = grants(['group', 'Family', 'dashcaddy', true]); const policy = compilePolicy({ accessMap: map, people: [adminViaGroup], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); expect(() => assertNoLockout(policy, { adminUserIds: ['1001'], hostResource })).not.toThrow(); }); test('requires a policy object (TypeError)', () => { expect(() => assertNoLockout(null, {})).toThrow(TypeError); expect(() => assertNoLockout({}, {})).toThrow(TypeError); }); test('LockoutError shape: code, statusCode, name', () => { const map = grants(['person', 'p-admin', 'plex', true]); const policy = compilePolicy({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE }); try { assertNoLockout(policy, { adminUserIds: ['1001'], hostResource }); throw new Error('should have thrown'); } catch (err) { expect(err).toBeInstanceOf(LockoutError); expect(err.name).toBe('LockoutError'); expect(err.code).toBe('DC_ACL_LOCKOUT'); expect(err.statusCode).toBe(409); expect(err.isOperational).toBe(true); } }); }); // ── drift detection ─────────────────────────────────────────────────────── describe('acl-compiler: drift detection (detectDrift)', () => { const map = grants(['person', 'p-admin', 'dashcaddy', true], ['person', 'p-admin', 'plex', true]); const { policy, hash } = compileAndValidate(_fullInput()); function _fullInput() { return { accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE, adminUserIds: ['1001'], hostResource: RESOURCES[0] }; } test('no drift: identical remote acls → drifted:false', () => { const remote = { acls: JSON.parse(JSON.stringify(policy.acls)) }; const res = detectDrift(hash, remote); expect(res.drifted).toBe(false); expect(res.remoteHash).toBe(hash); }); test('drift: outside edit added a rule → drifted:true', () => { const remote = { acls: [...policy.acls, { action: 'accept', src: ['outsider'], dst: ['plex.internal:32400'] }] }; expect(detectDrift(hash, remote).drifted).toBe(true); }); test('drift: rule removed remotely → drifted:true', () => { const remote = { acls: policy.acls.slice(0, -1) }; expect(detectDrift(hash, remote).drifted).toBe(true); }); test('baseline sections changing is NOT drift (only acls hashed)', () => { const remote = { acls: JSON.parse(JSON.stringify(policy.acls)), ssh: [{ action: 'accept', src: ['x'], dst: ['y'] }] }; expect(detectDrift(hash, remote).drifted).toBe(false); }); test('hash is order-stable (sorted keys) and content-sensitive', () => { expect(hashAcls(policy.acls)).toBe(hash); expect(hashAcls([])).not.toBe(hash); // key order inside entries does not matter const shuffled = policy.acls.map(e => ({ proto: e.proto, dst: e.dst, src: e.src, action: e.action })); expect(hashAcls(shuffled)).toBe(hash); }); }); // ── compileAndValidate ──────────────────────────────────────────────────── describe('acl-compiler: compileAndValidate', () => { test('happy path returns { policy, hash } and passes the guard', () => { const map = grants(['person', 'p-admin', 'dashcaddy', true]); const out = compileAndValidate({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE, adminUserIds: ['1001'], hostResource: RESOURCES[0], }); expect(out.policy.acls.some(e => e.src.includes('1001'))).toBe(true); expect(out.hash).toMatch(/^sha256:[0-9a-f]{64}$/); }); test('throws LockoutError (no TypeError leak) when the guard trips', () => { const map = grants(['person', 'p-admin', 'plex', true]); expect(() => compileAndValidate({ accessMap: map, people: [ADMIN], resources: RESOURCES, hostDeviceId: HOST_DEVICE, adminUserIds: ['1001'], hostResource: RESOURCES[0], })).toThrow(LockoutError); }); });