Initial import of verae-access-authz from zapier monorepo
This commit is contained in:
commit
e5ee590e16
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