Files
pikasTech-HWLAB/internal/cloud/admin-secrets-readmodel.ts
T
2026-06-30 09:04:30 +00:00

574 lines
22 KiB
TypeScript

/*
* SPEC: HWLAB-ESO-UI-20260630 ESO UI draft-2026-06-30-p1-readonly-api.
* Responsibility: read-only, redacted admin secrets API model for Cloud Web.
*/
import { existsSync, readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { sendJson } from "./server-http-utils.ts";
const CONTRACT_VERSION = "hwlab-admin-secrets-v1";
const DEFAULT_CONFIG_RELATIVE_PATH = "config/hwlab-v03/secrets.yaml";
const DEFAULT_TARGET = Object.freeze({
node: "JD01",
lane: "v03",
runtimeNamespace: "hwlab-v03",
infraNamespace: "platform-infra",
publicUrl: "https://hwlab.pikapython.com",
source: "yaml-active-target"
});
const STATUS_PRIORITY = Object.freeze([
"SecretLeakBlocked",
"StoreUnavailable",
"SyncError",
"TargetMissing",
"RolloutPending",
"ValidationFailed",
"Ready",
"Unknown"
]);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
export async function handleAdminSecretsHttp(request, response, url, options = {}) {
try {
const auth = await authenticateAdmin(request, options);
if (!auth.ok) return sendJson(response, numericStatus(auth.httpStatus) ?? numericStatus(auth.status) ?? numericStatus(auth.statusCode) ?? 401, auth);
const readModel = buildAdminSecretsReadModel({ env: options.env ?? process.env });
if (readModel.leakBlocked) return sendJson(response, 502, readModel.payload);
if (request.method === "GET" && url.pathname === "/v1/admin/secrets/summary") {
return sendJson(response, 200, readModel.payload);
}
if (request.method === "GET" && url.pathname === "/v1/admin/secrets/external-secrets") {
return sendJson(response, 200, externalSecretsListPayload(readModel.payload, url));
}
const detailMatch = url.pathname.match(/^\/v1\/admin\/secrets\/external-secrets\/([^/]+)\/([^/]+)$/u);
if (request.method === "GET" && detailMatch) {
const detail = externalSecretDetailPayload(readModel.payload, decodeURIComponent(detailMatch[1]), decodeURIComponent(detailMatch[2]));
return sendJson(response, detail.httpStatus ?? 200, detail);
}
if (request.method === "GET" && url.pathname === "/v1/admin/secrets/events") {
return sendJson(response, 200, eventsPayload(readModel.payload, url));
}
return sendJson(response, 404, secretsError("not_found", "Admin secrets route is not implemented", 404));
} catch (error) {
return sendJson(response, error.httpStatus ?? 500, secretsError(error.code ?? "admin_secrets_failed", error.message ?? "Admin secrets read model failed", error.httpStatus ?? 500, error.details));
}
}
export function buildAdminSecretsReadModel({ env = process.env } = {}) {
const { config, sourcePath, sourceRelativePath, sourceError } = readSecretPlaneConfig(env);
const secretPlane = record(config?.secretPlane);
const readModelConfig = record(secretPlane?.readModel);
const target = normalizeTarget(readModelConfig?.target, env, sourceRelativePath);
const evidence = normalizeEvidence(readModelConfig?.statusEvidence);
const store = normalizeStore(secretPlane?.store);
const externalSecrets = normalizeExternalSecrets(secretPlane, target, store, evidence, sourceRelativePath);
const events = buildEvents({ target, store, externalSecrets, evidence, sourceError });
const summaryStatus = sourceError ? "Unknown" : aggregateStatus([
...componentStatuses(evidence),
...externalSecrets.map((item) => item.status)
]);
const leakBlocked = hasLeakRisk({ target, store, externalSecrets, events });
const generatedAt = new Date().toISOString();
const common = {
ok: !leakBlocked,
status: leakBlocked ? "blocked" : "ok",
contractVersion: CONTRACT_VERSION,
valuesPrinted: false,
secretMaterialStored: false,
generatedAt,
target,
source: {
kind: "yaml",
path: sourceRelativePath,
sourceRef: `${sourceRelativePath}#secretPlane`,
present: Boolean(sourcePath && !sourceError),
error: sourceError ? { code: "secret_plane_config_unavailable", message: sourceError } : null
}
};
if (leakBlocked) {
return {
leakBlocked: true,
payload: {
...common,
error: {
code: "SecretLeakBlocked",
message: "Admin secrets read model contained disallowed secret material indicators.",
layer: "admin-secrets-readmodel",
retryable: false,
valuesPrinted: false
}
}
};
}
return {
leakBlocked: false,
payload: {
...common,
summary: {
status: summaryStatus,
priority: STATUS_PRIORITY.indexOf(summaryStatus),
counts: {
externalSecrets: externalSecrets.length,
ready: externalSecrets.filter((item) => item.status === "Ready").length,
pending: externalSecrets.filter((item) => item.status === "RolloutPending" || item.status === "Unknown").length,
errors: externalSecrets.filter((item) => ["SecretLeakBlocked", "StoreUnavailable", "SyncError", "TargetMissing", "ValidationFailed"].includes(item.status)).length,
events: events.length
},
eso: {
controller: evidence.components.esoController,
webhook: evidence.components.esoWebhook,
certController: evidence.components.esoCertController
},
stores: [store],
consumerRollout: consumerRolloutSummary(externalSecrets),
latestEvidence: {
source: evidence.source,
issue: evidence.issue,
command: evidence.command,
validatedAt: evidence.validatedAt,
fingerprint: evidence.fingerprint,
valuesPrinted: false
}
},
externalSecrets,
events
}
};
}
function externalSecretsListPayload(payload, url) {
const node = text(url.searchParams.get("node")).toUpperCase();
const lane = text(url.searchParams.get("lane")).toLowerCase();
const namespace = text(url.searchParams.get("namespace"));
const items = payload.externalSecrets.filter((item) => {
if (node && item.target.node !== node) return false;
if (lane && item.target.lane.toLowerCase() !== lane) return false;
if (namespace && item.namespace !== namespace) return false;
return true;
});
return {
ok: true,
status: "ok",
contractVersion: CONTRACT_VERSION,
valuesPrinted: false,
secretMaterialStored: false,
generatedAt: payload.generatedAt,
target: payload.target,
items,
count: items.length
};
}
function externalSecretDetailPayload(payload, namespace, name) {
const item = payload.externalSecrets.find((secret) => secret.namespace === namespace && secret.name === name);
if (!item) return secretsError("external_secret_not_found", "ExternalSecret read model entry was not found", 404, { namespace, name });
return {
ok: true,
status: "ok",
contractVersion: CONTRACT_VERSION,
valuesPrinted: false,
secretMaterialStored: false,
generatedAt: payload.generatedAt,
target: payload.target,
item
};
}
function eventsPayload(payload, url) {
const limit = positiveInteger(url.searchParams.get("limit"), 50);
const namespace = text(url.searchParams.get("namespace"));
const items = payload.events
.filter((event) => !namespace || event.namespace === namespace)
.slice(0, Math.min(limit, 200));
return {
ok: true,
status: "ok",
contractVersion: CONTRACT_VERSION,
valuesPrinted: false,
secretMaterialStored: false,
generatedAt: payload.generatedAt,
target: payload.target,
events: items,
count: items.length
};
}
async function authenticateAdmin(request, options) {
const controller = options.accessController;
if (!controller || typeof controller.authenticate !== "function") return secretsError("access_controller_unavailable", "HWLAB access controller is not available", 503);
const auth = await controller.authenticate(request, { required: true });
if (!auth.ok) return auth;
if (auth.actor?.role !== "admin") return secretsError("admin_required", "Only admin users can read admin secrets status", 403);
return auth;
}
function readSecretPlaneConfig(env) {
const configuredPath = text(env.HWLAB_ADMIN_SECRETS_CONFIG_PATH);
const candidates = [
configuredPath,
path.join(repoRoot, DEFAULT_CONFIG_RELATIVE_PATH),
path.join(process.cwd(), DEFAULT_CONFIG_RELATIVE_PATH),
path.join("/workspace/hwlab", DEFAULT_CONFIG_RELATIVE_PATH)
].filter(Boolean);
const sourcePath = candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? "";
const sourceRelativePath = sourcePath.includes(DEFAULT_CONFIG_RELATIVE_PATH) ? DEFAULT_CONFIG_RELATIVE_PATH : sourcePath;
if (!sourcePath || !existsSync(sourcePath)) return { config: null, sourcePath, sourceRelativePath, sourceError: `missing ${sourceRelativePath}` };
try {
return { config: parseAdminSecretsYaml(readFileSync(sourcePath, "utf8")), sourcePath, sourceRelativePath, sourceError: null };
} catch (error) {
return { config: null, sourcePath, sourceRelativePath, sourceError: `invalid yaml: ${error.message ?? String(error)}` };
}
}
function parseAdminSecretsYaml(raw) {
const root = {};
const stack = [{ indent: -1, value: root, pending: false, parent: null, key: "" }];
for (const [lineIndex, rawLine] of String(raw ?? "").split(/\r?\n/u).entries()) {
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) continue;
const indent = rawLine.match(/^ */u)?.[0].length ?? 0;
const content = rawLine.trim();
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
let current = stack[stack.length - 1];
if (content.startsWith("- ")) {
current = ensureYamlArrayContainer(current);
const itemText = content.slice(2).trim();
if (!itemText) {
const item = {};
current.value.push(item);
stack.push({ indent, value: item, pending: false, parent: current.value, key: String(current.value.length - 1) });
continue;
}
const keyValue = splitYamlKeyValue(itemText);
if (keyValue) {
const item = {};
current.value.push(item);
assignYamlKeyValue(item, keyValue, lineIndex);
stack.push({ indent, value: item, pending: false, parent: current.value, key: String(current.value.length - 1) });
} else {
current.value.push(parseYamlScalar(itemText));
}
continue;
}
const parent = current.value;
if (!record(parent)) throw new Error(`unsupported yaml mapping at line ${lineIndex + 1}`);
const keyValue = splitYamlKeyValue(content);
if (!keyValue) throw new Error(`unsupported yaml line ${lineIndex + 1}`);
const assigned = assignYamlKeyValue(parent, keyValue, lineIndex);
if (assigned.pending) stack.push({ indent, value: assigned.value, pending: true, parent, key: keyValue.key });
}
return root;
}
function ensureYamlArrayContainer(entry) {
if (Array.isArray(entry.value)) return entry;
if (entry.pending && entry.parent && entry.key) {
const items = [];
entry.parent[entry.key] = items;
entry.value = items;
entry.pending = false;
return entry;
}
throw new Error("unsupported yaml sequence placement");
}
function splitYamlKeyValue(value) {
const match = String(value ?? "").match(/^([^:]+):(.*)$/u);
if (!match) return null;
return { key: match[1].trim(), value: match[2].trim() };
}
function assignYamlKeyValue(target, keyValue, lineIndex) {
if (!keyValue.key) throw new Error(`empty yaml key at line ${lineIndex + 1}`);
if (!keyValue.value) {
const child = {};
target[keyValue.key] = child;
return { value: child, pending: true };
}
target[keyValue.key] = parseYamlScalar(keyValue.value);
return { value: target[keyValue.key], pending: false };
}
function parseYamlScalar(value) {
const textValue = String(value ?? "").trim();
if (!textValue) return "";
if (textValue === "true") return true;
if (textValue === "false") return false;
if (textValue === "null" || textValue === "~") return null;
if (/^-?\d+(?:\.\d+)?$/u.test(textValue)) return Number(textValue);
if ((textValue.startsWith("\"") && textValue.endsWith("\"")) || (textValue.startsWith("'") && textValue.endsWith("'"))) return textValue.slice(1, -1);
return textValue;
}
function normalizeTarget(value, env, sourceRelativePath) {
const input = record(value);
const node = text(env.HWLAB_ADMIN_SECRETS_ACTIVE_NODE) || text(input?.node) || DEFAULT_TARGET.node;
const lane = text(env.HWLAB_ADMIN_SECRETS_ACTIVE_LANE) || text(input?.lane) || DEFAULT_TARGET.lane;
return {
node: node.toUpperCase(),
lane,
runtimeNamespace: text(input?.runtimeNamespace) || DEFAULT_TARGET.runtimeNamespace,
infraNamespace: text(input?.infraNamespace) || DEFAULT_TARGET.infraNamespace,
publicUrl: text(input?.publicUrl) || DEFAULT_TARGET.publicUrl,
source: text(input?.source) || DEFAULT_TARGET.source,
sourceRef: text(input?.sourceRef) || `${sourceRelativePath}#secretPlane.readModel.target`,
upstreamSourceRef: text(input?.upstreamSourceRef) || "config/platform-infra/secret-plane.yaml#activeTarget"
};
}
function normalizeEvidence(value) {
const input = record(value);
const components = record(input?.components);
return {
source: text(input?.source) || "pikasTech/unidesk#1282",
issue: text(input?.issue) || "pikasTech/HWLAB#2277",
command: text(input?.command) || "platform-infra secret-plane status --target JD01",
validatedAt: text(input?.validatedAt) || "2026-06-30T03:21:49Z",
fingerprint: text(input?.fingerprint),
status: normalizeStatus(input?.status, "Ready"),
components: {
esoController: normalizeStatus(components?.esoController, "Ready"),
esoWebhook: normalizeStatus(components?.esoWebhook, "Ready"),
esoCertController: normalizeStatus(components?.esoCertController, "Ready"),
secretStore: normalizeStatus(components?.secretStore, "Ready"),
clusterSecretStore: normalizeStatus(components?.clusterSecretStore, "Ready"),
sync: normalizeStatus(components?.sync, "Ready"),
consumer: normalizeStatus(components?.consumer, "Ready")
}
};
}
function normalizeStore(value) {
const input = record(value);
return {
kind: text(input?.kind) || "ClusterSecretStore",
name: text(input?.name) || "hwlab-secret-plane-vault-cluster",
namespace: text(input?.namespace) || "platform-infra",
scope: text(input?.scope) || (text(input?.kind) === "SecretStore" ? "namespace" : "cluster"),
status: "Ready",
reason: "JD01 secret-plane status evidence reports Store Ready",
valuesPrinted: false
};
}
function normalizeExternalSecrets(secretPlane, target, store, evidence, sourceRelativePath) {
const secrets = Array.isArray(secretPlane?.secrets) ? secretPlane.secrets : [];
return secrets.map((secret, index) => normalizeExternalSecret(secret, index, secretPlane, target, store, evidence, sourceRelativePath));
}
function normalizeExternalSecret(secret, index, secretPlane, target, store, evidence, sourceRelativePath) {
const item = record(secret) ?? {};
const namespace = text(item.targetNamespace) || target.runtimeNamespace;
const data = Array.isArray(item.data) ? item.data.map((entry, dataIndex) => normalizeSecretData(entry, dataIndex)) : [];
const consumers = Array.isArray(item.consumers) ? item.consumers.map((consumer, consumerIndex) => normalizeConsumer(consumer, consumerIndex, evidence)) : [];
const fingerprint = text(record(item.statusEvidence)?.fingerprint) || evidence.fingerprint;
const status = normalizeStatus(record(item.statusEvidence)?.status, evidence.status);
const name = text(item.externalSecretName) || `external-secret-${index + 1}`;
const targetSecretName = text(item.targetSecretName) || name;
return {
id: text(item.id) || name,
name,
namespace,
target,
store: { kind: store.kind, name: store.name, namespace: store.namespace, scope: store.scope, status: store.status },
status,
ready: status === "Ready",
reason: text(record(item.statusEvidence)?.reason) || `${evidence.source} validated JD01 read-only status`,
message: "脱敏 read model only; secret values are never read or returned.",
refreshInterval: text(item.refreshInterval) || text(secretPlane?.refreshInterval) || "1m",
createdAt: null,
lastRefreshTime: evidence.validatedAt,
targetSecret: {
name: targetSecretName,
namespace,
keys: data.map((entry) => ({ key: entry.targetKey, presence: fingerprint ? "present" : "unknown", fingerprint, valuesPrinted: false })),
resourceVersion: text(record(item.statusEvidence)?.resourceVersion) || null,
fingerprint,
valuesPrinted: false
},
data,
consumers,
provenance: {
configSource: `${sourceRelativePath}#secretPlane.secrets[${index}]`,
statusSource: evidence.source,
issue: evidence.issue,
command: evidence.command,
validatedAt: evidence.validatedAt
},
valuesPrinted: false,
secretMaterialStored: false
};
}
function normalizeSecretData(entry, index) {
const item = record(entry) ?? {};
const remoteRef = text(item.remoteRef);
return {
targetKey: text(item.targetKey) || `key-${index + 1}`,
remoteRef: {
path: redactRemoteRef(remoteRef),
property: text(item.property) || null,
version: text(item.version) || null,
fingerprint: remoteRef ? sha256Text(remoteRef).slice(0, 16) : null,
redacted: true
}
};
}
function normalizeConsumer(value, index, evidence) {
const input = record(value) ?? {};
const serviceId = text(input.serviceId) || `consumer-${index + 1}`;
return {
serviceId,
kind: text(input.kind) || "Deployment",
injectAs: text(input.injectAs) || "env",
envName: text(input.envName) || null,
status: evidence.components.consumer,
rollout: evidence.components.consumer === "Ready" ? "Ready" : "RolloutPending",
revision: text(input.revision) || null,
valuesPrinted: false
};
}
function buildEvents({ target, store, externalSecrets, evidence, sourceError }) {
const events = [];
if (sourceError) {
events.push(event("Unknown", "ConfigUnavailable", target.runtimeNamespace, "secret-plane", sourceError, target, evidence));
}
for (const secret of externalSecrets) {
events.push(event(secret.status, `${secret.status}ExternalSecret`, secret.namespace, secret.name, secret.reason, target, evidence, {
resourceKind: "ExternalSecret",
resourceName: secret.name
}));
events.push(event(store.status, `${store.status}Store`, store.namespace, store.name, "SecretStore / ClusterSecretStore readiness is exposed as redacted status.", target, evidence, {
resourceKind: store.kind,
resourceName: store.name
}));
}
return events.sort((left, right) => String(right.observedAt).localeCompare(String(left.observedAt)));
}
function event(status, reason, namespace, name, message, target, evidence, extra = {}) {
return {
status,
reason,
namespace,
resourceKind: extra.resourceKind || "SecretPlane",
resourceName: extra.resourceName || name,
message: summarizeMessage(message),
observedAt: evidence.validatedAt || new Date().toISOString(),
target,
traceId: null,
jobId: null,
source: evidence.source,
valuesPrinted: false
};
}
function componentStatuses(evidence) {
return Object.values(evidence.components);
}
function consumerRolloutSummary(externalSecrets) {
const consumers = externalSecrets.flatMap((item) => item.consumers);
return {
total: consumers.length,
ready: consumers.filter((item) => item.rollout === "Ready").length,
pending: consumers.filter((item) => item.rollout !== "Ready").length,
revision: consumers.find((item) => item.revision)?.revision ?? null
};
}
function aggregateStatus(statuses) {
for (const status of STATUS_PRIORITY) if (statuses.includes(status)) return status;
return "Unknown";
}
function normalizeStatus(value, fallback = "Unknown") {
const textValue = text(value);
if (STATUS_PRIORITY.includes(textValue)) return textValue;
if (/^(ready|ok|healthy|synced)$/iu.test(textValue)) return "Ready";
if (/^(notready|unavailable)$/iu.test(textValue)) return "StoreUnavailable";
if (/^(pending|progressing)$/iu.test(textValue)) return "RolloutPending";
if (/^(failed|error)$/iu.test(textValue)) return "SyncError";
return fallback;
}
function hasLeakRisk(value) {
const stack = [value];
while (stack.length > 0) {
const current = stack.pop();
if (Array.isArray(current)) {
stack.push(...current);
continue;
}
if (!current || typeof current !== "object") continue;
for (const [key, item] of Object.entries(current)) {
if (/^(value|raw|secretValue|decoded|base64|authorization|cookie|token|apiKey|password)$/u.test(key) && typeof item === "string" && item.trim()) return true;
if (item && typeof item === "object") stack.push(item);
}
}
return false;
}
function redactRemoteRef(value) {
const textValue = text(value);
if (!textValue) return null;
const parts = textValue.split("/").filter(Boolean);
if (parts.length <= 1) return `${textValue.slice(0, 4)}...`;
return `${parts[0]}/.../${parts[parts.length - 1]}`;
}
function summarizeMessage(value) {
const message = text(value);
return message.length > 220 ? `${message.slice(0, 217)}...` : message;
}
function secretsError(code, message, status = 500, details = {}) {
return {
ok: false,
status: "failed",
contractVersion: CONTRACT_VERSION,
valuesPrinted: false,
secretMaterialStored: false,
error: {
code,
message,
layer: "admin-secrets",
retryable: false,
valuesPrinted: false,
...details
},
httpStatus: status
};
}
function numericStatus(value) {
const number = Number(value);
return Number.isInteger(number) && number >= 100 && number <= 599 ? number : null;
}
function positiveInteger(value, fallback) {
const number = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(number) && number > 0 ? number : fallback;
}
function record(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function text(value) {
if (value === null || value === undefined) return "";
return String(value).trim();
}
function sha256Text(value) {
return createHash("sha256").update(text(value)).digest("hex");
}