157 lines
6.6 KiB
JavaScript
157 lines
6.6 KiB
JavaScript
"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.PluggableAuthHandler = void 0;
|
|
const pluggable_auth_client_1 = require("./pluggable-auth-client");
|
|
const executable_response_1 = require("./executable-response");
|
|
const childProcess = require("child_process");
|
|
const fs = require("fs");
|
|
/**
|
|
* A handler used to retrieve 3rd party token responses from user defined
|
|
* executables and cached file output for the PluggableAuthClient class.
|
|
*/
|
|
class PluggableAuthHandler {
|
|
/**
|
|
* Instantiates a PluggableAuthHandler instance using the provided
|
|
* PluggableAuthHandlerOptions object.
|
|
*/
|
|
constructor(options) {
|
|
if (!options.command) {
|
|
throw new Error('No command provided.');
|
|
}
|
|
this.commandComponents = PluggableAuthHandler.parseCommand(options.command);
|
|
this.timeoutMillis = options.timeoutMillis;
|
|
if (!this.timeoutMillis) {
|
|
throw new Error('No timeoutMillis provided.');
|
|
}
|
|
this.outputFile = options.outputFile;
|
|
}
|
|
/**
|
|
* Calls user provided executable to get a 3rd party subject token and
|
|
* returns the response.
|
|
* @param envMap a Map of additional Environment Variables required for
|
|
* the executable.
|
|
* @return A promise that resolves with the executable response.
|
|
*/
|
|
retrieveResponseFromExecutable(envMap) {
|
|
return new Promise((resolve, reject) => {
|
|
// Spawn process to run executable using added environment variables.
|
|
const child = childProcess.spawn(this.commandComponents[0], this.commandComponents.slice(1), {
|
|
env: { ...process.env, ...Object.fromEntries(envMap) },
|
|
});
|
|
let output = '';
|
|
// Append stdout to output as executable runs.
|
|
child.stdout.on('data', (data) => {
|
|
output += data;
|
|
});
|
|
// Append stderr as executable runs.
|
|
child.stderr.on('data', (err) => {
|
|
output += err;
|
|
});
|
|
// Set up a timeout to end the child process and throw an error.
|
|
const timeout = setTimeout(() => {
|
|
// Kill child process and remove listeners so 'close' event doesn't get
|
|
// read after child process is killed.
|
|
child.removeAllListeners();
|
|
child.kill();
|
|
return reject(new Error('The executable failed to finish within the timeout specified.'));
|
|
}, this.timeoutMillis);
|
|
child.on('close', (code) => {
|
|
// Cancel timeout if executable closes before timeout is reached.
|
|
clearTimeout(timeout);
|
|
if (code === 0) {
|
|
// If the executable completed successfully, try to return the parsed response.
|
|
try {
|
|
const responseJson = JSON.parse(output);
|
|
const response = new executable_response_1.ExecutableResponse(responseJson);
|
|
return resolve(response);
|
|
}
|
|
catch (error) {
|
|
if (error instanceof executable_response_1.ExecutableResponseError) {
|
|
return reject(error);
|
|
}
|
|
return reject(new executable_response_1.ExecutableResponseError(`The executable returned an invalid response: ${output}`));
|
|
}
|
|
}
|
|
else {
|
|
return reject(new pluggable_auth_client_1.ExecutableError(output, code.toString()));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Checks user provided output file for response from previous run of
|
|
* executable and return the response if it exists, is formatted correctly, and is not expired.
|
|
*/
|
|
async retrieveCachedResponse() {
|
|
if (!this.outputFile || this.outputFile.length === 0) {
|
|
return undefined;
|
|
}
|
|
let filePath;
|
|
try {
|
|
filePath = await fs.promises.realpath(this.outputFile);
|
|
}
|
|
catch (_a) {
|
|
// If file path cannot be resolved, return undefined.
|
|
return undefined;
|
|
}
|
|
if (!(await fs.promises.lstat(filePath)).isFile()) {
|
|
// If path does not lead to file, return undefined.
|
|
return undefined;
|
|
}
|
|
const responseString = await fs.promises.readFile(filePath, {
|
|
encoding: 'utf8',
|
|
});
|
|
if (responseString === '') {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const responseJson = JSON.parse(responseString);
|
|
const response = new executable_response_1.ExecutableResponse(responseJson);
|
|
// Check if response is successful and unexpired.
|
|
if (response.isValid()) {
|
|
return new executable_response_1.ExecutableResponse(responseJson);
|
|
}
|
|
return undefined;
|
|
}
|
|
catch (error) {
|
|
if (error instanceof executable_response_1.ExecutableResponseError) {
|
|
throw error;
|
|
}
|
|
throw new executable_response_1.ExecutableResponseError(`The output file contained an invalid response: ${responseString}`);
|
|
}
|
|
}
|
|
/**
|
|
* Parses given command string into component array, splitting on spaces unless
|
|
* spaces are between quotation marks.
|
|
*/
|
|
static parseCommand(command) {
|
|
// Split the command into components by splitting on spaces,
|
|
// unless spaces are contained in quotation marks.
|
|
const components = command.match(/(?:[^\s"]+|"[^"]*")+/g);
|
|
if (!components) {
|
|
throw new Error(`Provided command: "${command}" could not be parsed.`);
|
|
}
|
|
// Remove quotation marks from the beginning and end of each component if they are present.
|
|
for (let i = 0; i < components.length; i++) {
|
|
if (components[i][0] === '"' && components[i].slice(-1) === '"') {
|
|
components[i] = components[i].slice(1, -1);
|
|
}
|
|
}
|
|
return components;
|
|
}
|
|
}
|
|
exports.PluggableAuthHandler = PluggableAuthHandler;
|