master-zapier-plan-draft/packages/verae-access-authz/src/policy.js
George Lambert 345aeeead9
Some checks are pending
offline / test (push) Waiting to run
Bind each customer to a Verae userId for hop tracing
Signup registers/binds a Verae central user and stores veraeUserId. Public access stays the zappier API key. Chain JWTs stay server-side behind tokenRef. Authz, billing, and jobs.watch carry veraeUserId.
2026-09-11 16:18:06 -04:00

100 lines
3.2 KiB
JavaScript

import { PLANES, parseAddress } from './subjects.js';
/**
* Default-deny. Each access plane may only touch listed internal prefixes.
* Internal workers (poller, WORM, account-balance) are not planes; they
* already sit on the private bus. This policy gates *ingress*.
*/
export const POLICY = Object.freeze({
zapier: {
title: 'Zapier Platform (HTTPS only; never a NATS client)',
allow: [
'verae.zapier.jobs.watch',
'verae.zapier.jobs.events',
'verae.zapier.webhooks.deliver',
'verae.billing.usage.recorded',
],
},
web: {
title: 'Direct customer web (portal browser)',
allow: [
'verae.billing.statement.get',
'verae.billing.balance.adjust',
'verae.billing.payment.recorded',
],
adjustKinds: ['payment', 'reload'],
},
api: {
title: 'Direct customer API (x-api-key, not Zapier)',
allow: ['verae.billing.statement.get', 'verae.billing.usage.recorded', 'verae.zapier.jobs.watch'],
},
leaf: {
title: 'Server-to-server NATS leaf / mTLS',
allow: [
'verae.archive.put',
'verae.archive.query',
'verae.archive.reply.',
'verae.zapier.jobs.watch',
'verae.zapier.jobs.events',
'verae.zapier.webhooks.deliver',
],
},
staff: {
title: 'CS / sales / admin / accounting web',
allow: [
'verae.billing.statement.get',
'verae.billing.balance.get',
'verae.billing.balance.adjust',
'verae.billing.credit.applied',
'verae.billing.payment.recorded',
],
},
});
function prefixAllowed(allow, subject) {
return allow.some((p) => (p.endsWith('.') ? subject.startsWith(p) : subject === p || subject.startsWith(`${p}.`)));
}
/**
* @param {{ plane: string, subject: string, kind?: string, principal?: string }} req
*/
export function authorize(req) {
const plane = String(req?.plane || '');
const parsed = parseAddress(req?.subject, plane);
if (parsed.authz) {
return deny(plane, req?.subject, 'authz subjects are not forwardable');
}
if (!PLANES.includes(plane)) {
return deny(plane, parsed.internal, `unknown access plane`);
}
if (parsed.plane && parsed.plane !== plane) {
return deny(plane, parsed.internal, `plane mismatch (address is ${parsed.plane})`);
}
const internal = parsed.internal;
if (!internal.startsWith('verae.') || internal.startsWith('verae.access.')) {
return deny(plane, internal, 'not an internal verae.* subject');
}
const rule = POLICY[plane];
if (!prefixAllowed(rule.allow, internal)) {
return deny(plane, internal, `${plane} cannot reach ${internal}`);
}
if (internal === 'verae.billing.balance.adjust' && rule.adjustKinds) {
const kind = req?.kind || req?.payload?.kind;
if (kind && !rule.adjustKinds.includes(kind)) {
return deny(plane, internal, `${plane} cannot adjust kind=${kind}`);
}
}
return {
allow: true,
plane,
subject: internal,
principal: req?.principal || null,
veraeUserId: req?.veraeUserId || req?.payload?.veraeUserId || null,
traceId: req?.traceId || req?.payload?.traceId || null,
reason: 'ok',
};
}
function deny(plane, subject, reason) {
return { allow: false, plane: plane || null, subject: subject || null, reason };
}