NODEDC_DESIGN_GUIDELINE/server/foundry-reader-grant-provis...

268 lines
10 KiB
JavaScript

import { constants as fsConstants } from "node:fs";
import { chmod, lstat, mkdir, open } from "node:fs/promises";
import { createHash, createPrivateKey, randomBytes, randomUUID, sign } from "node:crypto";
import { join, resolve } from "node:path";
const SIGNATURE_SCHEMA = "nodedc.external-data-plane.managed-provisioner-request/v1";
const TOKEN_PATTERN = /^ndc_edprb_[A-Za-z0-9_-]{32,512}$/;
const IDENTITY_PATTERN = /^[a-z][a-z0-9._:-]{2,127}$/i;
const AUDIENCE_PATTERN = /^[a-z][a-z0-9._:/-]{2,255}$/i;
function provisionerError(code, statusCode = 500) {
return Object.assign(new Error(code), { statusCode });
}
function targetAddress(target) {
const applicationId = String(target?.application?.id || target?.applicationId || "");
const pageId = String(target?.page?.id || target?.pageId || "");
const bindingId = String(target?.binding?.id || target?.bindingId || "");
if (!/^[0-9a-f-]{36}$/i.test(applicationId) || !IDENTITY_PATTERN.test(pageId)
|| !IDENTITY_PATTERN.test(bindingId)) {
throw provisionerError("foundry_reader_grant_target_invalid", 400);
}
return { applicationId, pageId, bindingId };
}
function targetIdentity(target) {
const address = targetAddress(target);
const dataProductId = String(target?.binding?.dataProductId || target?.dataProductId || "");
if (!IDENTITY_PATTERN.test(dataProductId)) throw provisionerError("foundry_reader_grant_target_invalid", 400);
const { applicationId, pageId, bindingId } = address;
return { applicationId, pageId, bindingId, dataProductId };
}
function targetDigest(target) {
const identity = targetAddress(target);
return createHash("sha256")
.update(`${identity.applicationId}/${identity.pageId}/${identity.bindingId}`, "utf8")
.digest("hex");
}
function signingPayload({ audience, serviceId, keyId, method, path, timestamp, nonce, bodySha256 }) {
return JSON.stringify({
schemaVersion: SIGNATURE_SCHEMA,
audience,
serviceId,
keyId,
method,
path,
timestamp,
nonce,
bodySha256,
});
}
export function createFoundryReaderGrantProvisioner({
dataPlaneUrl,
privateKeyFile,
grantsDir,
serviceId = "nodedc-module-foundry",
keyId = "foundry-edp-managed-provisioner-v1",
audience = "nodedc-external-data-plane.managed-provisioning.v1",
fetchImpl = fetch,
now = () => new Date(),
randomBytesImpl = randomBytes,
randomUUIDImpl = randomUUID,
production = process.env.NODE_ENV === "production",
}) {
const baseUrl = String(dataPlaneUrl || "").trim().replace(/\/$/, "");
const keyPath = String(privateKeyFile || "").trim();
const tokenRoot = String(grantsDir || "").trim();
const configured = Boolean(baseUrl && keyPath && tokenRoot && IDENTITY_PATTERN.test(serviceId)
&& IDENTITY_PATTERN.test(keyId) && AUDIENCE_PATTERN.test(audience));
let privateKeyPromise = null;
async function loadPrivateKey() {
if (!configured) throw provisionerError("foundry_reader_grant_provisioner_not_configured", 503);
if (!privateKeyPromise) privateKeyPromise = readPrivateKeySecurely(keyPath, { production });
return privateKeyPromise;
}
async function signedRequest(method, path, value) {
const privateKey = await loadPrivateKey();
const body = JSON.stringify(value);
const bodySha256 = createHash("sha256").update(body, "utf8").digest("hex");
const timestamp = now().toISOString();
const nonce = randomUUIDImpl();
const signature = sign(null, Buffer.from(signingPayload({
audience,
serviceId,
keyId,
method,
path,
timestamp,
nonce,
bodySha256,
}), "utf8"), privateKey).toString("base64url");
let response;
try {
response = await fetchImpl(new URL(path, `${baseUrl}/`), {
method,
headers: {
"content-type": "application/json",
accept: "application/json",
"x-nodedc-engine-service-id": serviceId,
"x-nodedc-engine-key-id": keyId,
"x-nodedc-request-audience": audience,
"x-nodedc-request-timestamp": timestamp,
"x-nodedc-request-nonce": nonce,
"x-nodedc-content-sha256": bodySha256,
"x-nodedc-request-signature": signature,
},
body,
signal: AbortSignal.timeout(10_000),
});
} catch {
throw provisionerError("foundry_reader_grant_provisioner_unavailable", 503);
}
const payload = await response.json().catch(() => null);
if (!response.ok) {
const code = String(payload?.error || payload?.code || "foundry_reader_grant_provisioner_rejected");
throw provisionerError(/^[a-z][a-z0-9_]{2,120}$/.test(code) ? code : "foundry_reader_grant_provisioner_rejected", response.status);
}
return payload;
}
async function plan(target) {
const identity = targetIdentity(target);
const payload = await signedRequest("POST", "/internal/data-plane/v1/consumer-reader-bindings/plan", {
allowedDataProductIds: [identity.dataProductId],
});
const product = Array.isArray(payload?.dataProducts)
? payload.dataProducts.find((candidate) => candidate?.id === identity.dataProductId)
: null;
if (!product || payload?.sourceScope !== "resolved-server-side") {
throw provisionerError("foundry_reader_grant_plan_response_invalid", 502);
}
return { product, sourceScope: "resolved-server-side" };
}
async function ensure(target) {
const identity = targetIdentity(target);
const digest = targetDigest(identity);
const token = await ensureReaderToken(join(resolve(tokenRoot), digest), {
production,
randomBytesImpl,
});
const bindingKey = `fndrc-${digest}`;
const payload = await signedRequest(
"PUT",
`/internal/data-plane/v1/consumer-reader-bindings/by-key/${encodeURIComponent(bindingKey)}`,
{
allowedDataProductIds: [identity.dataProductId],
expiresAt: null,
generation: 1,
capabilityDigest: createHash("sha256").update(token, "utf8").digest("hex"),
},
);
const binding = payload?.readerBinding;
if (binding?.bindingKey !== bindingKey || binding?.generation !== 1 || binding?.active !== true
|| binding?.expiresAt !== null || binding?.sourceScope !== "resolved-server-side"
|| !Array.isArray(binding?.allowedDataProductIds)
|| !binding.allowedDataProductIds.includes(identity.dataProductId)) {
throw provisionerError("foundry_reader_grant_ensure_response_invalid", 502);
}
return { ensured: true, idempotent: payload?.idempotent === true, generation: 1, sourceScope: "resolved-server-side" };
}
async function readToken(target) {
if (!tokenRoot) return null;
return readReaderToken(join(resolve(tokenRoot), targetDigest(target)), { production, missing: null });
}
return { configured, plan, ensure, readToken };
}
async function readPrivateKeySecurely(path, { production }) {
let handle;
try {
handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
} catch {
throw provisionerError("foundry_reader_grant_private_key_unreadable", 503);
}
try {
const metadata = await handle.stat();
if (!metadata.isFile() || metadata.size < 80 || metadata.size > 8192 || (metadata.mode & 0o022) !== 0
|| (production && metadata.uid !== 0)) {
throw provisionerError("foundry_reader_grant_private_key_invalid", 503);
}
const pem = await handle.readFile("utf8");
const privateKey = createPrivateKey(pem);
if (privateKey.type !== "private" || privateKey.asymmetricKeyType !== "ed25519") {
throw provisionerError("foundry_reader_grant_private_key_invalid", 503);
}
return privateKey;
} catch (error) {
if (String(error?.message || "").startsWith("foundry_reader_grant_")) throw error;
throw provisionerError("foundry_reader_grant_private_key_invalid", 503);
} finally {
await handle.close();
}
}
async function ensureReaderToken(path, { production, randomBytesImpl }) {
const existing = await readReaderToken(path, { production, missing: null });
if (existing) return existing;
const directory = resolve(path, "..");
await mkdir(directory, { recursive: true, mode: 0o700 });
await chmod(directory, 0o700);
const directoryMetadata = await lstat(directory);
if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()
|| (directoryMetadata.mode & 0o077) !== 0 || (production && directoryMetadata.uid !== 0)) {
throw provisionerError("foundry_reader_grant_store_invalid", 503);
}
const token = `ndc_edprb_${randomBytesImpl(32).toString("base64url")}`;
let handle;
try {
handle = await open(
path,
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | (fsConstants.O_NOFOLLOW || 0),
0o400,
);
await handle.writeFile(`${token}\n`, "utf8");
await handle.sync();
await handle.chmod(0o400);
return token;
} catch (error) {
if (error?.code === "EEXIST") return readConcurrentlyCreatedReaderToken(path, { production });
throw provisionerError("foundry_reader_grant_store_unavailable", 503);
} finally {
await handle?.close();
}
}
async function readConcurrentlyCreatedReaderToken(path, { production }) {
for (let attempt = 0; attempt < 10; attempt += 1) {
try {
const token = await readReaderToken(path, { production, missing: null });
if (token) return token;
} catch (error) {
if (error?.message !== "foundry_reader_grant_invalid") throw error;
}
await new Promise((resolveDelay) => setTimeout(resolveDelay, 10));
}
throw provisionerError("foundry_reader_grant_store_unavailable", 503);
}
async function readReaderToken(path, { production, missing }) {
let handle;
try {
handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
} catch (error) {
if (error?.code === "ENOENT") return missing;
throw provisionerError("foundry_reader_grant_unavailable", 503);
}
try {
const metadata = await handle.stat();
if (!metadata.isFile() || metadata.size < 2 || metadata.size > 1024 || (metadata.mode & 0o222) !== 0
|| (production && metadata.uid !== 0)) {
throw provisionerError("foundry_reader_grant_invalid", 503);
}
const token = (await handle.readFile("utf8")).trim();
if (!TOKEN_PATTERN.test(token)) throw provisionerError("foundry_reader_grant_invalid", 503);
return token;
} finally {
await handle.close();
}
}