feat(foundry): add managed data consumers and agent settings
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user