Files
stack/packages/bus/tests/principal.test.mjs

98 lines
7.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { verifyPrincipal } from '../src/principal.mjs';
const nonce='a'.repeat(64), uid=process.getuid();
const paths={human:'/tree/human-cli.mjs',tester:'/tree/tester-cli.mjs',user:'/tree/user-cli.mjs',console:'/tree/console.mjs'};
function context(kind='human') {
const variable=kind==='human'?'MOSAIC_BUS_CLI_NONCE':kind==='tester'?'MOSAIC_BUS_TESTER_NONCE':'MOSAIC_BUS_USER_NONCE';
const table=new Map([[50,{pid:50,ppid:40,startTime:'100',uid,argv:['node',paths[kind]],env:{[variable]:nonce}}],
[40,{pid:40,ppid:1,startTime:'90',uid,argv:['node',paths.console,'serve'],env:{}}]]);
return {peer:{pid:50,uid,gid:uid},paths,businesses:{demo:{human:'owner'}},launches:[],
readProcess:pid=>{if(!table.has(pid))throw Error('gone');return {...table.get(pid)};},
readTesterNonce:()=>table.get(50).env.MOSAIC_BUS_TESTER_NONCE,table};
}
const proof={pid:50,startTime:'100',nonce,business:'demo'};
test('principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body',()=>{
const c=context();
assert.deepEqual(verifyPrincipal({human:proof},c),{kind:'owner',business:'demo',id:'owner'});
for(const peer of [{pid:51,uid,gid:uid},{pid:50,uid:uid+1,gid:uid}])assert.throws(()=>verifyPrincipal({human:proof},{...c,peer}),/human-required/);
assert.throws(()=>verifyPrincipal({human:{...proof,human:'other'}},c),/human-required/);
assert.throws(()=>verifyPrincipal({human:proof,tester:proof},c),/invalid-envelope/);
});
test('user requires its own nonce and full walk plus immediate same-tree Console parent',()=>{
const request={user:{...proof,user:'owner'}};
assert.deepEqual(verifyPrincipal(request,context('user')),{kind:'user',business:'demo',id:'owner'});
for(const change of [{argv:['bash']},{argv:['node','/other/console.mjs','serve']},{argv:['node',paths.console,'other']},{env:{CODEX_THREAD_ID:'agent'}}]) {
const c=context('user');Object.assign(c.table.get(40),change);
assert.throws(()=>verifyPrincipal(request,c),/human-required/);
}
const wrong=context('user');wrong.table.get(50).env={MOSAIC_BUS_CLI_NONCE:nonce};
assert.throws(()=>verifyPrincipal(request,wrong),/human-required/);
const launched=context('user');launched.launches=[{pid:40,startTime:'90'}];
assert.throws(()=>verifyPrincipal(request,launched),/human-required/);
const changed=context('user'), read=changed.readProcess;let n=0;
changed.readProcess=pid=>{const p=read(pid);if(pid===40&&n++>0)p.startTime='91';return p;};
assert.throws(()=>verifyPrincipal(request,changed),/human-required/);
});
test('tester deliberately admits agent parents but rejects managed runs and another nonce',()=>{
const c=context('tester');c.table.get(40).env={CODEX_THREAD_ID:'seat'};
assert.deepEqual(verifyPrincipal({tester:proof},c),{kind:'tester',business:'demo',id:'tester'});
c.table.get(40).env={MOSAIC_RUN_ID:'managed'};
assert.throws(()=>verifyPrincipal({tester:proof},c),/tester-required/);
});
test('human leaf identity is reread after walking the parents',()=>{
const c=context(),read=c.readProcess;let n=0;
c.readProcess=pid=>{const p=read(pid);if(pid===50&&n++>0)p.startTime='101';return p;};
assert.throws(()=>verifyPrincipal({human:proof},c),/human-required/);
});
// Row 252 (#1753, design section 2, J3-J5): the Console owner. The same-tree console-cli wrapper with
// its own nonce, a direct same-tree `serve` parent, the subject from the wrapper's spawn environment,
// and that subject one of the configured owners (N1).
const owner={provider:'fake-oidc',iss:'https://idp.test',sub:'owner-1'};
function consoleContext() {
const c=context('user');c.paths={...paths,consoleCli:'/tree/console-cli.mjs'};c.consoleOwners=[{provider:'other',iss:'https://idp.test',sub:'owner-2'},owner];
Object.assign(c.table.get(50),{argv:['node','/tree/console-cli.mjs'],env:{MOSAIC_BUS_CONSOLE_NONCE:nonce,MOSAIC_BUS_CONSOLE_BUSINESS:'demo',MOSAIC_BUS_CONSOLE_PROVIDER:owner.provider,MOSAIC_BUS_CONSOLE_ISS:owner.iss,MOSAIC_BUS_CONSOLE_SUB:owner.sub}});
return c;
}
const consoleProof={...proof,...owner};
test('console requires its own wrapper, nonce and serve parent, and a configured owner from the spawn env',()=>{
assert.deepEqual(verifyPrincipal({console:consoleProof},consoleContext()),{kind:'console',business:'demo',id:'owner',via:'console',subject:owner});
// J3: the subject is the spawn's, never the body's alone, and must be a configured owner.
for(const change of [{sub:'owner-2'},{iss:'https://evil.test'},{provider:'other'}])
assert.throws(()=>verifyPrincipal({console:{...consoleProof,...change}},consoleContext()),/human-required/);
const unlisted=consoleContext();unlisted.consoleOwners=[];
assert.throws(()=>verifyPrincipal({console:consoleProof},unlisted),/human-required/);
for(const extra of [{name:'Jason'},{human:'owner'},{user:'owner'},{sub:''},{sub:'x'.repeat(513)}])
assert.throws(()=>verifyPrincipal({console:{...consoleProof,...extra}},consoleContext()),/human-required/);
// J4: the parent is the same-tree serve, direct, and not an agent.
for(const change of [{argv:['bash']},{argv:['node','/other/console.mjs','serve']},{argv:['node',paths.console,'other']},{env:{CODEX_THREAD_ID:'agent'}},{uid:uid+1}]) {
const c=consoleContext();Object.assign(c.table.get(40),change);
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
}
const launched=consoleContext();launched.launches=[{pid:40,startTime:'90'}];
assert.throws(()=>verifyPrincipal({console:consoleProof},launched),/human-required/);
// J5: another nonce namespace, or the wrapper path of another kind, refuses.
for(const env of [{MOSAIC_BUS_CLI_NONCE:nonce},{MOSAIC_BUS_USER_NONCE:nonce}]) {
const c=consoleContext();const t=c.table.get(50);t.env={...t.env,...env};delete t.env.MOSAIC_BUS_CONSOLE_NONCE;
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
}
for(const argv of [['node',paths.human],['node',paths.user],['node','/other/console-cli.mjs']]) {
const c=consoleContext();c.table.get(50).argv=argv;
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
}
// The Console's own nonce does not answer as the cli human or the user.
assert.throws(()=>verifyPrincipal({human:proof},consoleContext()),/human-required/);
assert.throws(()=>verifyPrincipal({user:{...proof,user:'owner'}},consoleContext()),/human-required/);
// Each piece is read again after the walk: a parent or a spawn subject that changed refuses.
for(const [pid,mutate] of [[40,p=>{p.startTime='91';}],[50,p=>{p.env={...p.env,MOSAIC_BUS_CONSOLE_SUB:'owner-2'};}],[50,p=>{p.ppid=41;}]]) {
const c=consoleContext(),read=c.readProcess;let n=0;
// The last read of each pid is verifyConsole's own reread: 40 is read 3 times, 50 is read 4 times.
c.readProcess=x=>{const p=read(x);if(x===pid&&n++===(pid===40?2:3))mutate(p);return p;};
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
}
for(const peer of [{pid:51,uid,gid:uid},{pid:50,uid:uid+1,gid:uid}])assert.throws(()=>verifyPrincipal({console:consoleProof},{...consoleContext(),peer}),/human-required/);
assert.throws(()=>verifyPrincipal({console:consoleProof,human:proof},consoleContext()),/invalid-envelope/);
});