Close remaining IAM JSON doors and re-walk the lab.
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
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.
This commit is contained in:
parent
c32b65038a
commit
2bb3884a1b
14 changed files with 37 additions and 8 deletions
|
|
@ -51,6 +51,7 @@ const server = http.createServer(async (req, res) => {
|
||||||
return json(200, { ok: true, role: 'verae-access-staff', plane: PLANE });
|
return json(200, { ok: true, role: 'verae-access-staff', plane: PLANE });
|
||||||
}
|
}
|
||||||
if (req.method === 'GET' && url.pathname === '/customers') {
|
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) });
|
return json(200, { customers: await listCustomers(EDGE, KEY) });
|
||||||
}
|
}
|
||||||
const review = url.pathname.match(/^\/review\/([^/]+)$/);
|
const review = url.pathname.match(/^\/review\/([^/]+)$/);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
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 authzPort = 18021;
|
||||||
const booksPort = 18022;
|
const booksPort = 18022;
|
||||||
const webPort = 18023;
|
const webPort = 18023;
|
||||||
|
const booksFile = path.join(os.tmpdir(), `web-books-${process.pid}.json`);
|
||||||
const authz = spawn(process.execPath, ['src/server.js'], {
|
const authz = spawn(process.execPath, ['src/server.js'], {
|
||||||
cwd: authzRoot,
|
cwd: authzRoot,
|
||||||
env: { ...process.env, PORT: String(authzPort) },
|
env: { ...process.env, PORT: String(authzPort), NATS_URL: '' },
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
const books = spawn(process.execPath, ['src/server.js'], {
|
const books = spawn(process.execPath, ['src/server.js'], {
|
||||||
cwd: booksRoot,
|
cwd: booksRoot,
|
||||||
env: { ...process.env, PORT: String(booksPort) },
|
env: { ...process.env, PORT: String(booksPort), BOOKS_PATH: booksFile, NATS_URL: '' },
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
const web = spawn(process.execPath, ['src/server.js'], {
|
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),
|
PORT: String(webPort),
|
||||||
AUTHZ_URL: `http://127.0.0.1:${authzPort}`,
|
AUTHZ_URL: `http://127.0.0.1:${authzPort}`,
|
||||||
ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`,
|
ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`,
|
||||||
|
NATS_URL: '',
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
|
|
@ -51,5 +55,6 @@ test('web plane can read statement after authz, cannot skip authz', async () =>
|
||||||
web.kill('SIGTERM');
|
web.kill('SIGTERM');
|
||||||
books.kill('SIGTERM');
|
books.kill('SIGTERM');
|
||||||
authz.kill('SIGTERM');
|
authz.kill('SIGTERM');
|
||||||
|
fs.rmSync(booksFile, { force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
"PORT": "3025",
|
"PORT": "3025",
|
||||||
"AUTHZ_URL": "http://127.0.0.1:3020",
|
"AUTHZ_URL": "http://127.0.0.1:3020",
|
||||||
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
"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"] }
|
"nats": { "in": [], "out": ["verae.access.authz.check"] }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
"ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000",
|
"ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000",
|
||||||
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
||||||
"NATS_URL": "nats://127.0.0.1:4222",
|
"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"] }
|
"nats": { "in": [], "out": ["verae.billing.statement.get"] }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
||||||
"NATS_URL": "nats://127.0.0.1:4222",
|
"NATS_URL": "nats://127.0.0.1:4222",
|
||||||
"AUTHZ_URL": "http://127.0.0.1:3020",
|
"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": {
|
"nats": {
|
||||||
"in": [],
|
"in": [],
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
||||||
"NATS_URL": "nats://127.0.0.1:4222",
|
"NATS_URL": "nats://127.0.0.1:4222",
|
||||||
"AUTHZ_URL": "http://127.0.0.1:3020",
|
"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"] }
|
"nats": { "in": [], "out": ["verae.billing.statement.get"] }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"env": {
|
"env": {
|
||||||
"PORT": "3000",
|
"PORT": "3000",
|
||||||
"STAFF_IAM_URL": "http://127.0.0.1:3028",
|
"STAFF_IAM_URL": "http://127.0.0.1:3028",
|
||||||
|
"STAFF_AUTH": "1",
|
||||||
"BIND": "127.0.0.1",
|
"BIND": "127.0.0.1",
|
||||||
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
|
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
|
||||||
"CS_SERVICE_URL": "http://127.0.0.1:3011",
|
"CS_SERVICE_URL": "http://127.0.0.1:3011",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Port **`:3028`**. UI: sign-in, people, roles, audit.
|
||||||
| `accounting` | `acct-dev-key` | accounting |
|
| `accounting` | `acct-dev-key` | accounting |
|
||||||
| `operator` | `fleet-dev-key` | operator |
|
| `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
|
## Wire other doors
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,12 @@
|
||||||
- Disable lan-134 unless `FLEET_ENABLE_LAN134=1`.
|
- Disable lan-134 unless `FLEET_ENABLE_LAN134=1`.
|
||||||
- SSH spawn timeout 8s; failed hosts skipped.
|
- 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
|
## 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 `*`.
|
- 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 `*`.
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,5 @@
|
||||||
- [x] Staff IAM: named users, roles, permissions (`verae-staff-iam` :3028).
|
- [x] Staff IAM: named users, roles, permissions (`verae-staff-iam` :3028).
|
||||||
- [ ] Zapier Platform `push` of a private app.
|
- [ ] 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] 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.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ const server = http.createServer(async (req, res) => {
|
||||||
return json(200, { ok: true, role: 'zappier-accounting-export' });
|
return json(200, { ok: true, role: 'zappier-accounting-export' });
|
||||||
}
|
}
|
||||||
if (req.method === 'GET' && url.pathname === '/customers') {
|
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) });
|
return json(200, { customers: await listCustomers(EDGE, KEY) });
|
||||||
}
|
}
|
||||||
const review = url.pathname.match(/^\/review\/([^/]+)$/);
|
const review = url.pathname.match(/^\/review\/([^/]+)$/);
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ const server = http.createServer(async (req, res) => {
|
||||||
return json(r.status, body);
|
return json(r.status, body);
|
||||||
}
|
}
|
||||||
if (req.method === 'GET' && url.pathname === '/credits') {
|
if (req.method === 'GET' && url.pathname === '/credits') {
|
||||||
|
if (!(await denyOrRedirect(req, res, json, { permission: 'cs.review' }))) return;
|
||||||
const id = url.searchParams.get('customerId');
|
const id = url.searchParams.get('customerId');
|
||||||
if (!id) return json(400, { error: 'customerId required' });
|
if (!id) return json(400, { error: 'customerId required' });
|
||||||
const out = await statement(id);
|
const out = await statement(id);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
const root = path.dirname(fileURLToPath(new URL('.', import.meta.url)));
|
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 () => {
|
test('customer-service review via account-balance HTTP', async () => {
|
||||||
const booksPort = 18014;
|
const booksPort = 18014;
|
||||||
const csPort = 18015;
|
const csPort = 18015;
|
||||||
|
const booksFile = path.join(os.tmpdir(), `cs-books-${process.pid}.json`);
|
||||||
const books = spawn(process.execPath, ['src/server.js'], {
|
const books = spawn(process.execPath, ['src/server.js'], {
|
||||||
cwd: path.join(root, '..', 'zappier-account-balance'),
|
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'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
const cs = spawn(process.execPath, ['src/server.js'], {
|
const cs = spawn(process.execPath, ['src/server.js'], {
|
||||||
|
|
@ -40,6 +43,8 @@ test('customer-service review via account-balance HTTP', async () => {
|
||||||
...process.env,
|
...process.env,
|
||||||
PORT: String(csPort),
|
PORT: String(csPort),
|
||||||
ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`,
|
ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`,
|
||||||
|
NATS_URL: '',
|
||||||
|
STAFF_IAM_URL: '',
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
|
|
@ -58,5 +63,6 @@ test('customer-service review via account-balance HTTP', async () => {
|
||||||
} finally {
|
} finally {
|
||||||
cs.kill('SIGTERM');
|
cs.kill('SIGTERM');
|
||||||
books.kill('SIGTERM');
|
books.kill('SIGTERM');
|
||||||
|
fs.rmSync(booksFile, { force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ const server = http.createServer(async (req, res) => {
|
||||||
return json(200, { ok: true, role: 'zappier-sales-pricing' });
|
return json(200, { ok: true, role: 'zappier-sales-pricing' });
|
||||||
}
|
}
|
||||||
if (req.method === 'GET' && url.pathname === '/customers') {
|
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) });
|
return json(200, { customers: await listCustomers(EDGE, KEY) });
|
||||||
}
|
}
|
||||||
const review = url.pathname.match(/^\/review\/([^/]+)$/);
|
const review = url.pathname.match(/^\/review\/([^/]+)$/);
|
||||||
|
|
@ -68,6 +69,7 @@ const server = http.createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
const price = url.pathname.match(/^\/customers\/([^/]+)\/pricing$/);
|
const price = url.pathname.match(/^\/customers\/([^/]+)\/pricing$/);
|
||||||
if (req.method === 'PUT' && price) {
|
if (req.method === 'PUT' && price) {
|
||||||
|
if (!(await denyOrRedirect(req, res, json, { permission: 'sales.quote' }))) return;
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for await (const c of req) chunks.push(c);
|
for await (const c of req) chunks.push(c);
|
||||||
const payload = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
const payload = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue