From 2bb3884a1bddbee570d5a3318cffaaa538095a98 Mon Sep 17 00:00:00 2001 From: George Lambert Date: Fri, 11 Sep 2026 19:08:20 -0400 Subject: [PATCH] Close remaining IAM JSON doors and re-walk the lab. CS/sales/accounting/staff list endpoints and sales pricing writes require IAM; fleet service env sets STAFF_AUTH=1. Department tests use a private books file so lab NATS does not leak into them. --- packages/verae-access-staff/src/server.js | 1 + packages/verae-access-web/test/health.test.js | 9 +++++++-- packages/verae-fleet/services/access-staff.json | 3 ++- packages/verae-fleet/services/accounting-export.json | 3 ++- packages/verae-fleet/services/customer-service.json | 3 ++- packages/verae-fleet/services/sales-pricing.json | 3 ++- packages/verae-fleet/services/zappier-edge.json | 1 + packages/verae-staff-iam/README.md | 2 +- packages/zapier-decisions/LOG.md | 6 ++++++ packages/zapier-decisions/TODO.md | 2 ++ packages/zappier-accounting-export/src/server.js | 1 + packages/zappier-customer-service/src/server.js | 1 + packages/zappier-customer-service/test/health.test.js | 8 +++++++- packages/zappier-sales-pricing/src/server.js | 2 ++ 14 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/verae-access-staff/src/server.js b/packages/verae-access-staff/src/server.js index 7a0b75e..09b8a1e 100644 --- a/packages/verae-access-staff/src/server.js +++ b/packages/verae-access-staff/src/server.js @@ -51,6 +51,7 @@ const server = http.createServer(async (req, res) => { return json(200, { ok: true, role: 'verae-access-staff', plane: PLANE }); } if (req.method === 'GET' && url.pathname === '/customers') { + if (!(await denyOrRedirect(req, res, json, { permission: 'staff.plane' }))) return; return json(200, { customers: await listCustomers(EDGE, KEY) }); } const review = url.pathname.match(/^\/review\/([^/]+)$/); diff --git a/packages/verae-access-web/test/health.test.js b/packages/verae-access-web/test/health.test.js index 0bd85cb..f0c8afb 100644 --- a/packages/verae-access-web/test/health.test.js +++ b/packages/verae-access-web/test/health.test.js @@ -1,6 +1,8 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -12,14 +14,15 @@ test('web plane can read statement after authz, cannot skip authz', async () => const authzPort = 18021; const booksPort = 18022; const webPort = 18023; + const booksFile = path.join(os.tmpdir(), `web-books-${process.pid}.json`); const authz = spawn(process.execPath, ['src/server.js'], { cwd: authzRoot, - env: { ...process.env, PORT: String(authzPort) }, + env: { ...process.env, PORT: String(authzPort), NATS_URL: '' }, stdio: ['ignore', 'pipe', 'pipe'], }); const books = spawn(process.execPath, ['src/server.js'], { cwd: booksRoot, - env: { ...process.env, PORT: String(booksPort) }, + env: { ...process.env, PORT: String(booksPort), BOOKS_PATH: booksFile, NATS_URL: '' }, stdio: ['ignore', 'pipe', 'pipe'], }); const web = spawn(process.execPath, ['src/server.js'], { @@ -29,6 +32,7 @@ test('web plane can read statement after authz, cannot skip authz', async () => PORT: String(webPort), AUTHZ_URL: `http://127.0.0.1:${authzPort}`, ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`, + NATS_URL: '', }, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -51,5 +55,6 @@ test('web plane can read statement after authz, cannot skip authz', async () => web.kill('SIGTERM'); books.kill('SIGTERM'); authz.kill('SIGTERM'); + fs.rmSync(booksFile, { force: true }); } }); diff --git a/packages/verae-fleet/services/access-staff.json b/packages/verae-fleet/services/access-staff.json index bef385e..912dac8 100644 --- a/packages/verae-fleet/services/access-staff.json +++ b/packages/verae-fleet/services/access-staff.json @@ -17,7 +17,8 @@ "PORT": "3025", "AUTHZ_URL": "http://127.0.0.1:3020", "ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010", - "STAFF_IAM_URL": "http://127.0.0.1:3028" + "STAFF_IAM_URL": "http://127.0.0.1:3028", + "STAFF_AUTH": "1" }, "nats": { "in": [], "out": ["verae.access.authz.check"] } } diff --git a/packages/verae-fleet/services/accounting-export.json b/packages/verae-fleet/services/accounting-export.json index a0b27ce..e1bca79 100644 --- a/packages/verae-fleet/services/accounting-export.json +++ b/packages/verae-fleet/services/accounting-export.json @@ -18,7 +18,8 @@ "ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000", "ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010", "NATS_URL": "nats://127.0.0.1:4222", - "STAFF_IAM_URL": "http://127.0.0.1:3028" + "STAFF_IAM_URL": "http://127.0.0.1:3028", + "STAFF_AUTH": "1" }, "nats": { "in": [], "out": ["verae.billing.statement.get"] } } diff --git a/packages/verae-fleet/services/customer-service.json b/packages/verae-fleet/services/customer-service.json index 8470c02..cff1017 100644 --- a/packages/verae-fleet/services/customer-service.json +++ b/packages/verae-fleet/services/customer-service.json @@ -19,7 +19,8 @@ "ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010", "NATS_URL": "nats://127.0.0.1:4222", "AUTHZ_URL": "http://127.0.0.1:3020", - "STAFF_IAM_URL": "http://127.0.0.1:3028" + "STAFF_IAM_URL": "http://127.0.0.1:3028", + "STAFF_AUTH": "1" }, "nats": { "in": [], diff --git a/packages/verae-fleet/services/sales-pricing.json b/packages/verae-fleet/services/sales-pricing.json index d95744e..082498e 100644 --- a/packages/verae-fleet/services/sales-pricing.json +++ b/packages/verae-fleet/services/sales-pricing.json @@ -19,7 +19,8 @@ "ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010", "NATS_URL": "nats://127.0.0.1:4222", "AUTHZ_URL": "http://127.0.0.1:3020", - "STAFF_IAM_URL": "http://127.0.0.1:3028" + "STAFF_IAM_URL": "http://127.0.0.1:3028", + "STAFF_AUTH": "1" }, "nats": { "in": [], "out": ["verae.billing.statement.get"] } } diff --git a/packages/verae-fleet/services/zappier-edge.json b/packages/verae-fleet/services/zappier-edge.json index 3f9ed25..917cdb5 100644 --- a/packages/verae-fleet/services/zappier-edge.json +++ b/packages/verae-fleet/services/zappier-edge.json @@ -16,6 +16,7 @@ "env": { "PORT": "3000", "STAFF_IAM_URL": "http://127.0.0.1:3028", + "STAFF_AUTH": "1", "BIND": "127.0.0.1", "ZAPPIER_UPSTREAM": "http://127.0.0.1:3100", "CS_SERVICE_URL": "http://127.0.0.1:3011", diff --git a/packages/verae-staff-iam/README.md b/packages/verae-staff-iam/README.md index bf5db20..b56782f 100644 --- a/packages/verae-staff-iam/README.md +++ b/packages/verae-staff-iam/README.md @@ -16,7 +16,7 @@ Port **`:3028`**. UI: sign-in, people, roles, audit. | `accounting` | `acct-dev-key` | accounting | | `operator` | `fleet-dev-key` | operator | -Override with `IAM_OWNER_PASSWORD`, `IAM_CS_PASSWORD`, etc. Persist: `STAFF_IAM_PATH` (users, audit, **sessions**). Login is rate-limited (8 failures / 10 minutes / IP). +Override with `IAM_OWNER_PASSWORD`, `IAM_CS_PASSWORD`, etc. Persist: `STAFF_IAM_PATH` (users, audit, **sessions**). Login is rate-limited (8 failures / 10 minutes / IP+username). JSON APIs return 401/403; HTML doors 302 to `/login`. ## Wire other doors diff --git a/packages/zapier-decisions/LOG.md b/packages/zapier-decisions/LOG.md index f8fb409..e42932c 100644 --- a/packages/zapier-decisions/LOG.md +++ b/packages/zapier-decisions/LOG.md @@ -32,6 +32,12 @@ - Disable lan-134 unless `FLEET_ENABLE_LAN134=1`. - SSH spawn timeout 8s; failed hosts skipped. +## 2026-09-11 — IAM JSON doors closed + +- CS/sales/accounting/staff JSON list and sales PUT pricing now go through IAM (401 JSON, 302 HTML). +- Fleet service env sets `STAFF_AUTH=1` with `STAFF_IAM_URL`. +- Re-walk: cs credit 200 agent=`cs`; cs export 403; operator Fleet POST 200; admin `/me` `permissions:['*']`. + ## 2026-09-11 — IAM on lab, portal door, hardening - Restarted fleet with `STAFF_IAM_URL`. Walk: cs credits (agent=`cs`) and is 403 on accounting export; operator fleet POST 200, cs 403; admin `/me` permissions `*`. diff --git a/packages/zapier-decisions/TODO.md b/packages/zapier-decisions/TODO.md index a1ff100..e72f0e8 100644 --- a/packages/zapier-decisions/TODO.md +++ b/packages/zapier-decisions/TODO.md @@ -7,3 +7,5 @@ - [x] Staff IAM: named users, roles, permissions (`verae-staff-iam` :3028). - [ ] Zapier Platform `push` of a private app. - [x] Move portal static files fully into `verae-access-web` (`/portal/` public door; API proxied to loopback edge). +- [x] Turn IAM on for the running lab (fleet `STAFF_IAM_URL` + restart). +- [x] IAM hardening: JSON sessions, login rate-limit, JSON 401 as well as HTML 302, credit principal = IAM username. diff --git a/packages/zappier-accounting-export/src/server.js b/packages/zappier-accounting-export/src/server.js index aef0758..548ac82 100644 --- a/packages/zappier-accounting-export/src/server.js +++ b/packages/zappier-accounting-export/src/server.js @@ -35,6 +35,7 @@ const server = http.createServer(async (req, res) => { return json(200, { ok: true, role: 'zappier-accounting-export' }); } if (req.method === 'GET' && url.pathname === '/customers') { + if (!(await denyOrRedirect(req, res, json, { permission: 'accounting.review' }))) return; return json(200, { customers: await listCustomers(EDGE, KEY) }); } const review = url.pathname.match(/^\/review\/([^/]+)$/); diff --git a/packages/zappier-customer-service/src/server.js b/packages/zappier-customer-service/src/server.js index 8ec9e26..41465b6 100644 --- a/packages/zappier-customer-service/src/server.js +++ b/packages/zappier-customer-service/src/server.js @@ -87,6 +87,7 @@ const server = http.createServer(async (req, res) => { return json(r.status, body); } if (req.method === 'GET' && url.pathname === '/credits') { + if (!(await denyOrRedirect(req, res, json, { permission: 'cs.review' }))) return; const id = url.searchParams.get('customerId'); if (!id) return json(400, { error: 'customerId required' }); const out = await statement(id); diff --git a/packages/zappier-customer-service/test/health.test.js b/packages/zappier-customer-service/test/health.test.js index 876eb6b..dddec48 100644 --- a/packages/zappier-customer-service/test/health.test.js +++ b/packages/zappier-customer-service/test/health.test.js @@ -2,6 +2,8 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; const root = path.dirname(fileURLToPath(new URL('.', import.meta.url))); @@ -29,9 +31,10 @@ test('customer-service health', async () => { test('customer-service review via account-balance HTTP', async () => { const booksPort = 18014; const csPort = 18015; + const booksFile = path.join(os.tmpdir(), `cs-books-${process.pid}.json`); const books = spawn(process.execPath, ['src/server.js'], { cwd: path.join(root, '..', 'zappier-account-balance'), - env: { ...process.env, PORT: String(booksPort) }, + env: { ...process.env, PORT: String(booksPort), BOOKS_PATH: booksFile, NATS_URL: '' }, stdio: ['ignore', 'pipe', 'pipe'], }); const cs = spawn(process.execPath, ['src/server.js'], { @@ -40,6 +43,8 @@ test('customer-service review via account-balance HTTP', async () => { ...process.env, PORT: String(csPort), ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`, + NATS_URL: '', + STAFF_IAM_URL: '', }, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -58,5 +63,6 @@ test('customer-service review via account-balance HTTP', async () => { } finally { cs.kill('SIGTERM'); books.kill('SIGTERM'); + fs.rmSync(booksFile, { force: true }); } }); diff --git a/packages/zappier-sales-pricing/src/server.js b/packages/zappier-sales-pricing/src/server.js index ff85574..1370a42 100644 --- a/packages/zappier-sales-pricing/src/server.js +++ b/packages/zappier-sales-pricing/src/server.js @@ -47,6 +47,7 @@ const server = http.createServer(async (req, res) => { return json(200, { ok: true, role: 'zappier-sales-pricing' }); } if (req.method === 'GET' && url.pathname === '/customers') { + if (!(await denyOrRedirect(req, res, json, { permission: 'sales.review' }))) return; return json(200, { customers: await listCustomers(EDGE, KEY) }); } const review = url.pathname.match(/^\/review\/([^/]+)$/); @@ -68,6 +69,7 @@ const server = http.createServer(async (req, res) => { } const price = url.pathname.match(/^\/customers\/([^/]+)\/pricing$/); if (req.method === 'PUT' && price) { + if (!(await denyOrRedirect(req, res, json, { permission: 'sales.quote' }))) return; const chunks = []; for await (const c of req) chunks.push(c); const payload = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');