UNPKG

@atomist/sdm-core

Version:

Atomist Software Delivery Machine - Implementation

246 lines 10.9 kB
"use strict"; /* * Copyright © 2019 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const automation_client_1 = require("@atomist/automation-client"); const sdm_1 = require("@atomist/sdm"); const stringify = require("fast-json-stable-stringify"); const fs = require("fs-extra"); const path = require("path"); const array_1 = require("../../util/misc/array"); const rsaGoalSigning_1 = require("./rsaGoalSigning"); exports.DefaultGoalSigningAlgorithm = rsaGoalSigning_1.RsaGoalSigningAlgorithm; /** * AutomationEventListener that verifies incoming SDM goals against a set of configurable * verification public keys. * * Optionally a private key can be specified to sign outgoing goals. Setting this is strongly * recommended to prevent executing untrusted and/or tampered SDM goals. */ class GoalSigningAutomationEventListener { constructor(gsc) { this.gsc = gsc; this.initVerificationKeys(); } operationStarting(operation) { var _a; if (operation.operationName === "UpdateSdmGoal") { const goal = (_a = operation.variables) === null || _a === void 0 ? void 0 : _a.goal; if (!!goal) { operation.variables.goal = signGoal(goal, this.gsc); } } return operation; } initVerificationKeys() { this.gsc.verificationKeys = array_1.toArray(this.gsc.verificationKeys) || []; // If signing key is set, also use it to verify if (!!this.gsc.signingKey) { this.gsc.verificationKeys.push(this.gsc.signingKey); } // Load the Atomist public key const publicKey = fs.readFileSync(path.join(__dirname, "atomist-public.pem")).toString(); this.gsc.verificationKeys.push({ publicKey, name: "atomist.com/sdm" }); } } exports.GoalSigningAutomationEventListener = GoalSigningAutomationEventListener; /** * Verify a goal signature against the public keys configured in provided Configuration. * If signature can't be verified, the goal will be marked as failed and an Error will be thrown. * @param goal goal to verify * @param gsc signing configuration * @param ctx */ function verifyGoal(goal, gsc, ctx) { return __awaiter(this, void 0, void 0, function* () { if (!!gsc && gsc.enabled === true && !!goal && isInScope(gsc.scope, ctx) && !isGoalRejected(goal)) { if (!!goal.signature) { const message = normalizeGoal(goal); let verifiedWith; for (const key of array_1.toArray(gsc.verificationKeys)) { if (findAlgorithm(key, gsc).verify(message, goal.signature, key)) { verifiedWith = key; break; } } if (!!verifiedWith) { automation_client_1.logger.debug(`Verified signature for incoming goal '${goal.uniqueName}' of '${goal.goalSetId}' with key '${verifiedWith.name}' and algorithm '${verifiedWith.algorithm || exports.DefaultGoalSigningAlgorithm.name}'`); } else { yield rejectGoal("signature invalid", goal, ctx); throw new Error("SDM goal signature invalid. Rejecting goal!"); } } else { yield rejectGoal("signature missing", goal, ctx); throw new Error("SDM goal signature is missing. Rejecting goal!"); } } }); } exports.verifyGoal = verifyGoal; /** * Add a signature to a goal * @param goal * @param gsc */ function signGoal(goal, gsc) { if (!!gsc && gsc.enabled === true && !!gsc.signingKey) { goal.signature = findAlgorithm(gsc.signingKey, gsc).sign(normalizeGoal(goal), gsc.signingKey); automation_client_1.logger.debug(`Signed goal '${goal.uniqueName}' of '${goal.goalSetId}'`); return goal; } else { return goal; } } exports.signGoal = signGoal; function rejectGoal(reason, sdmGoal, ctx) { return __awaiter(this, void 0, void 0, function* () { yield sdm_1.updateGoal(ctx, sdmGoal, { state: sdm_1.SdmGoalState.failure, description: `Rejected: ${sdmGoal.name}`, phase: reason, }); }); } function findAlgorithm(key, gsc) { const algorithm = [...array_1.toArray(gsc.algorithms || []), exports.DefaultGoalSigningAlgorithm] .find(a => a.name.toLowerCase() === (key.algorithm || exports.DefaultGoalSigningAlgorithm.name).toLowerCase()); if (!algorithm) { throw new Error(`Goal signing or verification key '${key.name}' requested algorithm '${key.algorithm}' which isn't configured`); } return algorithm; } function isInScope(scope, ctx) { if (scope === sdm_1.GoalSigningScope.All) { return true; } else if (scope === sdm_1.GoalSigningScope.Fulfillment && ctx.context.operation === "FulfillGoalOnRequested") { return true; } else { return false; } } function isGoalRejected(sdmGoal) { return sdmGoal.state === sdm_1.SdmGoalState.failure && sdmGoal.description === `Rejected: ${sdmGoal.name}`; } function normalizeGoal(goal) { // Create a new goal with only the relevant and sensible fields const newGoal = { uniqueName: normalizeValue(goal.uniqueName), name: normalizeValue(goal.name), environment: normalizeValue(goal.environment), repo: { owner: normalizeValue(goal.repo.owner), name: normalizeValue(goal.repo.name), providerId: normalizeValue(goal.repo.providerId), }, goalSet: normalizeValue(goal.goalSet), registration: normalizeValue(goal.registration), goalSetId: normalizeValue(goal.goalSetId), externalKey: normalizeValue(goal.externalKey), sha: normalizeValue(goal.sha), branch: normalizeValue(goal.branch), state: normalizeValue(goal.state), phase: normalizeValue(goal.phase), version: normalizeValue(goal.version), description: normalizeValue(goal.description), descriptions: !!goal.descriptions ? { planned: normalizeValue(goal.descriptions.planned), requested: normalizeValue(goal.descriptions.requested), inProcess: normalizeValue(goal.descriptions.inProcess), completed: normalizeValue(goal.descriptions.completed), failed: normalizeValue(goal.descriptions.failed), skipped: normalizeValue(goal.descriptions.skipped), canceled: normalizeValue(goal.descriptions.canceled), stopped: normalizeValue(goal.descriptions.stopped), waitingForApproval: normalizeValue(goal.descriptions.waitingForApproval), waitingForPreApproval: normalizeValue(goal.descriptions.waitingForPreApproval), } : undefined, ts: normalizeValue(goal.ts), data: normalizeValue(goal.data), parameters: normalizeValue(goal.parameters), url: normalizeValue(goal.url), externalUrls: !!goal.externalUrls ? goal.externalUrls.map(e => ({ url: normalizeValue(e.url), label: normalizeValue(e.label), })) : [], preApprovalRequired: normalizeValue(goal.preApprovalRequired), preApproval: !!goal.preApproval ? { channelId: normalizeValue(goal.preApproval.channelId), correlationId: normalizeValue(goal.preApproval.correlationId), name: normalizeValue(goal.preApproval.name), registration: normalizeValue(goal.preApproval.registration), ts: normalizeValue(goal.preApproval.ts), userId: normalizeValue(goal.preApproval.userId), version: normalizeValue(goal.preApproval.version), } : undefined, approvalRequired: normalizeValue(goal.approvalRequired), approval: !!goal.approval ? { channelId: normalizeValue(goal.approval.channelId), correlationId: normalizeValue(goal.approval.correlationId), name: normalizeValue(goal.approval.name), registration: normalizeValue(goal.approval.registration), ts: normalizeValue(goal.approval.ts), userId: normalizeValue(goal.approval.userId), version: normalizeValue(goal.approval.version), } : undefined, retryFeasible: normalizeValue(goal.retryFeasible), error: normalizeValue(goal.error), preConditions: !!goal.preConditions ? goal.preConditions.map(c => ({ environment: normalizeValue(c.environment), name: normalizeValue(c.name), uniqueName: normalizeValue(c.uniqueName), })) : [], fulfillment: !!goal.fulfillment ? { method: normalizeValue(goal.fulfillment.method), registration: normalizeValue(goal.fulfillment.registration), name: normalizeValue(goal.fulfillment.name), } : undefined, provenance: !!goal.provenance ? goal.provenance.map(p => ({ channelId: normalizeValue(p.channelId), correlationId: normalizeValue(p.correlationId), name: normalizeValue(p.name), registration: normalizeValue(p.registration), ts: normalizeValue(p.ts), userId: normalizeValue(p.userId), version: normalizeValue(p.version), })) : [], }; return stringify(newGoal); } exports.normalizeGoal = normalizeGoal; function normalizeValue(value) { if (value !== undefined && value !== null) { return value; } else { return undefined; } } //# sourceMappingURL=goalSigning.js.map