fix(control-panel): sign strict goal approvals via oidc

This commit is contained in:
thanhnv
2026-07-18 13:59:15 +07:00
parent c0a6dbc25f
commit 03ddeb7123
5 changed files with 109 additions and 21 deletions
@@ -10,7 +10,7 @@ const projectAdmin = { actor: 'alice', role: 'project-admin', project: 'default'
const approver = { actor: 'bob', role: 'approver', project: 'default', tenant: 'default' };
const orgAdmin = { actor: 'root', role: 'org-admin', project: 'default', tenant: 'default' };
function withTempGovernance(fn: (paths: { inbox: string; store: string }) => void) {
async function withTempGovernance(fn: (paths: { inbox: string; store: string }) => Promise<void> | void) {
const prevInbox = process.env.CASAN_APPROVAL_INBOX_FILE;
const prevStrict = process.env.CASAN_APPROVAL_STRICT;
const prevStore = process.env.CASAN_CP_STORE_FILE;
@@ -24,7 +24,7 @@ function withTempGovernance(fn: (paths: { inbox: string; store: string }) => voi
process.env.CASAN_CP_KEY_DIR = join(work, 'keys');
process.env.CASAN_CP_PUB = join(work, 'cp.pub');
try {
fn({ inbox: process.env.CASAN_APPROVAL_INBOX_FILE, store: process.env.CASAN_CP_STORE_FILE });
await fn({ inbox: process.env.CASAN_APPROVAL_INBOX_FILE, store: process.env.CASAN_CP_STORE_FILE });
} finally {
if (prevInbox === undefined) delete process.env.CASAN_APPROVAL_INBOX_FILE;
else process.env.CASAN_APPROVAL_INBOX_FILE = prevInbox;
@@ -41,8 +41,8 @@ function withTempGovernance(fn: (paths: { inbox: string; store: string }) => voi
}
}
test('approval inbox submit -> approve applies governed setting and writes oversight', () => {
withTempGovernance(({ inbox, store }) => {
test('approval inbox submit -> approve applies governed setting and writes oversight', async () => {
await withTempGovernance(async ({ inbox, store }) => {
const svc = new ApprovalsService();
const submitted = svc.submit({
action: 'settings.write',
@@ -55,7 +55,7 @@ test('approval inbox submit -> approve applies governed setting and writes overs
assert.equal(submitted.proposal.status, 'pending');
assert.equal(submitted.proposal.delegation.requires_approval, true);
const decided = svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'approved by reviewer' }, approver) as any;
const decided = await svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'approved by reviewer' }, approver) as any;
assert.equal(decided.proposal.status, 'approved');
assert.equal(decided.applied.value, true);
@@ -69,8 +69,8 @@ test('approval inbox submit -> approve applies governed setting and writes overs
});
});
test('approval inbox denies forged JWT in strict mode without deciding proposal', () => {
withTempGovernance(({ inbox }) => {
test('approval inbox denies forged JWT in strict mode without deciding proposal', async () => {
await withTempGovernance(async ({ inbox }) => {
const svc = new ApprovalsService();
const submitted = svc.submit({
action: 'settings.write',
@@ -81,8 +81,8 @@ test('approval inbox denies forged JWT in strict mode without deciding proposal'
payload: { key: 'security.strict', value: true },
}, projectAdmin) as any;
process.env.CASAN_APPROVAL_STRICT = '1';
assert.throws(
() => svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'forged jwt', approvalJwt: 'fake.jwt.token' }, approver),
await assert.rejects(
svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'forged jwt', approvalJwt: 'fake.jwt.token' }, approver),
ForbiddenException,
);
const inboxRaw = JSON.parse(readFileSync(inbox, 'utf8'));
@@ -91,8 +91,8 @@ test('approval inbox denies forged JWT in strict mode without deciding proposal'
});
});
test('approval inbox enforces separation of duties', () => {
withTempGovernance(() => {
test('approval inbox enforces separation of duties', async () => {
await withTempGovernance(async () => {
const svc = new ApprovalsService();
const submitted = svc.submit({
action: 'settings.write',
@@ -101,8 +101,8 @@ test('approval inbox enforces separation of duties', () => {
reason: 'change by root',
payload: { key: 'compression.enabled', value: true },
}, orgAdmin) as any;
assert.throws(
() => svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'self approve' }, orgAdmin),
await assert.rejects(
svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'self approve' }, orgAdmin),
ForbiddenException,
);
});