128 lines
5.4 KiB
JavaScript
128 lines
5.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash, generateKeyPairSync, verify } from "node:crypto";
|
|
import { chmod, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import { createFoundryReaderGrantProvisioner } from "./foundry-reader-grant-provisioner.mjs";
|
|
|
|
const signatureSchema = "nodedc.external-data-plane.managed-provisioner-request/v1";
|
|
const audience = "nodedc-external-data-plane.managed-provisioning.v1";
|
|
const serviceId = "nodedc-module-foundry";
|
|
const keyId = "foundry-edp-managed-provisioner-v1";
|
|
const target = {
|
|
application: { id: "11111111-1111-4111-8111-111111111111" },
|
|
page: { id: "map" },
|
|
binding: { id: "fleet-current", dataProductId: "fleet.positions.current.v1" },
|
|
};
|
|
|
|
test("managed Foundry grant provisioning signs digest-only requests and persists the token privately", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "foundry-reader-grant-"));
|
|
const privateKeyFile = join(root, "private-key.pem");
|
|
const grantsDir = join(root, "grants");
|
|
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
await writeFile(privateKeyFile, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o400 });
|
|
await chmod(privateKeyFile, 0o400);
|
|
const requestBodies = [];
|
|
let capabilityDigest = null;
|
|
const fetchImpl = async (input, options) => {
|
|
const url = new URL(input);
|
|
const body = String(options.body);
|
|
const parsed = JSON.parse(body);
|
|
requestBodies.push(body);
|
|
const bodySha256 = createHash("sha256").update(body, "utf8").digest("hex");
|
|
assert.equal(options.headers["x-nodedc-content-sha256"], bodySha256);
|
|
const signedPayload = JSON.stringify({
|
|
schemaVersion: signatureSchema,
|
|
audience,
|
|
serviceId,
|
|
keyId,
|
|
method: options.method,
|
|
path: url.pathname,
|
|
timestamp: options.headers["x-nodedc-request-timestamp"],
|
|
nonce: options.headers["x-nodedc-request-nonce"],
|
|
bodySha256,
|
|
});
|
|
assert.equal(verify(
|
|
null,
|
|
Buffer.from(signedPayload, "utf8"),
|
|
publicKey,
|
|
Buffer.from(options.headers["x-nodedc-request-signature"], "base64url"),
|
|
), true);
|
|
assert.equal(options.headers["x-nodedc-engine-service-id"], serviceId);
|
|
assert.equal(options.headers["x-nodedc-engine-key-id"], keyId);
|
|
assert.equal(options.headers["x-nodedc-request-audience"], audience);
|
|
assert.deepEqual(parsed.allowedDataProductIds, ["fleet.positions.current.v1"]);
|
|
for (const forbidden of ["provider", "providerId", "tenant", "tenantId", "connection", "connectionId", "token", "capability"]) {
|
|
assert.equal(Object.hasOwn(parsed, forbidden), false);
|
|
}
|
|
if (url.pathname.endsWith("/plan")) {
|
|
assert.deepEqual(Object.keys(parsed), ["allowedDataProductIds"]);
|
|
return Response.json({
|
|
ok: true,
|
|
sourceScope: "resolved-server-side",
|
|
dataProducts: [{
|
|
id: "fleet.positions.current.v1",
|
|
version: "1.0.0",
|
|
deliveryMode: "snapshot+patch",
|
|
semanticTypes: ["map.moving_object"],
|
|
active: true,
|
|
}],
|
|
});
|
|
}
|
|
assert.match(url.pathname, /\/consumer-reader-bindings\/by-key\/fndrc-[0-9a-f]{64}$/);
|
|
assert.deepEqual(Object.keys(parsed).sort(), ["allowedDataProductIds", "capabilityDigest", "expiresAt", "generation"].sort());
|
|
assert.match(parsed.capabilityDigest, /^[0-9a-f]{64}$/);
|
|
capabilityDigest ||= parsed.capabilityDigest;
|
|
assert.equal(parsed.capabilityDigest, capabilityDigest);
|
|
const bindingKey = decodeURIComponent(url.pathname.split("/").at(-1));
|
|
return Response.json({
|
|
ok: true,
|
|
idempotent: capabilityDigest === parsed.capabilityDigest,
|
|
readerBinding: {
|
|
bindingKey,
|
|
generation: 1,
|
|
active: true,
|
|
expiresAt: null,
|
|
sourceScope: "resolved-server-side",
|
|
allowedDataProductIds: parsed.allowedDataProductIds,
|
|
},
|
|
});
|
|
};
|
|
const provisioner = createFoundryReaderGrantProvisioner({
|
|
dataPlaneUrl: "http://edp.test",
|
|
privateKeyFile,
|
|
grantsDir,
|
|
serviceId,
|
|
keyId,
|
|
audience,
|
|
fetchImpl,
|
|
now: () => new Date("2026-07-19T14:00:00.000Z"),
|
|
randomBytesImpl: () => Buffer.alloc(32, 7),
|
|
randomUUIDImpl: () => "22222222-2222-4222-8222-222222222222",
|
|
production: false,
|
|
});
|
|
try {
|
|
assert.equal(provisioner.configured, true);
|
|
const planned = await provisioner.plan(target);
|
|
assert.equal(planned.product.id, "fleet.positions.current.v1");
|
|
const first = await provisioner.ensure(target);
|
|
const second = await provisioner.ensure(target);
|
|
assert.equal(first.ensured, true);
|
|
assert.equal(second.ensured, true);
|
|
const files = await readdir(grantsDir);
|
|
assert.equal(files.length, 1);
|
|
const tokenPath = join(grantsDir, files[0]);
|
|
const token = (await readFile(tokenPath, "utf8")).trim();
|
|
assert.match(token, /^ndc_edprb_/);
|
|
assert.equal((await stat(tokenPath)).mode & 0o777, 0o400);
|
|
assert.equal(createHash("sha256").update(token, "utf8").digest("hex"), capabilityDigest);
|
|
assert.equal(await provisioner.readToken(target), token);
|
|
assert.equal(requestBodies.some((body) => body.includes(token)), false);
|
|
assert.equal(JSON.stringify([planned, first, second]).includes(token), false);
|
|
assert.equal(requestBodies.some((body) => /provider|tenant|connection/i.test(body)), false);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|