Files
pikasTech-unidesk/scripts/src/platform-infra-pac-provenance.ts
T

311 lines
18 KiB
TypeScript

import { createHash } from "node:crypto";
import { rootPath } from "./config";
import { readYamlRecord } from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";
const configPath = "config/platform-infra/pipelines-as-code.yaml";
export interface PacAdmissionConsumerContract {
readonly id: string;
readonly node: string;
readonly namespace: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly markerValue: string;
readonly executionServiceAccountName: string;
readonly gitOps: { readonly repoUrl: string; readonly targetRevision: string };
}
export interface PacAdmissionContract {
readonly version: string;
readonly markerAnnotation: string;
readonly controllerUsername: string;
readonly policyName: string;
readonly bindingName: string;
readonly failurePolicy: "Fail";
readonly validationActions: readonly ["Deny"];
readonly child: {
readonly enabled: false;
readonly parentNameAnnotation: string;
readonly parentUidAnnotation: string;
};
readonly consumers: readonly PacAdmissionConsumerContract[];
}
export interface PacAdmissionDesiredIdentity {
readonly configSha256: string;
readonly policyName: string;
readonly bindingName: string;
readonly version: string;
readonly controllerIdentity: string;
}
export function readPacAdmissionContract(): PacAdmissionContract {
const source = readYamlRecord<Record<string, unknown>>(rootPath("config", "platform-infra", "pipelines-as-code.yaml"), "platform-infra-pipelines-as-code");
return parsePacAdmissionContract(source);
}
export function parsePacAdmissionContract(source: Record<string, unknown>): PacAdmissionContract {
const root = record(materializeYamlComposition(source, { label: configPath }).value, configPath);
const provenance = record(root.deliveryProvenance, `${configPath}#deliveryProvenance`);
const child = record(provenance.child, `${configPath}#deliveryProvenance.child`);
const controller = record(provenance.controller, `${configPath}#deliveryProvenance.controller`);
const admission = record(provenance.admission, `${configPath}#deliveryProvenance.admission`);
const controllerNamespace = required(controller.namespace, "deliveryProvenance.controller.namespace");
const controllerServiceAccount = required(controller.serviceAccountName, "deliveryProvenance.controller.serviceAccountName");
const validationActions = stringArray(admission.validationActions, "deliveryProvenance.admission.validationActions");
if (validationActions.length !== 1 || validationActions[0] !== "Deny") throw new Error("deliveryProvenance.admission.validationActions must be exactly [Deny]");
const consumers = recordArray(root.consumers, "consumers").flatMap((consumer, index) => {
if (consumer.deliveryProvenance === undefined) return [];
const value = record(consumer.deliveryProvenance, `consumers[${index}].deliveryProvenance`);
if (value.required === false) return [];
if (value.required !== true) throw new Error(`consumers[${index}].deliveryProvenance.required must be boolean true or false`);
const gitOps = record(value.gitOps, `consumers[${index}].deliveryProvenance.gitOps`);
return [{
id: required(consumer.id, `consumers[${index}].id`),
node: required(consumer.node, `consumers[${index}].node`),
namespace: required(consumer.namespace, `consumers[${index}].namespace`),
pipeline: required(consumer.pipeline, `consumers[${index}].pipeline`),
pipelineRunPrefix: required(consumer.pipelineRunPrefix, `consumers[${index}].pipelineRunPrefix`),
markerValue: required(value.markerValue, `consumers[${index}].deliveryProvenance.markerValue`),
executionServiceAccountName: required(value.executionServiceAccountName, `consumers[${index}].deliveryProvenance.executionServiceAccountName`),
gitOps: {
repoUrl: required(gitOps.repoUrl, `consumers[${index}].deliveryProvenance.gitOps.repoUrl`),
targetRevision: required(gitOps.targetRevision, `consumers[${index}].deliveryProvenance.gitOps.targetRevision`),
},
}];
});
const markerValues = new Set(consumers.map((consumer) => consumer.markerValue));
if (markerValues.size !== consumers.length) throw new Error("deliveryProvenance markerValue must be unique per consumer");
const version = required(provenance.version, "deliveryProvenance.version");
const versionMatch = version.match(/-v([1-9][0-9]*)$/u);
if (versionMatch === null) throw new Error("deliveryProvenance.version must end with a positive -vN resource epoch");
const resourceEpoch = `-v${versionMatch[1]}`;
const policyName = required(admission.policyName, "deliveryProvenance.admission.policyName");
const bindingName = required(admission.bindingName, "deliveryProvenance.admission.bindingName");
if (!policyName.endsWith(resourceEpoch) || !bindingName.endsWith(resourceEpoch)) throw new Error(`deliveryProvenance admission resource names must end with ${resourceEpoch}`);
return {
version,
markerAnnotation: required(provenance.markerAnnotation, "deliveryProvenance.markerAnnotation"),
controllerUsername: `system:serviceaccount:${controllerNamespace}:${controllerServiceAccount}`,
policyName,
bindingName,
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Fail"),
validationActions: ["Deny"],
child: {
enabled: literal(child.enabled, "deliveryProvenance.child.enabled", false),
parentNameAnnotation: required(child.parentNameAnnotation, "deliveryProvenance.child.parentNameAnnotation"),
parentUidAnnotation: required(child.parentUidAnnotation, "deliveryProvenance.child.parentUidAnnotation"),
},
consumers,
};
}
export function pacAdmissionConsumer(consumerId: string): PacAdmissionConsumerContract | null {
return readPacAdmissionContract().consumers.find((consumer) => consumer.id === consumerId) ?? null;
}
export function renderPacAdmissionDesiredFragment(targetId: string): string {
const contract = readPacAdmissionContract();
const consumers = contract.consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase());
if (consumers.length === 0) return "";
const annotation = contract.markerAnnotation;
const markerPresent = `has(object.metadata.annotations) && ${cel(annotation)} in object.metadata.annotations`;
const oldMarkerPresent = `has(oldObject.metadata.annotations) && ${cel(annotation)} in oldObject.metadata.annotations`;
const protectedUpdate = [markerPresent, oldMarkerPresent, ...consumers.flatMap((consumer) => [candidateExpression(consumer, "object"), candidateExpression(consumer, "oldObject")])].map((item) => `(${item})`).join(" || ");
const immutableProofKeys = [
["labels", "app.kubernetes.io/managed-by"],
["labels", "pipelinesascode.tekton.dev/event-type"],
["labels", "pipelinesascode.tekton.dev/repository"],
["labels", "pipelinesascode.tekton.dev/sha"],
["labels", "pipelinesascode.tekton.dev/commit"],
["labels", "pipelinesascode.tekton.dev/branch"],
["labels", "pipelinesascode.tekton.dev/sender"],
["annotations", "pipelinesascode.tekton.dev/controller-info"],
["annotations", "pipelinesascode.tekton.dev/event-type"],
["annotations", "pipelinesascode.tekton.dev/git-provider"],
["annotations", "pipelinesascode.tekton.dev/repository"],
["annotations", "pipelinesascode.tekton.dev/sha"],
["annotations", "pipelinesascode.tekton.dev/commit"],
["annotations", "pipelinesascode.tekton.dev/branch"],
["annotations", "pipelinesascode.tekton.dev/source-branch"],
["annotations", "pipelinesascode.tekton.dev/sender"],
["annotations", contract.child.parentNameAnnotation],
["annotations", contract.child.parentUidAnnotation],
] as const;
const validations: Record<string, unknown>[] = [
{
expression: `request.operation != 'CREATE' || !(${markerPresent}) || request.userInfo.username == ${cel(contract.controllerUsername)}`,
message: "reserved PaC provenance marker may only be created by the authenticated PaC controller ServiceAccount",
reason: "Forbidden",
},
{
expression: `request.operation != 'UPDATE' || ((${markerPresent}) == (${oldMarkerPresent}) && (!(${markerPresent}) || object.metadata.annotations[${cel(annotation)}] == oldObject.metadata.annotations[${cel(annotation)}]))`,
message: "reserved PaC provenance marker is immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'CREATE' || ((!has(object.metadata.annotations) || !(${cel(contract.child.parentNameAnnotation)} in object.metadata.annotations)) && (!has(object.metadata.annotations) || !(${cel(contract.child.parentUidAnnotation)} in object.metadata.annotations)))`,
message: "child PipelineRun provenance is disabled until an admission-verified parent name and UID contract is enabled",
reason: "Forbidden",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${immutableProofKeys.map(([map, key]) => metadataEntryEqual(map, key)).join(" && ")})`,
message: "PaC repository, branch, revision, event, controller, sender, and parent identity proof fields are immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || ((!has(object.spec.params) && !has(oldObject.spec.params)) || (has(object.spec.params) && has(oldObject.spec.params) && object.spec.params == oldObject.spec.params))`,
message: "PaC source revision parameters are immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${serviceAccountEqual()})`,
message: "PaC execution ServiceAccount is immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${pipelineRefEqual()})`,
message: "PaC referenced Pipeline identity is immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || object.spec == oldObject.spec`,
message: "admission-proven PaC PipelineRun spec is immutable, including embedded pipelineSpec, workspaces, and timeouts",
reason: "Invalid",
},
...consumers.flatMap((consumer) => [{
expression: `request.operation != 'CREATE' || !(${candidateExpression(consumer, "object")}) || (request.userInfo.username == ${cel(contract.controllerUsername)} && (${markerPresent}) && object.metadata.annotations[${cel(annotation)}] == ${cel(consumer.markerValue)} && ${usesServiceAccount(consumer, "object")})`,
message: `PipelineRun candidates for ${consumer.id} require its exact admission marker and execution ServiceAccount`,
reason: "Invalid",
}, {
expression: `!(${usesServiceAccount(consumer, "object")}) || ((${candidateExpression(consumer, "object")}) && (${markerPresent}) && object.metadata.annotations[${cel(annotation)}] == ${cel(consumer.markerValue)})`,
message: `execution ServiceAccount for ${consumer.id} is reserved to its admission-proven PipelineRun candidates`,
reason: "Forbidden",
}]),
];
const policySpec = {
failurePolicy: contract.failurePolicy,
matchConstraints: {
matchPolicy: "Equivalent",
resourceRules: [{ apiGroups: ["tekton.dev"], apiVersions: ["v1"], operations: ["CREATE", "UPDATE"], resources: ["pipelineruns"] }],
},
validations,
};
const bindingSpec = { policyName: contract.policyName, validationActions: contract.validationActions };
const policyHash = admissionSpecSha256(targetId, contract.policyName, contract.bindingName, policySpec, bindingSpec);
const metadata = {
labels: { "app.kubernetes.io/managed-by": "unidesk", "app.kubernetes.io/part-of": "platform-infra" },
annotations: {
"argocd.argoproj.io/sync-wave": "0",
"unidesk.ai/pac-provenance-version": contract.version,
"unidesk.ai/pac-controller-identity": contract.controllerUsername,
"unidesk.ai/owning-config-ref": `${configPath}#deliveryProvenance`,
"unidesk.ai/effective-config-sha256": policyHash,
},
};
const policy = {
apiVersion: "admissionregistration.k8s.io/v1",
kind: "ValidatingAdmissionPolicy",
metadata: { name: contract.policyName, ...metadata },
spec: policySpec,
};
const binding = {
apiVersion: "admissionregistration.k8s.io/v1",
kind: "ValidatingAdmissionPolicyBinding",
metadata: {
name: contract.bindingName,
...metadata,
annotations: { ...metadata.annotations, "argocd.argoproj.io/sync-wave": "1" },
},
spec: bindingSpec,
};
return [policy, binding].map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n") + "\n";
}
export function pacAdmissionDesiredIdentity(targetId: string): PacAdmissionDesiredIdentity {
const contract = readPacAdmissionContract();
const manifest = renderPacAdmissionDesiredFragment(targetId);
const policy = manifest.length === 0 ? null : record(Bun.YAML.parse(manifest.split(/^---\s*$/mu)[0] ?? ""), "rendered admission policy");
const policyMetadata = policy === null ? {} : record(policy.metadata, "rendered admission policy metadata");
const annotations = policy === null ? {} : record(policyMetadata.annotations, "rendered admission policy annotations");
return {
configSha256: policy === null
? `sha256:${createHash("sha256").update(stableCanonicalJson({ targetId, disabled: true })).digest("hex")}`
: required(annotations["unidesk.ai/effective-config-sha256"], "rendered admission policy effective config sha256"),
policyName: contract.policyName,
bindingName: contract.bindingName,
version: contract.version,
controllerIdentity: contract.controllerUsername,
};
}
function admissionSpecSha256(targetId: string, policyName: string, bindingName: string, policySpec: Record<string, unknown>, bindingSpec: Record<string, unknown>): string {
const input = { targetId, policyName, bindingName, policySpec, bindingSpec };
return `sha256:${createHash("sha256").update(stableCanonicalJson(input)).digest("hex")}`;
}
function stableCanonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stableCanonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
return `{${Object.entries(value as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableCanonicalJson(item)}`).join(",")}}`;
}
return JSON.stringify(value);
}
function candidateExpression(consumer: PacAdmissionConsumerContract, root: "object" | "oldObject"): string {
return `${root}.metadata.namespace == ${cel(consumer.namespace)} && (${root}.metadata.name.startsWith(${cel(consumer.pipelineRunPrefix)}) || (has(${root}.metadata.labels) && ((${cel("tekton.dev/pipeline")} in ${root}.metadata.labels && ${root}.metadata.labels[${cel("tekton.dev/pipeline")}] == ${cel(consumer.pipeline)}) || (${cel("tekton.dev/pipelineName")} in ${root}.metadata.labels && ${root}.metadata.labels[${cel("tekton.dev/pipelineName")}] == ${cel(consumer.pipeline)}))) || (has(${root}.spec.pipelineRef) && has(${root}.spec.pipelineRef.name) && ${root}.spec.pipelineRef.name == ${cel(consumer.pipeline)}))`;
}
function usesServiceAccount(consumer: PacAdmissionConsumerContract, root: "object" | "oldObject"): string {
return `has(${root}.spec.taskRunTemplate) && has(${root}.spec.taskRunTemplate.serviceAccountName) && ${root}.spec.taskRunTemplate.serviceAccountName == ${cel(consumer.executionServiceAccountName)}`;
}
function metadataEntryEqual(map: "annotations" | "labels", key: string): string {
const current = `has(object.metadata.${map}) && ${cel(key)} in object.metadata.${map}`;
const previous = `has(oldObject.metadata.${map}) && ${cel(key)} in oldObject.metadata.${map}`;
return `((${current}) == (${previous}) && (!(${current}) || object.metadata.${map}[${cel(key)}] == oldObject.metadata.${map}[${cel(key)}]))`;
}
function serviceAccountEqual(): string {
const current = "has(object.spec.taskRunTemplate) && has(object.spec.taskRunTemplate.serviceAccountName)";
const previous = "has(oldObject.spec.taskRunTemplate) && has(oldObject.spec.taskRunTemplate.serviceAccountName)";
return `((${current}) == (${previous}) && (!(${current}) || object.spec.taskRunTemplate.serviceAccountName == oldObject.spec.taskRunTemplate.serviceAccountName))`;
}
function pipelineRefEqual(): string {
const current = "has(object.spec.pipelineRef)";
const previous = "has(oldObject.spec.pipelineRef)";
return `((${current}) == (${previous}) && (!(${current}) || object.spec.pipelineRef == oldObject.spec.pipelineRef))`;
}
function cel(value: string): string {
return JSON.stringify(value);
}
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function recordArray(value: unknown, path: string): Record<string, unknown>[] {
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => record(item, `${path}[${index}]`));
}
function required(value: unknown, path: string): string {
if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${path} must be a non-empty single-line string`);
return value;
}
function stringArray(value: unknown, path: string): string[] {
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path} must be a non-empty string array`);
return value as string[];
}
function literal<T extends string | boolean>(value: unknown, path: string, expected: T): T {
if (value !== expected) throw new Error(`${path} must be ${expected}`);
return expected;
}