Some checks are pending
offline / test (push) Waiting to run
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.
100 lines
3.2 KiB
JavaScript
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 };
|
|
}
|