"use strict";
// Copyright 2022 Google LLC
//
// 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.PluggableAuthClient = exports.ExecutableError = void 0;
const baseexternalclient_1 = require("./baseexternalclient");
const executable_response_1 = require("./executable-response");
const pluggable_auth_handler_1 = require("./pluggable-auth-handler");
/**
 * Error thrown from the executable run by PluggableAuthClient.
 */
class ExecutableError extends Error {
    constructor(message, code) {
        super(`The executable failed with exit code: ${code} and error message: ${message}.`);
        this.code = code;
        Object.setPrototypeOf(this, new.target.prototype);
    }
}
exports.ExecutableError = ExecutableError;
/**
 * The default executable timeout when none is provided, in milliseconds.
 */
const DEFAULT_EXECUTABLE_TIMEOUT_MILLIS = 30 * 1000;
/**
 * The minimum allowed executable timeout in milliseconds.
 */
const MINIMUM_EXECUTABLE_TIMEOUT_MILLIS = 5 * 1000;
/**
 * The maximum allowed executable timeout in milliseconds.
 */
const MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS = 120 * 1000;
/**
 * The environment variable to check to see if executable can be run.
 * Value must be set to '1' for the executable to run.
 */
const GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = 'GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES';
/**
 * The maximum currently supported executable version.
 */
const MAXIMUM_EXECUTABLE_VERSION = 1;
/**
 * PluggableAuthClient enables the exchange of workload identity pool external credentials for
 * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
 * scripts/executables are completely independent of the Google Cloud Auth libraries. These
 * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
 * to be exchanged for a Google access token.
 *
 * <p>To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
 * must be set to '1'. This is for security reasons.
 *
 * <p>Both OIDC and SAML are supported. The executable must adhere to a specific response format
 * defined below.
 *
 * <p>The executable must print out the 3rd party token to STDOUT in JSON format. When an
 * output_file is specified in the credential configuration, the executable must also handle writing the
 * JSON response to this file.
 *
 * <pre>
 * OIDC response sample:
 * {
 *   "version": 1,
 *   "success": true,
 *   "token_type": "urn:ietf:params:oauth:token-type:id_token",
 *   "id_token": "HEADER.PAYLOAD.SIGNATURE",
 *   "expiration_time": 1620433341
 * }
 *
 * SAML2 response sample:
 * {
 *   "version": 1,
 *   "success": true,
 *   "token_type": "urn:ietf:params:oauth:token-type:saml2",
 *   "saml_response": "...",
 *   "expiration_time": 1620433341
 * }
 *
 * Error response sample:
 * {
 *   "version": 1,
 *   "success": false,
 *   "code": "401",
 *   "message": "Error message."
 * }
 * </pre>
 *
 * <p>The "expiration_time" field in the JSON response is only required for successful
 * responses when an output file was specified in the credential configuration
 *
 * <p>The auth libraries will populate certain environment variables that will be accessible by the
 * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,
 * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and
 * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE.
 *
 * <p>Please see this repositories README for a complete executable request/response specification.
 */
class PluggableAuthClient extends baseexternalclient_1.BaseExternalAccountClient {
    /**
     * Instantiates a PluggableAuthClient instance using the provided JSON
     * object loaded from an external account credentials file.
     * An error is thrown if the credential is not a valid pluggable auth credential.
     * @param options The external account options object typically loaded from
     *   the external account JSON credential file.
     * @param additionalOptions **DEPRECATED, all options are available in the
     *   `options` parameter.** Optional additional behavior customization options.
     *   These currently customize expiration threshold time and whether to retry
     *   on 401/403 API request errors.
     */
    constructor(options, additionalOptions) {
        super(options, additionalOptions);
        if (!options.credential_source.executable) {
            throw new Error('No valid Pluggable Auth "credential_source" provided.');
        }
        this.command = options.credential_source.executable.command;
        if (!this.command) {
            throw new Error('No valid Pluggable Auth "credential_source" provided.');
        }
        // Check if the provided timeout exists and if it is valid.
        if (options.credential_source.executable.timeout_millis === undefined) {
            this.timeoutMillis = DEFAULT_EXECUTABLE_TIMEOUT_MILLIS;
        }
        else {
            this.timeoutMillis = options.credential_source.executable.timeout_millis;
            if (this.timeoutMillis < MINIMUM_EXECUTABLE_TIMEOUT_MILLIS ||
                this.timeoutMillis > MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS) {
                throw new Error(`Timeout must be between ${MINIMUM_EXECUTABLE_TIMEOUT_MILLIS} and ` +
                    `${MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS} milliseconds.`);
            }
        }
        this.outputFile = options.credential_source.executable.output_file;
        this.handler = new pluggable_auth_handler_1.PluggableAuthHandler({
            command: this.command,
            timeoutMillis: this.timeoutMillis,
            outputFile: this.outputFile,
        });
        this.credentialSourceType = 'executable';
    }
    /**
     * Triggered when an external subject token is needed to be exchanged for a
     * GCP access token via GCP STS endpoint.
     * This uses the `options.credential_source` object to figure out how
     * to retrieve the token using the current environment. In this case,
     * this calls a user provided executable which returns the subject token.
     * The logic is summarized as:
     * 1. Validated that the executable is allowed to run. The
     *    GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment must be set to
     *    1 for security reasons.
     * 2. If an output file is specified by the user, check the file location
     *    for a response. If the file exists and contains a valid response,
     *    return the subject token from the file.
     * 3. Call the provided executable and return response.
     * @return A promise that resolves with the external subject token.
     */
    async retrieveSubjectToken() {
        // Check if the executable is allowed to run.
        if (process.env[GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES] !== '1') {
            throw new Error('Pluggable Auth executables need to be explicitly allowed to run by ' +
                'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment ' +
                'Variable to 1.');
        }
        let executableResponse = undefined;
        // Try to get cached executable response from output file.
        if (this.outputFile) {
            executableResponse = await this.handler.retrieveCachedResponse();
        }
        // If no response from output file, call the executable.
        if (!executableResponse) {
            // Set up environment map with required values for the executable.
            const envMap = new Map();
            envMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', this.audience);
            envMap.set('GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', this.subjectTokenType);
            // Always set to 0 because interactive mode is not supported.
            envMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0');
            if (this.outputFile) {
                envMap.set('GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', this.outputFile);
            }
            const serviceAccountEmail = this.getServiceAccountEmail();
            if (serviceAccountEmail) {
                envMap.set('GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL', serviceAccountEmail);
            }
            executableResponse =
                await this.handler.retrieveResponseFromExecutable(envMap);
        }
        if (executableResponse.version > MAXIMUM_EXECUTABLE_VERSION) {
            throw new Error(`Version of executable is not currently supported, maximum supported version is ${MAXIMUM_EXECUTABLE_VERSION}.`);
        }
        // Check that response was successful.
        if (!executableResponse.success) {
            throw new ExecutableError(executableResponse.errorMessage, executableResponse.errorCode);
        }
        // Check that response contains expiration time if output file was specified.
        if (this.outputFile) {
            if (!executableResponse.expirationTime) {
                throw new executable_response_1.InvalidExpirationTimeFieldError('The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.');
            }
        }
        // Check that response is not expired.
        if (executableResponse.isExpired()) {
            throw new Error('Executable response is expired.');
        }
        // Return subject token from response.
        return executableResponse.subjectToken;
    }
}
exports.PluggableAuthClient = PluggableAuthClient;
