Initial import of verae-access-authz from zapier monorepo
This commit is contained in:
commit
2d4194d54c
10 changed files with 476 additions and 0 deletions
11
NATS.md
Normal file
11
NATS.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# NATS — verae-access-authz
|
||||||
|
|
||||||
|
| Direction | Address | Kind |
|
||||||
|
|-----------|---------|------|
|
||||||
|
| IN | `verae.access.authz.check` | request-reply, queue `access-authz` |
|
||||||
|
| OUT | `verae.access.authz.deny` | pub on deny (audit) |
|
||||||
|
|
||||||
|
Body in: `{ plane, subject, principal, kind, payload }`.
|
||||||
|
Body out: `{ allow, plane, subject, reason }`.
|
||||||
|
|
||||||
|
Fail closed: if this process is down, access gateways must not forward.
|
||||||
34
README.md
Normal file
34
README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# verae-access-authz
|
||||||
|
|
||||||
|
Authorization step on the NATS **address path**. Zapier is only **one** access plane. Direct web, customer API, and server-to-server leaf nodes are separate, and each is default-deny against internal subjects.
|
||||||
|
|
||||||
|
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-authz
|
||||||
|
**Catalog:** https://zapier.georgelambert.org/packages/verae-access-authz/README.pdf
|
||||||
|
|
||||||
|
## Address path
|
||||||
|
|
||||||
|
```text
|
||||||
|
client
|
||||||
|
→ verae.access.<plane>.<area>.<resource>.<action> (ingress, that plane only)
|
||||||
|
→ verae.access.authz.check (allow / deny)
|
||||||
|
→ verae.<area>.<resource>.<action> (internal bus)
|
||||||
|
```
|
||||||
|
|
||||||
|
Internal subjects (`verae.billing.*`, `verae.archive.*`, `verae.zapier.jobs.*`) stay stable. Planes never publish them until `authz.check` returns `allow`.
|
||||||
|
|
||||||
|
| Plane | Who | May reach |
|
||||||
|
|-------|-----|-----------|
|
||||||
|
| `zapier` | Zapier Platform HTTPS | jobs.*, webhooks.deliver, billing.usage.recorded |
|
||||||
|
| `web` | Customer browser portal | statement.get, balance.adjust kind=reload\|payment |
|
||||||
|
| `api` | Customer `x-api-key` (not Zapier) | statement.get, usage.recorded, jobs.watch |
|
||||||
|
| `leaf` | S2S NATS leaf / mTLS | archive.*, jobs.*, webhooks — **not billing** |
|
||||||
|
| `staff` | CS / sales / admin | statement, balance.adjust (credits) |
|
||||||
|
|
||||||
|
A leaf node cannot credit an account. A Zapier hop cannot read a customer statement. A browser cannot `archive.put`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NATS_URL=nats://127.0.0.1:4222 PORT=3020 npm start
|
||||||
|
curl -s http://127.0.0.1:3020/policy
|
||||||
|
curl -s -X POST http://127.0.0.1:3020/check -H 'content-type: application/json' \
|
||||||
|
-d '{"plane":"leaf","subject":"verae.billing.balance.adjust"}'
|
||||||
|
```
|
||||||
46
package-lock.json
generated
Normal file
46
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "verae-access-authz",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "verae-access-authz",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"nats": "^2.28.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nats": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/nats/-/nats-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==",
|
||||||
|
"deprecated": "Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"nkeys.js": "1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nkeys.js": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tweetnacl": "1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
package.json
Normal file
14
package.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "verae-access-authz",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "NATS authorization step for access planes (web, api, zapier, leaf, staff)",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.js",
|
||||||
|
"test": "node --test test/*.test.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nats": "^2.28.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/client.js
Normal file
38
src/client.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/** Call verae.access.authz.check then the internal subject. Fail closed. */
|
||||||
|
import { SUBJECTS } from './subjects.js';
|
||||||
|
|
||||||
|
export async function authorizeThenRequest(nc, sc, { plane, subject, principal, payload, timeout = 2000 }) {
|
||||||
|
const kind = payload?.kind;
|
||||||
|
const checkMsg = await nc.request(
|
||||||
|
SUBJECTS.AUTHZ_CHECK,
|
||||||
|
sc.encode(JSON.stringify({ plane, subject, principal, kind, payload })),
|
||||||
|
{ timeout },
|
||||||
|
);
|
||||||
|
const decision = JSON.parse(sc.decode(checkMsg.data) || '{}');
|
||||||
|
if (!decision.allow) {
|
||||||
|
const err = new Error(decision.reason || 'denied');
|
||||||
|
err.decision = decision;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const internal = decision.subject;
|
||||||
|
const body = { ...payload, plane, principal, traceId: payload?.traceId };
|
||||||
|
const m = await nc.request(internal, sc.encode(JSON.stringify(body)), { timeout });
|
||||||
|
return { decision, body: JSON.parse(sc.decode(m.data) || '{}') };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authorizeThenPublish(nc, sc, { plane, subject, principal, payload, timeout = 2000 }) {
|
||||||
|
const kind = payload?.kind;
|
||||||
|
const checkMsg = await nc.request(
|
||||||
|
SUBJECTS.AUTHZ_CHECK,
|
||||||
|
sc.encode(JSON.stringify({ plane, subject, principal, kind, payload })),
|
||||||
|
{ timeout },
|
||||||
|
);
|
||||||
|
const decision = JSON.parse(sc.decode(checkMsg.data) || '{}');
|
||||||
|
if (!decision.allow) {
|
||||||
|
const err = new Error(decision.reason || 'denied');
|
||||||
|
err.decision = decision;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
nc.publish(decision.subject, sc.encode(JSON.stringify({ ...payload, plane, principal })));
|
||||||
|
return { decision };
|
||||||
|
}
|
||||||
100
src/policy.js
Normal file
100
src/policy.js
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
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 };
|
||||||
|
}
|
||||||
83
src/server.js
Normal file
83
src/server.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import http from 'node:http';
|
||||||
|
import { authorize, POLICY } from './policy.js';
|
||||||
|
import { PLANES, SUBJECTS } from './subjects.js';
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PORT || process.env.FLEET_HEALTH_PORT || 3020);
|
||||||
|
const BIND = process.env.FLEET_HEALTH_BIND || '0.0.0.0';
|
||||||
|
let natsOk = false;
|
||||||
|
|
||||||
|
async function startNats() {
|
||||||
|
const url = process.env.NATS_URL;
|
||||||
|
if (!url) return;
|
||||||
|
const { connect, StringCodec } = await import('nats');
|
||||||
|
const nc = await connect({ servers: url.split(','), name: 'verae-access-authz' });
|
||||||
|
const sc = StringCodec();
|
||||||
|
const sub = nc.subscribe(SUBJECTS.AUTHZ_CHECK, { queue: SUBJECTS.QUEUE });
|
||||||
|
(async () => {
|
||||||
|
for await (const m of sub) {
|
||||||
|
let payload = {};
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(sc.decode(m.data) || '{}');
|
||||||
|
} catch {
|
||||||
|
payload = {};
|
||||||
|
}
|
||||||
|
const out = authorize(payload);
|
||||||
|
if (!out.allow && m.reply) {
|
||||||
|
nc.publish(SUBJECTS.AUTHZ_DENY, sc.encode(JSON.stringify({ ...out, at: new Date().toISOString() })));
|
||||||
|
}
|
||||||
|
if (m.reply) m.respond(sc.encode(JSON.stringify(out)));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
natsOk = true;
|
||||||
|
process.stdout.write(`access-authz nats ${url}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBody(req) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const chunks = [];
|
||||||
|
req.on('data', (c) => chunks.push(c));
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
|
||||||
|
} catch {
|
||||||
|
resolve({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`);
|
||||||
|
const json = (code, obj) => {
|
||||||
|
res.writeHead(code, { 'content-type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(obj));
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (req.method === 'GET' && url.pathname === '/health') {
|
||||||
|
return json(200, {
|
||||||
|
ok: true,
|
||||||
|
role: 'verae-access-authz',
|
||||||
|
nats: natsOk,
|
||||||
|
planes: PLANES,
|
||||||
|
subject: SUBJECTS.AUTHZ_CHECK,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (req.method === 'GET' && url.pathname === '/policy') {
|
||||||
|
return json(200, { planes: PLANES, policy: POLICY, subjects: SUBJECTS });
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && url.pathname === '/check') {
|
||||||
|
const body = await readBody(req);
|
||||||
|
const out = authorize(body);
|
||||||
|
return json(out.allow ? 200 : 403, out);
|
||||||
|
}
|
||||||
|
json(404, { error: 'not found' });
|
||||||
|
} catch (err) {
|
||||||
|
json(500, { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, BIND, () => {
|
||||||
|
process.stdout.write(`verae-access-authz http://${BIND}:${PORT}/\n`);
|
||||||
|
});
|
||||||
|
startNats().catch((err) => process.stderr.write(`nats optional: ${err.message}\n`));
|
||||||
36
src/subjects.js
Normal file
36
src/subjects.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Access planes sit in front of internal verae.<area>.* addresses.
|
||||||
|
* Pattern:
|
||||||
|
* verae.access.<plane>.<area>.<resource>.<action> ingress (that plane only)
|
||||||
|
* verae.access.authz.check authorization step
|
||||||
|
* verae.<area>.<resource>.<action> internal (unchanged)
|
||||||
|
*/
|
||||||
|
export const PLANES = Object.freeze(['zapier', 'web', 'api', 'leaf', 'staff']);
|
||||||
|
|
||||||
|
export const SUBJECTS = Object.freeze({
|
||||||
|
AUTHZ_CHECK: 'verae.access.authz.check',
|
||||||
|
AUTHZ_DENY: 'verae.access.authz.deny',
|
||||||
|
QUEUE: 'access-authz',
|
||||||
|
ingress: (plane) => `verae.access.${plane}.in`,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** verae.billing.statement.get + plane web → verae.access.web.billing.statement.get */
|
||||||
|
export function accessAddress(plane, internalSubject) {
|
||||||
|
if (!PLANES.includes(plane)) throw new Error(`unknown plane ${plane}`);
|
||||||
|
if (!internalSubject.startsWith('verae.') || internalSubject.startsWith('verae.access.')) {
|
||||||
|
throw new Error(`not an internal verae.* subject: ${internalSubject}`);
|
||||||
|
}
|
||||||
|
return `verae.access.${plane}.${internalSubject.slice('verae.'.length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse an access or internal subject into { plane, internal }. */
|
||||||
|
export function parseAddress(subject, planeHint) {
|
||||||
|
const s = String(subject || '');
|
||||||
|
const m = s.match(/^verae\.access\.([a-z]+)\.(.+)$/);
|
||||||
|
if (m) {
|
||||||
|
const plane = m[1];
|
||||||
|
if (plane === 'authz') return { plane: planeHint || null, internal: s, authz: true };
|
||||||
|
return { plane, internal: `verae.${m[2]}`, authz: false };
|
||||||
|
}
|
||||||
|
return { plane: planeHint || null, internal: s, authz: false };
|
||||||
|
}
|
||||||
35
test/health.test.js
Normal file
35
test/health.test.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
|
test('authz health policy and check', async () => {
|
||||||
|
const port = 18020;
|
||||||
|
const child = spawn(process.execPath, ['src/server.js'], {
|
||||||
|
cwd: root,
|
||||||
|
env: { ...process.env, PORT: String(port) },
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
await new Promise((r) => setTimeout(r, 400));
|
||||||
|
try {
|
||||||
|
const h = await (await fetch(`http://127.0.0.1:${port}/health`)).json();
|
||||||
|
assert.equal(h.role, 'verae-access-authz');
|
||||||
|
const ok = await fetch(`http://127.0.0.1:${port}/check`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ plane: 'web', subject: 'verae.billing.statement.get' }),
|
||||||
|
});
|
||||||
|
assert.equal(ok.status, 200);
|
||||||
|
const no = await fetch(`http://127.0.0.1:${port}/check`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ plane: 'leaf', subject: 'verae.billing.balance.adjust' }),
|
||||||
|
});
|
||||||
|
assert.equal(no.status, 403);
|
||||||
|
} finally {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
});
|
||||||
79
test/policy.test.js
Normal file
79
test/policy.test.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { authorize } from '../src/policy.js';
|
||||||
|
import { accessAddress, parseAddress, PLANES } from '../src/subjects.js';
|
||||||
|
|
||||||
|
test('five access planes', () => {
|
||||||
|
assert.deepEqual([...PLANES], ['zapier', 'web', 'api', 'leaf', 'staff']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('web may read statement and reload, not archive or jobs', () => {
|
||||||
|
assert.equal(authorize({ plane: 'web', subject: 'verae.billing.statement.get' }).allow, true);
|
||||||
|
assert.equal(
|
||||||
|
authorize({ plane: 'web', subject: 'verae.billing.balance.adjust', kind: 'reload' }).allow,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
authorize({ plane: 'web', subject: 'verae.billing.balance.adjust', kind: 'credit' }).allow,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(authorize({ plane: 'web', subject: 'verae.archive.put' }).allow, false);
|
||||||
|
assert.equal(authorize({ plane: 'web', subject: 'verae.zapier.jobs.watch' }).allow, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leaf may archive and jobs, not billing adjust', () => {
|
||||||
|
assert.equal(authorize({ plane: 'leaf', subject: 'verae.archive.put' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'leaf', subject: 'verae.archive.reply.abc' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'leaf', subject: 'verae.zapier.jobs.watch' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'leaf', subject: 'verae.billing.balance.adjust' }).allow, false);
|
||||||
|
assert.equal(authorize({ plane: 'leaf', subject: 'verae.billing.statement.get' }).allow, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zapier may meter and jobs, not customer statement or credits', () => {
|
||||||
|
assert.equal(authorize({ plane: 'zapier', subject: 'verae.zapier.jobs.watch' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'zapier', subject: 'verae.billing.usage.recorded' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'zapier', subject: 'verae.billing.statement.get' }).allow, false);
|
||||||
|
assert.equal(authorize({ plane: 'zapier', subject: 'verae.billing.balance.adjust' }).allow, false);
|
||||||
|
assert.equal(authorize({ plane: 'zapier', subject: 'verae.archive.put' }).allow, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('api may statement and usage, not credits or archive', () => {
|
||||||
|
assert.equal(authorize({ plane: 'api', subject: 'verae.billing.statement.get' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'api', subject: 'verae.billing.usage.recorded' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'api', subject: 'verae.billing.balance.adjust' }).allow, false);
|
||||||
|
assert.equal(authorize({ plane: 'api', subject: 'verae.archive.query' }).allow, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('staff may credit and statement, not archive', () => {
|
||||||
|
assert.equal(authorize({ plane: 'staff', subject: 'verae.billing.balance.adjust', kind: 'credit' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'staff', subject: 'verae.billing.statement.get' }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'staff', subject: 'verae.archive.put' }).allow, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('access-prefixed address is mapped to internal', () => {
|
||||||
|
const addr = accessAddress('leaf', 'verae.archive.put');
|
||||||
|
assert.equal(addr, 'verae.access.leaf.archive.put');
|
||||||
|
const parsed = parseAddress(addr);
|
||||||
|
assert.equal(parsed.plane, 'leaf');
|
||||||
|
assert.equal(parsed.internal, 'verae.archive.put');
|
||||||
|
assert.equal(authorize({ plane: 'leaf', subject: addr }).allow, true);
|
||||||
|
assert.equal(authorize({ plane: 'web', subject: addr }).allow, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allow echoes veraeUserId for hop tracing', () => {
|
||||||
|
const out = authorize({
|
||||||
|
plane: 'web',
|
||||||
|
subject: 'verae.billing.statement.get',
|
||||||
|
principal: 'cust_1',
|
||||||
|
veraeUserId: 'vu_deadbeefdeadbeef',
|
||||||
|
traceId: 'tr-1',
|
||||||
|
});
|
||||||
|
assert.equal(out.allow, true);
|
||||||
|
assert.equal(out.veraeUserId, 'vu_deadbeefdeadbeef');
|
||||||
|
assert.equal(out.traceId, 'tr-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown plane and authz subjects denied', () => {
|
||||||
|
assert.equal(authorize({ plane: 'partner', subject: 'verae.archive.put' }).allow, false);
|
||||||
|
assert.equal(authorize({ plane: 'web', subject: 'verae.access.authz.check' }).allow, false);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue