"use strict";
/*
 * Copyright © 2020 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeGoal = exports.signGoal = exports.verifyGoal = exports.GoalSigningAutomationEventListener = exports.DefaultGoalSigningAlgorithm = void 0;
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const fs = require("fs-extra");
const stringify = require("json-stable-stringify");
const path = require("path");
const storeGoals_1 = require("../../api-helper/goal/storeGoals");
const SigningKeys_1 = require("../../api/machine/SigningKeys");
const types_1 = require("../../typings/types");
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
 */
async function verifyGoal(goal, gsc, ctx) {
    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) {
                logger_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 {
                await rejectGoal("signature invalid", goal, ctx);
                throw new Error("SDM goal signature invalid. Rejecting goal!");
            }
        }
        else {
            await 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);
        logger_1.logger.debug(`Signed goal '${goal.uniqueName}' of '${goal.goalSetId}'`);
        return goal;
    }
    else {
        return goal;
    }
}
exports.signGoal = signGoal;
async function rejectGoal(reason, sdmGoal, ctx) {
    await storeGoals_1.updateGoal(ctx, sdmGoal, {
        state: types_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 === SigningKeys_1.GoalSigningScope.All) {
        return true;
    }
    else if (scope === SigningKeys_1.GoalSigningScope.Fulfillment &&
        ctx.context.operation === "FulfillGoalOnRequested") {
        return true;
    }
    else {
        return false;
    }
}
function isGoalRejected(sdmGoal) {
    return sdmGoal.state === types_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