UNPKG

@onboardbase/cli

Version:

[![Version](https://img.shields.io/npm/v/@onboardbase/cli.svg)](https://www.npmjs.com/package/@onboardbase/cli) [![Downloads/week](https://img.shields.io/npm/dw/@onboardbase/cli.svg)](https://www.npmjs.com/package/@onboardbase/cli) [![License](https://img

310 lines (309 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SecretsService = void 0; const fs_1 = require("fs"); const base_service_1 = require("../../common/base.service"); const crypto_1 = require("../../common/utils/crypto"); const access_manager_1 = require("../access-manager"); const path_1 = require("path"); const machineId_1 = require("../../common/utils/machineId"); const json_1 = require("../../common/utils/json"); const chalk = require("chalk"); const safelyGetFileContent_1 = require("../../common/utils/safelyGetFileContent"); const convertEnvFileToJSON_1 = require("../../common/utils/convertEnvFileToJSON"); const errors_1 = require("../../common/errors"); const messages = { errInvalidCommand: "Please specify a command to execute", errInvalidSourcePath: "--source-path is not valid or the file does not exists.", errInvalidUserSecrets: "--secrets contains an invalid JSON content", errNoSecretsToInject: "no secrets to inject to start your command, enable --source or --sync options", }; class SecretsService extends base_service_1.BaseService { constructor(configManager) { super(configManager); this.CACHE_FILE_LIFESPAN = 86400000; // ONE DAY this.accessManager = new access_manager_1.AccessManager(configManager); } clearCache(project, environment) { try { const cacheFilePath = this._getCacheFilePath(project, environment); (0, fs_1.unlinkSync)(cacheFilePath); } catch (error) { } } async pullFromRemote(opts) { var _a, _b, _c; const authHandshakeResult = await this.accessManager.getAuthInfoFromDeviceToken(opts.token); const secrets = await this._httpGetSecrets(authHandshakeResult.accessToken, opts.project, opts.environment); const exposedSecrets = await this._decryptValueRemoteSecrets(secrets, authHandshakeResult.secretKey); const cliSecrets = this._constructCLISecretFromRemoteSecret(exposedSecrets); return { secrets: cliSecrets, project: (_a = secrets[0]) === null || _a === void 0 ? void 0 : _a.project, environment: (_b = secrets[0]) === null || _b === void 0 ? void 0 : _b.environment, totalSecrets: (_c = secrets === null || secrets === void 0 ? void 0 : secrets.length) !== null && _c !== void 0 ? _c : 0, }; } _constructCLISecretFromRemoteSecret(remoteSecrets) { return remoteSecrets.reduce((acc, remoteSecret) => { const secret = {}; secret[remoteSecret.key] = remoteSecret.value; return Object.assign(acc, secret); }, {}); } async _httpGetSecrets(accessToken, project, environment) { return this.httpInstance.getSecrets({ accessToken: accessToken, project: project, environment, }); } async saveToCache({ secrets, project, environment, secretVersion, }) { if (!Object.keys(secrets).length) return; const cacheFilePath = this._getCacheFilePath(project, environment); await this._encryptSecretsAndCache(secrets, secretVersion, cacheFilePath); } _getCacheFilePath(project, environment) { const cacheDir = this.configManager.getCacheDir(); const cacheFile = `${project}_${environment}`; const cacheFilePath = (0, path_1.join)(cacheDir, cacheFile); return cacheFilePath; } async _encryptSecretsAndCache(secrets, secretVersion, cacheFilePath) { secrets["OBB_UPSTREAM_VERSION"] = secretVersion; const encryptedSecrets = await this._encryptSecrets(secrets); this._saveEncryptedSecrets({ encryptedSecrets, cacheFilePath }); } _saveEncryptedSecrets(data) { const { encryptedSecrets, cacheFilePath } = data; (0, fs_1.writeFileSync)(cacheFilePath, encryptedSecrets); } async _encryptSecrets(secrets) { const encryptedSecrets = await (0, crypto_1.encryptPlainText)({ plainText: JSON.stringify(secrets), passphrase: machineId_1.MACHINE_ID, }); return encryptedSecrets; } async _checkIfEnvironmentVersionDiffers({ cliToken, project, environment, version, }) { const { accessToken } = await this.accessManager.getAuthInfoFromDeviceToken(cliToken); const remoteVersion = await this.httpInstance.getEnvironmentVersion({ accessToken, project, environment, }); // call secret version api return version !== remoteVersion; } async getSecrets(cliToken, project, environment, forcePull) { const cachedSecrets = await this.getSecretsFromCache(project, environment); const version = this.configManager.getFromGlobal(`versions.${project}_${environment}`); console.log("Checking for update..."); // checks if version exists in config if not returns true // checks if version found in config matches remote if not returns true let shouldPullFromRemote = await this._checkIfEnvironmentVersionDiffers({ cliToken, project, environment, version, }); if (forcePull) shouldPullFromRemote = true; if (!shouldPullFromRemote && cachedSecrets) { console.log("No remote change on secrets. Running with local cache"); return cachedSecrets; } console.log("Pulling new secret updates..."); return this.pullFromRemoteAndCache({ token: cliToken, projectTitle: project, environmentTitle: environment, }); } async pullFromRemoteAndCache({ token, projectTitle, environmentTitle, }) { const { secrets, environment, totalSecrets } = await this.pullFromRemote({ token, project: projectTitle, environment: environmentTitle, }); await this.saveToCache({ secrets, project: projectTitle, environment: environmentTitle, secretVersion: (environment === null || environment === void 0 ? void 0 : environment.secretVersion) || -1, }); // we need to store the version to cache this.configManager.setGlobal(`versions.${projectTitle}_${environmentTitle}`, (environment === null || environment === void 0 ? void 0 : environment.secretVersion) || -1); // we need to gracefully store the environment id environment && this.setScopeConfig(`environment-id`, environment === null || environment === void 0 ? void 0 : environment.id); environment && this.setScopeConfig(`environment-name`, environmentTitle); // eventually set this project name as the last successful project // for this scope if secrets were pulled from the API // an empty secret is not counted as a successfull pull if (totalSecrets) this.setScopeConfig(`project-name`, projectTitle); return secrets; } async _decryptValueRemoteSecrets(secrets, secretKey) { return Promise.all(secrets.map(async (secret) => { return { key: await (0, crypto_1.decryptCipherText)({ cipherText: secret.key, passphrase: secretKey, }), value: await (0, crypto_1.decryptCipherText)({ cipherText: secret.value, passphrase: secretKey, }), }; })); } deleteCacheIfExpired(project, environment) { try { const fallbackFile = this._getCacheFilePath(project, environment); const fileStats = (0, fs_1.statSync)(fallbackFile); //statSync can throw, but we don't want to catch the error as the error is not useful, regardless what is it. const now = new Date().getTime(); const fileCreatedTime = new Date(fileStats.ctime).getTime(); const fileExpiryTime = fileCreatedTime + this.CACHE_FILE_LIFESPAN; if (now > fileExpiryTime) { (0, fs_1.unlinkSync)(fallbackFile); } } catch (error) { } } _getOldCachefilePath(filename) { return (0, path_1.join)(this.configManager.getOldConfigDir(), "fallback", filename); } _getCacheContent(project, environment) { const filename = `${project}_${environment}`; const oldCacheFile = this._getOldCachefilePath(filename); const content = (0, safelyGetFileContent_1.safelyGetFileContent)(oldCacheFile); if (content) { (0, fs_1.writeFileSync)(`${oldCacheFile}.bak`, content); (0, fs_1.unlinkSync)(oldCacheFile); return content; } const newCacheFile = (0, path_1.join)(this.configManager.getCacheDir(), filename); return (0, safelyGetFileContent_1.safelyGetFileContent)(newCacheFile); } async getSecretsFromCache(project, environment) { const cacheContent = this._getCacheContent(project, environment); if (!cacheContent) return; const plainSecrets = await (0, crypto_1.decryptCipherText)({ cipherText: cacheContent, passphrase: machineId_1.MACHINE_ID, }); return (0, json_1.safelyJSONParse)(plainSecrets); } getSecretsFromSourcePath(sourcePath) { // try { let content = ""; // if sourcePath is "-", the intention is to read from STDIN if (sourcePath === "-") { content = this.getJSONEnvsFromPath(0); } else { content = this.getJSONEnvsFromPath(sourcePath); } // we assume that it is coming from a remote source or local source let parsedJSON = (0, json_1.safelyJSONParse)(content); if (!parsedJSON) parsedJSON = (0, convertEnvFileToJSON_1.convertEnvFileToJSON)(content); return parsedJSON; } parseUserGeneratedSecrets(secrets) { const envs = (0, json_1.safelyJSONParse)(secrets); if (!envs) throw errors_1.BadInputError.from(messages.errInvalidUserSecrets); return envs; } getJSONEnvsFromPath(path) { try { const content = (0, fs_1.readFileSync)(path, "utf-8"); return content.toString(); } catch (error) { throw errors_1.BadInputError.from(messages.errInvalidSourcePath); } } generateEnvs(envs, prefix = "", verbose = false) { const localSecrets = this.configManager.getFromProject("secrets.local"); this.resolveSecretsConflicts(verbose, envs); const mergedSecrets = Object.assign({}, envs, localSecrets, process.env); return mergedSecrets; } _getProjectSecretConflictWithRemote(projectSecrets, remoteSecrets = {}) { const conflicts = {}; const newSecrets = []; Object.keys(projectSecrets).map((key) => { const value = projectSecrets[key]; if (remoteSecrets[key] === value) { conflicts[key] = value; } else if (remoteSecrets[key] !== value) { newSecrets.push(key); } }); return { conflicts, newSecrets }; } _printRecommendationMessage(secretKeys) { let formattedMessage = "We noticed you have some new secret which have not been added to Onboardbase and suggest you create a recommendation to avoid having sensitive secrets locally.\n\nThe secret keynames are:\n\n"; secretKeys.map((secretKey) => (formattedMessage += `- ${secretKey}\n`)); console.log(formattedMessage); console.log(`Run ${chalk.greenBright("onboardbase mr:create --from-local-secrets")} to upload your local secrets to onboardbase.`); } _printConflictingSecretMessage(secrets) { const secretKeys = Object.keys(secrets); if (secretKeys.length) { console.log(`Duplicate secret found: \n\n`); secretKeys.map((key) => { const value = secrets[key]; console.log(`${chalk.greenBright.bold(`${key}=${value}`)}\n\n`); }); chalk.grey("...removing the secret from your local file"); } } resolveSecretsConflicts(verbose, remoteSecrets = {}) { if (verbose) { const projectSecrets = this.configManager.getFromProject("secrets.local") || {}; const { conflicts, newSecrets } = this._getProjectSecretConflictWithRemote(projectSecrets, remoteSecrets); if (conflicts && Object.keys(conflicts).length) { this._printConflictingSecretMessage(conflicts); this.configManager.setProject("secrets.local", newSecrets.map((key) => { const sec = {}; sec[key] = projectSecrets[key]; return sec; })); } if (verbose && newSecrets.length) { this._printRecommendationMessage(newSecrets); } } } async acknowledgeSecretShare() { // secret logs are shared to the API by a teammate, // fetch it and acknowledge that theyhave been fetched. } async decryptSecretAndParse(encSecret, passphrase) { const decryptedSecret = await (0, crypto_1.decryptCipherText)({ cipherText: encSecret, passphrase, }); const secret = (0, json_1.safelyJSONParse)(decryptedSecret); return secret; } async _processSharedSecrets(shareLogs) { const sharedSecrets = {}; await Promise.all(shareLogs.map(({ id, secrets, senderEmail }) => { secrets.map(async (encSecret) => { const secret = await this.decryptSecretAndParse(encSecret, senderEmail); console.log(chalk.greenBright.bold(secret.key), `was shared with you from ${senderEmail}`); sharedSecrets[secret.key] = secret.value; }); })); return sharedSecrets; } } exports.SecretsService = SecretsService;