@onboardbase/cli
Version:
[](https://www.npmjs.com/package/@onboardbase/cli) [](https://www.npmjs.com/package/@onboardbase/cli) [ • 14.1 kB
JavaScript
;
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;