From 6574b6489f08ebf5ebdeb16a151fc8d13b22f869 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Mon, 31 Jul 2023 23:24:30 +0530 Subject: [PATCH 01/11] fix: added support for secret import and expansion in integrations --- backend/src/helpers/bot.ts | 134 ++- backend/src/helpers/integration.ts | 111 ++- backend/src/helpers/secrets.ts | 149 ++- backend/src/integrations/sync.ts | 1343 ++++++++++++++-------------- 4 files changed, 963 insertions(+), 774 deletions(-) diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 2252dcc64..cf6d31e4f 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -4,18 +4,20 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8, - generateKeyPair, + generateKeyPair } from "../utils/crypto"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, - SECRET_SHARED, + SECRET_SHARED } from "../variables"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { InternalServerError } from "../utils/errors"; import Folder from "../models/folder"; import { getFolderByPath } from "../services/FolderService"; +import { getAllImportedSecrets } from "../services/SecretImportService"; +import { expandSecrets } from "./secrets"; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -25,7 +27,7 @@ import { getFolderByPath } from "../services/FolderService"; */ export const createBot = async ({ name, - workspaceId, + workspaceId }: { name: string; workspaceId: Types.ObjectId; @@ -36,10 +38,7 @@ export const createBot = async ({ const { publicKey, privateKey } = generateKeyPair(); if (rootEncryptionKey) { - const { ciphertext, iv, tag } = client.encryptSymmetric( - privateKey, - rootEncryptionKey - ); + const { ciphertext, iv, tag } = client.encryptSymmetric(privateKey, rootEncryptionKey); return await new Bot({ name, @@ -50,12 +49,12 @@ export const createBot = async ({ iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64, + keyEncoding: ENCODING_SCHEME_BASE64 }).save(); } else if (encryptionKey) { const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: privateKey, - key: await getEncryptionKey(), + key: await getEncryptionKey() }); return await new Bot({ @@ -67,12 +66,12 @@ export const createBot = async ({ iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }).save(); } throw InternalServerError({ - message: "Failed to create new bot due to missing encryption key", + message: "Failed to create new bot due to missing encryption key" }); }; @@ -82,7 +81,7 @@ export const createBot = async ({ */ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { const botKey = await BotKey.exists({ - workspace: workspaceId, + workspace: workspaceId }); return botKey ? false : true; @@ -98,19 +97,19 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { export const getSecretsBotHelper = async ({ workspaceId, environment, - secretPath, + secretPath }: { workspaceId: Types.ObjectId; environment: string; secretPath: string; }) => { - const content = {} as any; + const content: Record = {}; const key = await getKey({ workspaceId: workspaceId }); let folderId = "root"; const folders = await Folder.findOne({ workspace: workspaceId, - environment, + environment }); if (!folders && secretPath !== "/") { @@ -129,7 +128,43 @@ export const getSecretsBotHelper = async ({ workspace: workspaceId, environment, type: SECRET_SHARED, - folder: folderId, + folder: folderId + }); + + const importedSecrets = await getAllImportedSecrets( + workspaceId.toString(), + environment, + folderId + ); + + importedSecrets.forEach(({ secrets }) => { + secrets.forEach((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + + content[secretKey] = { value: secretValue }; + + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key + }); + content[secretKey].comment = commentValue; + } + }); }); secrets.forEach((secret: ISecret) => { @@ -137,19 +172,31 @@ export const getSecretsBotHelper = async ({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, - key, + key }); const secretValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretValueCiphertext, iv: secret.secretValueIV, tag: secret.secretValueTag, - key, + key }); - content[secretKey] = secretValue; + content[secretKey] = { value: secretValue }; + + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key + }); + content[secretKey].comment = commentValue; + } }); + await expandSecrets(workspaceId.toString(), key, content); + return content; }; @@ -160,22 +207,18 @@ export const getSecretsBotHelper = async ({ * @param {String} obj.workspaceId - id of workspace * @returns {String} key - decrypted workspace key */ -export const getKey = async ({ - workspaceId, -}: { - workspaceId: Types.ObjectId; -}) => { +export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); const botKey = await BotKey.findOne({ - workspace: workspaceId, + workspace: workspaceId }).populate<{ sender: IUser }>("sender", "publicKey"); if (!botKey) throw new Error("Failed to find bot key"); const bot = await Bot.findOne({ - workspace: workspaceId, + workspace: workspaceId }).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding"); if (!bot) throw new Error("Failed to find bot"); @@ -194,7 +237,7 @@ export const getKey = async ({ ciphertext: botKey.encryptedKey, nonce: botKey.nonce, publicKey: botKey.sender.publicKey as string, - privateKey: privateKeyBot, + privateKey: privateKeyBot }); } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { // case: encoding scheme is utf8 @@ -202,20 +245,19 @@ export const getKey = async ({ ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: encryptionKey, + key: encryptionKey }); return decryptAsymmetric({ ciphertext: botKey.encryptedKey, nonce: botKey.nonce, publicKey: botKey.sender.publicKey as string, - privateKey: privateKeyBot, + privateKey: privateKeyBot }); } throw InternalServerError({ - message: - "Failed to obtain bot's copy of workspace key needed for bot operations", + message: "Failed to obtain bot's copy of workspace key needed for bot operations" }); }; @@ -228,7 +270,7 @@ export const getKey = async ({ */ export const encryptSymmetricHelper = async ({ workspaceId, - plaintext, + plaintext }: { workspaceId: Types.ObjectId; plaintext: string; @@ -236,13 +278,13 @@ export const encryptSymmetricHelper = async ({ const key = await getKey({ workspaceId: workspaceId }); const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext, - key, + key }); return { ciphertext, iv, - tag, + tag }; }; /** @@ -258,7 +300,7 @@ export const decryptSymmetricHelper = async ({ workspaceId, ciphertext, iv, - tag, + tag }: { workspaceId: Types.ObjectId; ciphertext: string; @@ -270,7 +312,7 @@ export const decryptSymmetricHelper = async ({ ciphertext, iv, tag, - key, + key }); return plaintext; @@ -281,24 +323,24 @@ export const decryptSymmetricHelper = async ({ * and [envionment] using bot * @param {Object} obj * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.environment - environment + * @param {String} obj.environment - environment */ export const getSecretsCommentBotHelper = async ({ workspaceId, environment, secretPath -} : { +}: { workspaceId: Types.ObjectId; environment: string; secretPath: string; }) => { const content = {} as any; const key = await getKey({ workspaceId: workspaceId }); - + let folderId = "root"; const folders = await Folder.findOne({ workspace: workspaceId, - environment, + environment }); if (!folders && secretPath !== "/") { @@ -317,23 +359,23 @@ export const getSecretsCommentBotHelper = async ({ workspace: workspaceId, environment, type: SECRET_SHARED, - folder: folderId, + folder: folderId }); secrets.forEach((secret: ISecret) => { - if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, - key, + key }); - + const commentValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretCommentCiphertext, iv: secret.secretCommentIV, tag: secret.secretCommentTag, - key, + key }); content[secretKey] = commentValue; @@ -341,4 +383,4 @@ export const getSecretsCommentBotHelper = async ({ }); return content; -} \ No newline at end of file +}; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 196078533..ea196c472 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -6,7 +6,7 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_NETLIFY, - INTEGRATION_VERCEL, + INTEGRATION_VERCEL } from "../variables"; import { UnauthorizedRequestError } from "../utils/errors"; import * as Sentry from "@sentry/node"; @@ -34,7 +34,7 @@ export const handleOAuthExchangeHelper = async ({ workspaceId, integration, code, - environment, + environment }: { workspaceId: string; integration: string; @@ -43,21 +43,20 @@ export const handleOAuthExchangeHelper = async ({ }) => { const bot = await Bot.findOne({ workspace: workspaceId, - isActive: true, + isActive: true }); - if (!bot) - throw new Error("Bot must be enabled for OAuth2 code-token exchange"); + if (!bot) throw new Error("Bot must be enabled for OAuth2 code-token exchange"); // exchange code for access and refresh tokens const res = await exchangeCode({ integration, - code, + code }); const update: Update = { workspace: workspaceId, - integration, + integration }; switch (integration) { @@ -72,12 +71,12 @@ export const handleOAuthExchangeHelper = async ({ const integrationAuth = await IntegrationAuth.findOneAndUpdate( { workspace: workspaceId, - integration, + integration }, update, { new: true, - upsert: true, + upsert: true } ); @@ -86,7 +85,7 @@ export const handleOAuthExchangeHelper = async ({ // set integration auth refresh token await setIntegrationAuthRefreshHelper({ integrationAuthId: integrationAuth._id.toString(), - refreshToken: res.refreshToken, + refreshToken: res.refreshToken }); } @@ -97,7 +96,7 @@ export const handleOAuthExchangeHelper = async ({ integrationAuthId: integrationAuth._id.toString(), accessId: null, accessToken: res.accessToken, - accessExpiresAt: res.accessExpiresAt, + accessExpiresAt: res.accessExpiresAt }); } @@ -111,7 +110,7 @@ export const handleOAuthExchangeHelper = async ({ */ export const syncIntegrationsHelper = async ({ workspaceId, - environment, + environment }: { workspaceId: Types.ObjectId; environment?: string; @@ -121,11 +120,11 @@ export const syncIntegrationsHelper = async ({ workspace: workspaceId, ...(environment ? { - environment, - } - : {}), + environment + } + : {}), isActive: true, - app: { $ne: null }, + app: { $ne: null } }); // for each workspace integration, sync/push secrets @@ -135,25 +134,16 @@ export const syncIntegrationsHelper = async ({ const secrets = await BotService.getSecrets({ workspaceId: integration.workspace, environment: integration.environment, - secretPath: integration.secretPath, + secretPath: integration.secretPath }); - // get workspace, environment (shared) secrets comments - const secretComments = await BotService.getSecretComments({ - workspaceId: integration.workspace, - environment: integration.environment, - secretPath: integration.secretPath, - }) - - const integrationAuth = await IntegrationAuth.findById( - integration.integrationAuth - ); + const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); if (!integrationAuth) throw new Error("Failed to find integration auth"); - + // get integration auth access token const access = await getIntegrationAuthAccessHelper({ - integrationAuthId: integration.integrationAuth, + integrationAuthId: integration.integrationAuth }); // sync secrets to integration @@ -162,14 +152,17 @@ export const syncIntegrationsHelper = async ({ integrationAuth, secrets, accessId: access.accessId === undefined ? null : access.accessId, - accessToken: access.accessToken, - secretComments + accessToken: access.accessToken }); } } catch (err) { Sentry.captureException(err); - console.log(`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, err) // eslint-disable-line no-use-before-define - throw err + // eslint-disable-next-line + console.log( + `syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, + err + ); // eslint-disable-line no-use-before-define + throw err; } }; @@ -182,24 +175,24 @@ export const syncIntegrationsHelper = async ({ * @param {String} refreshToken - decrypted refresh token */ export const getIntegrationAuthRefreshHelper = async ({ - integrationAuthId, + integrationAuthId }: { integrationAuthId: Types.ObjectId; }) => { - const integrationAuth = await IntegrationAuth.findById( - integrationAuthId - ).select("+refreshCiphertext +refreshIV +refreshTag"); + const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( + "+refreshCiphertext +refreshIV +refreshTag" + ); if (!integrationAuth) throw UnauthorizedRequestError({ - message: "Failed to locate Integration Authentication credentials", + message: "Failed to locate Integration Authentication credentials" }); const refreshToken = await BotService.decryptSymmetric({ workspaceId: integrationAuth.workspace, ciphertext: integrationAuth.refreshCiphertext as string, iv: integrationAuth.refreshIV as string, - tag: integrationAuth.refreshTag as string, + tag: integrationAuth.refreshTag as string }); return refreshToken; @@ -214,28 +207,26 @@ export const getIntegrationAuthRefreshHelper = async ({ * @returns {String} accessToken - decrypted access token */ export const getIntegrationAuthAccessHelper = async ({ - integrationAuthId, + integrationAuthId }: { integrationAuthId: Types.ObjectId; }) => { let accessId; let accessToken; - const integrationAuth = await IntegrationAuth.findById( - integrationAuthId - ).select( + const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag" ); if (!integrationAuth) throw UnauthorizedRequestError({ - message: "Failed to locate Integration Authentication credentials", + message: "Failed to locate Integration Authentication credentials" }); accessToken = await BotService.decryptSymmetric({ workspaceId: integrationAuth.workspace, ciphertext: integrationAuth.accessCiphertext as string, iv: integrationAuth.accessIV as string, - tag: integrationAuth.accessTag as string, + tag: integrationAuth.accessTag as string }); if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { @@ -245,11 +236,11 @@ export const getIntegrationAuthAccessHelper = async ({ if (integrationAuth.accessExpiresAt < new Date()) { // access token is expired const refreshToken = await getIntegrationAuthRefreshHelper({ - integrationAuthId, + integrationAuthId }); accessToken = await exchangeRefresh({ integrationAuth, - refreshToken, + refreshToken }); } } @@ -263,13 +254,13 @@ export const getIntegrationAuthAccessHelper = async ({ workspaceId: integrationAuth.workspace, ciphertext: integrationAuth.accessIdCiphertext as string, iv: integrationAuth.accessIdIV as string, - tag: integrationAuth.accessIdTag as string, + tag: integrationAuth.accessIdTag as string }); } return { accessId, - accessToken, + accessToken }; }; @@ -283,7 +274,7 @@ export const getIntegrationAuthAccessHelper = async ({ */ export const setIntegrationAuthRefreshHelper = async ({ integrationAuthId, - refreshToken, + refreshToken }: { integrationAuthId: string; refreshToken: string; @@ -294,22 +285,22 @@ export const setIntegrationAuthRefreshHelper = async ({ const obj = await BotService.encryptSymmetric({ workspaceId: integrationAuth.workspace, - plaintext: refreshToken, + plaintext: refreshToken }); integrationAuth = await IntegrationAuth.findOneAndUpdate( { - _id: integrationAuthId, + _id: integrationAuthId }, { refreshCiphertext: obj.ciphertext, refreshIV: obj.iv, refreshTag: obj.tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }, { - new: true, + new: true } ); @@ -329,7 +320,7 @@ export const setIntegrationAuthAccessHelper = async ({ integrationAuthId, accessId, accessToken, - accessExpiresAt, + accessExpiresAt }: { integrationAuthId: string; accessId: string | null; @@ -342,20 +333,20 @@ export const setIntegrationAuthAccessHelper = async ({ const encryptedAccessTokenObj = await BotService.encryptSymmetric({ workspaceId: integrationAuth.workspace, - plaintext: accessToken, + plaintext: accessToken }); let encryptedAccessIdObj; if (accessId) { encryptedAccessIdObj = await BotService.encryptSymmetric({ workspaceId: integrationAuth.workspace, - plaintext: accessId, + plaintext: accessId }); } integrationAuth = await IntegrationAuth.findOneAndUpdate( { - _id: integrationAuthId, + _id: integrationAuthId }, { accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, @@ -366,10 +357,10 @@ export const setIntegrationAuthAccessHelper = async ({ accessTag: encryptedAccessTokenObj.tag, accessExpiresAt, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }, { - new: true, + new: true } ); diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 04275ed32..818fdf85b 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -42,9 +42,10 @@ import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { EELogService, EESecretService } from "../ee/services"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; -import { getFolderIdFromServiceToken } from "../services/FolderService"; +import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; import path from "path"; +import Folder, { TFolderRootSchema } from "../models/folder"; export const isValidScope = ( authPayload: IServiceTokenData, @@ -64,10 +65,9 @@ export const isValidScope = ( export function containsGlobPatterns(secretPath: string) { const globChars = ["*", "?", "[", "]", "{", "}", "**"]; const normalizedPath = path.normalize(secretPath); - return globChars.some(char => normalizedPath.includes(char)); + return globChars.some((char) => normalizedPath.includes(char)); } - /** * Returns an object containing secret [secret] but with its value, key, comment decrypted. * @@ -929,3 +929,146 @@ export const deleteSecretHelper = async ({ secret }; }; + +const fetchSecretsCrossEnv = (workspaceId: string, folders: TFolderRootSchema[], key: string) => { + const fetchCache: Record> = {}; + + return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => { + const secRefPathUrl = path.join("/", ...secRefPath); + const uniqKey = `${secRefEnv}-${secRefPathUrl}`; + + if (fetchCache?.[uniqKey]) { + return fetchCache[uniqKey][secRefKey]; + } + + let folderId = "root"; + const folder = folders.find(({ environment }) => environment === secRefEnv); + if (!folder && secRefPathUrl !== "/") { + throw BadRequestError({ message: "Folder not found" }); + } + + if (folder) { + const selectedFolder = getFolderByPath(folder.nodes, secRefPathUrl); + if (!selectedFolder) { + throw BadRequestError({ message: "Folder not found" }); + } + folderId = selectedFolder.id; + } + + const secrets = await Secret.find({ + workspace: workspaceId, + environment: secRefEnv, + type: SECRET_SHARED, + folder: folderId + }); + + const decryptedSec = secrets.reduce>((prev, secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key + }); + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + + prev[secretKey] = secretValue; + return prev; + }, {}); + + fetchCache[uniqKey] = decryptedSec; + + return fetchCache[uniqKey][secRefKey]; + }; +}; + +const INTERPOLATION_SYNTAX_REG = new RegExp(/\${([^}]+)}/g); +const recursivelyExpandSecret = async ( + expandedSec: Record, + interpolatedSec: Record, + fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise, + key: string +) => { + if (expandedSec?.[key]) { + return expandedSec[key]; + } + + let interpolatedValue = interpolatedSec[key]; + if (!interpolatedValue) { + throw new Error(`Couldn't find referenced value - ${key}`); + } + + const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG); + if (refs) { + for (const interpolationSyntax of refs) { + const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); + const entities = interpolationKey.trim().split("."); + + if (entities.length === 1) { + const val = await recursivelyExpandSecret( + expandedSec, + interpolatedSec, + fetchCrossEnv, + interpolationKey + ); + if (val) { + interpolatedValue = interpolatedValue.replace(interpolationSyntax, val); + } + return; + } + + if (entities.length > 1) { + const secRefEnv = entities[0]; + const secRefPath = entities.slice(1, entities.length - 1); + const secRefKey = entities[entities.length - 1]; + + const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey); + interpolatedValue = interpolatedValue.replace(interpolationSyntax, val); + } + } + } + + return interpolatedValue; +}; + +export const expandSecrets = async ( + workspaceId: string, + rootEncKey: string, + secrets: Record +) => { + const expandedSec: Record = {}; + const interpolatedSec: Record = {}; + + const folders = await Folder.find({ workspace: workspaceId }); + const crossSecEnvFetch = fetchSecretsCrossEnv(workspaceId, folders, rootEncKey); + + Object.keys(secrets).forEach((key) => { + if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) { + interpolatedSec[key] = secrets[key].value; + } else { + expandedSec[key] = secrets[key].value; + } + }); + + for (const key of Object.keys(secrets)) { + if (expandedSec?.[key]) { + secrets[key].value = expandedSec[key]; + return; + } + + const expandedVal = await recursivelyExpandSecret( + expandedSec, + interpolatedSec, + crossSecEnvFetch, + key + ); + + secrets[key].value = expandedVal || ""; + } + + return secrets; +}; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 28103b57f..e178af833 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -51,7 +51,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, INTEGRATION_WINDMILL, - INTEGRATION_WINDMILL_API_URL, + INTEGRATION_WINDMILL_API_URL } from "../variables"; import AWS from "aws-sdk"; import { Octokit } from "@octokit/rest"; @@ -59,6 +59,12 @@ import _ from "lodash"; import sodium from "libsodium-wrappers"; import { standardRequest } from "../config/request"; +const getSecretKeyValuePair = (secrets: Record) => + Object.keys(secrets).reduce>((prev, key) => { + prev[key] = secrets[key].value; + return prev; + }, {}); + /** * Sync/push [secrets] to [app] in integration named [integration] * @param {Object} obj @@ -74,22 +80,20 @@ const syncSecrets = async ({ integrationAuth, secrets, accessId, - accessToken, - secretComments + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; - secretComments: any; }) => { switch (integration.integration) { case INTEGRATION_AZURE_KEY_VAULT: await syncSecretsAzureKeyVault({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_AWS_PARAMETER_STORE: @@ -97,7 +101,7 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_AWS_SECRET_MANAGER: @@ -105,14 +109,14 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_HEROKU: await syncSecretsHeroku({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_VERCEL: @@ -120,7 +124,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }); break; case INTEGRATION_NETLIFY: @@ -128,49 +132,49 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }); break; case INTEGRATION_GITHUB: await syncSecretsGitHub({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_GITLAB: await syncSecretsGitLab({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_RENDER: await syncSecretsRender({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_RAILWAY: await syncSecretsRailway({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_CIRCLECI: await syncSecretsCircleCI({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_LARAVELFORGE: @@ -178,35 +182,35 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_TRAVISCI: await syncSecretsTravisCI({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_SUPABASE: await syncSecretsSupabase({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_CHECKLY: await syncSecretsCheckly({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_TERRAFORM_CLOUD: await syncSecretsTerraformCloud({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_HASHICORP_VAULT: @@ -215,7 +219,7 @@ const syncSecrets = async ({ integrationAuth, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_CLOUDFLARE_PAGES: @@ -230,21 +234,21 @@ const syncSecrets = async ({ await syncSecretsCodefresh({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_BITBUCKET: await syncSecretsBitBucket({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: await syncSecretsDigitalOceanAppPlatform({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_CLOUD_66: @@ -263,13 +267,12 @@ const syncSecrets = async ({ break; case INTEGRATION_WINDMILL: await syncSecretsWindmill({ - integration, - secrets, - accessToken, - secretComments + integration, + secrets, + accessToken }); break; - } + } }; /** @@ -282,21 +285,21 @@ const syncSecrets = async ({ const syncSecretsAzureKeyVault = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface GetAzureKeyVaultSecret { id: string; // secret URI attributes: { - enabled: true, + enabled: true; created: number; updated: number; recoveryLevel: string; recoverableDays: number; - } + }; } interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { @@ -306,15 +309,15 @@ const syncSecretsAzureKeyVault = async ({ /** * Return all secrets from Azure Key Vault by paginating through URL [url] * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault - * @returns + * @returns */ const paginateAzureKeyVaultSecrets = async (url: string) => { let result: GetAzureKeyVaultSecret[] = []; while (url) { const res = await standardRequest.get(url, { headers: { - Authorization: `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } }); result = result.concat(res.data.value); @@ -323,31 +326,42 @@ const syncSecretsAzureKeyVault = async ({ } return result; - } + }; - const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets( + `${integration.app}/secrets?api-version=7.3` + ); let lastSlashIndex: number; - const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { - if (!lastSlashIndex) { - lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); - } + const res = ( + await Promise.all( + getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + if (!lastSlashIndex) { + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); + } - const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { - headers: { - "Authorization": `Bearer ${accessToken}`, - }, - }); + const azureKeyVaultSecret = await standardRequest.get( + `${getAzureKeyVaultSecret.id}?api-version=7.3`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); - return ({ - ...azureKeyVaultSecret.data, - key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1), - }); - }))) - .reduce((obj: any, secret: any) => ({ + return { + ...azureKeyVaultSecret.data, + key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1) + }; + }) + ) + ).reduce( + (obj: any, secret: any) => ({ ...obj, - [secret.key]: secret, - }), {}); + [secret.key]: secret + }), + {} + ); const setSecrets: { key: string; @@ -360,14 +374,14 @@ const syncSecretsAzureKeyVault = async ({ // case: secret has been created setSecrets.push({ key: hyphenatedKey, - value: secrets[key], + value: secrets[key].value }); } else { if (secrets[key] !== res[hyphenatedKey].value) { // case: secret has been updated setSecrets.push({ key: hyphenatedKey, - value: secrets[key], + value: secrets[key].value }); } } @@ -386,7 +400,7 @@ const syncSecretsAzureKeyVault = async ({ key, value, integration, - accessToken, + accessToken }: { key: string; value: string; @@ -402,36 +416,36 @@ const syncSecretsAzureKeyVault = async ({ await standardRequest.put( `${integration.app}/secrets/${key}?api-version=7.3`, { - value, + value }, { headers: { - Authorization: `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } } ); isSecretSet = true; - } catch (err) { const error: any = err; if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { await standardRequest.post( - `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {}, + `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, + {}, { headers: { - Authorization: `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } } ); - await new Promise(resolve => setTimeout(resolve, 10000)); + await new Promise((resolve) => setTimeout(resolve, 10000)); } else { - await new Promise(resolve => setTimeout(resolve, 10000)); + await new Promise((resolve) => setTimeout(resolve, 10000)); maxTries--; } } } - } + }; // Sync/push set secrets for await (const setSecret of setSecrets) { @@ -440,7 +454,7 @@ const syncSecretsAzureKeyVault = async ({ key, value, integration, - accessToken, + accessToken }); } @@ -448,8 +462,8 @@ const syncSecretsAzureKeyVault = async ({ const { key } = deleteSecret; await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { headers: { - "Authorization": `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } }); } }; @@ -466,10 +480,10 @@ const syncSecretsAWSParameterStore = async ({ integration, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { @@ -478,31 +492,34 @@ const syncSecretsAWSParameterStore = async ({ AWS.config.update({ region: integration.region, accessKeyId: accessId, - secretAccessKey: accessToken, + secretAccessKey: accessToken }); const ssm = new AWS.SSM({ apiVersion: "2014-11-06", - region: integration.region, + region: integration.region }); const params = { Path: integration.path, Recursive: true, - WithDecryption: true, + WithDecryption: true }; - const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters + const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters; let awsParameterStoreSecretsObj: { - [key: string]: any // TODO: fix type + [key: string]: any; // TODO: fix type } = {}; if (parameterList) { - awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ - ...obj, - [secret.Name.split("/").pop()]: secret, - }), {}); + awsParameterStoreSecretsObj = parameterList.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.Name.split("/").pop()]: secret + }), + {} + ); } // Identify secrets to create @@ -510,24 +527,28 @@ const syncSecretsAWSParameterStore = async ({ if (!(key in awsParameterStoreSecretsObj)) { // case: secret does not exist in AWS parameter store // -> create secret - await ssm.putParameter({ - Name: `${integration.path}${key}`, - Type: "SecureString", - Value: secrets[key], - Overwrite: true, - }).promise(); + await ssm + .putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key].value, + Overwrite: true + }) + .promise(); } else { // case: secret exists in AWS parameter store - if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) { + if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { // case: secret value doesn't match one in AWS parameter store // -> update secret - await ssm.putParameter({ - Name: `${integration.path}${key}`, - Type: "SecureString", - Value: secrets[key], - Overwrite: true, - }).promise(); + await ssm + .putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key].value, + Overwrite: true + }) + .promise(); } } }); @@ -535,20 +556,22 @@ const syncSecretsAWSParameterStore = async ({ // Identify secrets to delete Object.keys(awsParameterStoreSecretsObj).map(async (key) => { if (!(key in secrets)) { - // case: + // case: // -> delete secret - await ssm.deleteParameter({ - Name: awsParameterStoreSecretsObj[key].Name, - }).promise(); + await ssm + .deleteParameter({ + Name: awsParameterStoreSecretsObj[key].Name + }) + .promise(); } }); AWS.config.update({ region: undefined, accessKeyId: undefined, - secretAccessKey: undefined, + secretAccessKey: undefined }); -} +}; /** * Sync/push [secrets] to AWS secret manager @@ -562,34 +585,35 @@ const syncSecretsAWSSecretManager = async ({ integration, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { let secretsManager; + const secKeyVal = getSecretKeyValuePair(secrets); try { if (!accessId) return; AWS.config.update({ region: integration.region, accessKeyId: accessId, - secretAccessKey: accessToken, + secretAccessKey: accessToken }); secretsManager = new SecretsManagerClient({ region: integration.region, credentials: { accessKeyId: accessId, - secretAccessKey: accessToken, - }, + secretAccessKey: accessToken + } }); const awsSecretManagerSecret = await secretsManager.send( new GetSecretValueCommand({ - SecretId: integration.app, + SecretId: integration.app }) ); @@ -599,32 +623,36 @@ const syncSecretsAWSSecretManager = async ({ awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); } - if (!_.isEqual(awsSecretManagerSecretObj, secrets)) { - await secretsManager.send(new UpdateSecretCommand({ - SecretId: integration.app, - SecretString: JSON.stringify(secrets), - })); + if (!_.isEqual(awsSecretManagerSecretObj, secKeyVal)) { + await secretsManager.send( + new UpdateSecretCommand({ + SecretId: integration.app, + SecretString: JSON.stringify(secKeyVal) + }) + ); } AWS.config.update({ region: undefined, accessKeyId: undefined, - secretAccessKey: undefined, + secretAccessKey: undefined }); } catch (err) { if (err instanceof ResourceNotFoundException && secretsManager) { - await secretsManager.send(new CreateSecretCommand({ - Name: integration.app, - SecretString: JSON.stringify(secrets), - })); + await secretsManager.send( + new CreateSecretCommand({ + Name: integration.app, + SecretString: JSON.stringify(secKeyVal) + }) + ); } AWS.config.update({ region: undefined, accessKeyId: undefined, - secretAccessKey: undefined, + secretAccessKey: undefined }); } -} +}; /** * Sync/push [secrets] to Heroku app named [integration.app] @@ -636,40 +664,37 @@ const syncSecretsAWSSecretManager = async ({ const syncSecretsHeroku = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { const herokuSecrets = ( - await standardRequest.get( - `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - { - headers: { - Accept: "application/vnd.heroku+json; version=3", - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + await standardRequest.get(`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, { + headers: { + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ) + }) ).data; Object.keys(herokuSecrets).forEach((key) => { if (!(key in secrets)) { - secrets[key] = null; + delete secrets[key]; } }); await standardRequest.patch( `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - secrets, + getSecretKeyValuePair(secrets), { headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }; @@ -684,11 +709,11 @@ const syncSecretsVercel = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessToken: string; }) => { interface VercelSecret { @@ -705,52 +730,54 @@ const syncSecretsVercel = async ({ decrypt: "true", ...(integrationAuth?.teamId ? { - teamId: integrationAuth.teamId, - } - : {}), + teamId: integrationAuth.teamId + } + : {}) }; - const vercelSecrets: VercelSecret[] = (await standardRequest.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - { + const vercelSecrets: VercelSecret[] = ( + await standardRequest.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { params, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } + }) + ).data.envs.filter((secret: VercelSecret) => { + if (!secret.target.includes(integration.targetEnvironment)) { + // case: secret does not have the same target environment + return false; } - )) - .data - .envs - .filter((secret: VercelSecret) => { - if (!secret.target.includes(integration.targetEnvironment)) { - // case: secret does not have the same target environment - return false; - } - if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) { - // case: secret on preview environment does not have same target git branch - return false; - } + if ( + integration.targetEnvironment === "preview" && + integration.path && + integration.path !== secret.gitBranch + ) { + // case: secret on preview environment does not have same target git branch + return false; + } - return true; - }); + return true; + }); const res: { [key: string]: VercelSecret } = {}; for await (const vercelSecret of vercelSecrets) { if (vercelSecret.type === "encrypted") { // case: secret is encrypted -> need to decrypt - const decryptedSecret = (await standardRequest.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - )).data; + const decryptedSecret = ( + await standardRequest.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data; res[vercelSecret.key] = decryptedSecret; } else { @@ -768,12 +795,14 @@ const syncSecretsVercel = async ({ // case: secret has been created newSecrets.push({ key: key, - value: secrets[key], + value: secrets[key].value, type: "encrypted", target: [integration.targetEnvironment], - ...(integration.path ? { - gitBranch: integration.path, - } : {}), + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) }); } }); @@ -781,19 +810,21 @@ const syncSecretsVercel = async ({ // Identify secrets to update and delete Object.keys(res).map((key) => { if (key in secrets) { - if (res[key].value !== secrets[key]) { + if (res[key].value !== secrets[key].value) { // case: secret value has changed updateSecrets.push({ id: res[key].id, key: key, - value: secrets[key], + value: secrets[key].value, type: res[key].type, target: res[key].target.includes(integration.targetEnvironment) ? [...res[key].target] : [...res[key].target, integration.targetEnvironment], - ...(integration.path ? { - gitBranch: integration.path, - } : {}), + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) }); } } else { @@ -804,9 +835,11 @@ const syncSecretsVercel = async ({ value: res[key].value, type: "encrypted", // value doesn't matter target: [integration.targetEnvironment], - ...(integration.path ? { - gitBranch: integration.path, - } : {}), + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) }); } }); @@ -820,8 +853,8 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -830,14 +863,14 @@ const syncSecretsVercel = async ({ if (secret.type !== "sensitive") { const { id, ...updatedSecret } = secret; await standardRequest.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, updatedSecret, { params, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -850,8 +883,8 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -869,11 +902,11 @@ const syncSecretsNetlify = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessToken: string; }) => { interface NetlifyValue { @@ -887,13 +920,9 @@ const syncSecretsNetlify = async ({ values: NetlifyValue[]; } - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } - const getParams = new URLSearchParams({ context_name: "all", // integration.context or all - site_id: integration.appId, + site_id: integration.appId }); const res = ( @@ -903,14 +932,14 @@ const syncSecretsNetlify = async ({ params: getParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ) ).data.reduce( (obj: any, secret: any) => ({ ...obj, - [secret.key]: secret, + [secret.key]: secret }), {} ); @@ -928,17 +957,17 @@ const syncSecretsNetlify = async ({ key, values: [ { - value: secrets[key], - context: integration.targetEnvironment, - }, - ], + value: secrets[key].value, + context: integration.targetEnvironment + } + ] }); } else { // case: Infisical secret exists in Netlify const contexts = res[key].values.reduce( (obj: any, value: NetlifyValue) => ({ ...obj, - [value.context]: value, + [value.context]: value }), {} ); @@ -953,9 +982,9 @@ const syncSecretsNetlify = async ({ values: [ { context: integration.targetEnvironment, - value: secrets[key], - }, - ], + value: secrets[key].value + } + ] }); } } else { @@ -966,9 +995,9 @@ const syncSecretsNetlify = async ({ values: [ { context: integration.targetEnvironment, - value: secrets[key], - }, - ], + value: secrets[key].value + } + ] }); } } @@ -996,9 +1025,9 @@ const syncSecretsNetlify = async ({ { id: value.id, context: integration.targetEnvironment, - value: value.value, - }, - ], + value: value.value + } + ] }); } } @@ -1007,7 +1036,7 @@ const syncSecretsNetlify = async ({ }); const syncParams = new URLSearchParams({ - site_id: integration.appId, + site_id: integration.appId }); if (newSecrets.length > 0) { @@ -1018,8 +1047,8 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -1030,14 +1059,14 @@ const syncSecretsNetlify = async ({ `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, { context: secret.values[0].context, - value: secret.values[0].value, + value: secret.values[0].value }, { params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }); @@ -1051,8 +1080,8 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }); @@ -1066,8 +1095,8 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }); @@ -1085,10 +1114,10 @@ const syncSecretsNetlify = async ({ const syncSecretsGitHub = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface GitHubRepoKey { @@ -1106,47 +1135,39 @@ const syncSecretsGitHub = async ({ [index: string]: GitHubSecret; } - const deleteSecrets: GitHubSecret[] = []; - const octokit = new Octokit({ - auth: accessToken, + auth: accessToken }); // const user = (await octokit.request('GET /user', {})).data; const repoPublicKey: GitHubRepoKey = ( - await octokit.request( - "GET /repos/{owner}/{repo}/actions/secrets/public-key", - { - owner: integration.owner, - repo: integration.app, - } - ) + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { + owner: integration.owner, + repo: integration.app + }) ).data; // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key const encryptedSecrets: GitHubSecretRes = ( await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { owner: integration.owner, - repo: integration.app, + repo: integration.app }) ).data.secrets.reduce( (obj: any, secret: any) => ({ ...obj, - [secret.name]: secret, + [secret.name]: secret }), {} ); Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { - await octokit.request( - "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - } - ); + await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner, + repo: integration.app, + secret_name: key + }); } }); @@ -1154,31 +1175,22 @@ const syncSecretsGitHub = async ({ // let encryptedSecret; sodium.ready.then(async () => { // convert secret & base64 key to Uint8Array. - const binkey = sodium.from_base64( - repoPublicKey.key, - sodium.base64_variants.ORIGINAL - ); - const binsec = sodium.from_string(secrets[key]); + const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL); + const binsec = sodium.from_string(secrets[key].value); // encrypt secret using libsodium const encBytes = sodium.crypto_box_seal(binsec, binkey); // convert encrypted Uint8Array to base64 - const encryptedSecret = sodium.to_base64( - encBytes, - sodium.base64_variants.ORIGINAL - ); + const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL); - await octokit.request( - "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - encrypted_value: encryptedSecret, - key_id: repoPublicKey.key_id, - } - ); + await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner, + repo: integration.app, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id + }); }); }); }; @@ -1193,23 +1205,23 @@ const syncSecretsGitHub = async ({ const syncSecretsRender = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { await standardRequest.put( `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, Object.keys(secrets).map((key) => ({ key, - value: secrets[key], + value: secrets[key].value })), { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }; @@ -1225,33 +1237,32 @@ const syncSecretsLaravelForge = async ({ integration, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { - function transformObjectToString(obj: any) { let result = ""; for (const key in obj) { - result += `${key}=${obj[key]}\n`; + result += `${key}=${obj[key].value}\n`; } return result; } - + await standardRequest.put( `${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${accessId}/sites/${integration.appId}/env`, { - content: transformObjectToString(secrets), + content: transformObjectToString(secrets) }, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } } ); }; @@ -1266,10 +1277,10 @@ const syncSecretsLaravelForge = async ({ const syncSecretsRailway = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { const query = ` @@ -1283,22 +1294,26 @@ const syncSecretsRailway = async ({ environmentId: integration.targetEnvironmentId, ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), replace: true, - variables: secrets, + variables: getSecretKeyValuePair(secrets) }; - await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { - query, - variables: { - input, + await standardRequest.post( + INTEGRATION_RAILWAY_API_URL, + { + query, + variables: { + input + } }, - }, { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, - }); -} + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); +}; /** * Sync/push [secrets] to Fly.io app @@ -1310,10 +1325,10 @@ const syncSecretsRailway = async ({ const syncSecretsFlyio = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { // set secrets @@ -1337,23 +1352,27 @@ const syncSecretsFlyio = async ({ } `; - await standardRequest.post(INTEGRATION_FLYIO_API_URL, { - query: SetSecrets, - variables: { - input: { - appId: integration.app, - secrets: Object.entries(secrets).map(([key, value]) => ({ - key, - value, - })), - }, + await standardRequest.post( + INTEGRATION_FLYIO_API_URL, + { + query: SetSecrets, + variables: { + input: { + appId: integration.app, + secrets: Object.entries(secrets).map(([key, data]) => ({ + key, + value: data.value + })) + } + } }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - "Accept-Encoding": "application/json", - }, - }); + { + headers: { + Authorization: "Bearer " + accessToken, + "Accept-Encoding": "application/json" + } + } + ); // get secrets interface FlyioSecret { @@ -1372,18 +1391,24 @@ const syncSecretsFlyio = async ({ } }`; - const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, { - query: GetSecrets, - variables: { - appName: integration.app, - }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, - })).data.data.app.secrets; + const getSecretsRes = ( + await standardRequest.post( + INTEGRATION_FLYIO_API_URL, + { + query: GetSecrets, + variables: { + appName: integration.app + } + }, + { + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ) + ).data.data.app.secrets; const deleteSecretsKeys = getSecretsRes .filter((secret: FlyioSecret) => !(secret.name in secrets)) @@ -1408,21 +1433,25 @@ const syncSecretsFlyio = async ({ } }`; - await standardRequest.post(INTEGRATION_FLYIO_API_URL, { - query: DeleteSecrets, - variables: { - input: { - appId: integration.app, - keys: deleteSecretsKeys, - }, + await standardRequest.post( + INTEGRATION_FLYIO_API_URL, + { + query: DeleteSecrets, + variables: { + input: { + appId: integration.app, + keys: deleteSecretsKeys + } + } }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, - }); + { + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); }; /** @@ -1435,18 +1464,18 @@ const syncSecretsFlyio = async ({ const syncSecretsCircleCI = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { const circleciOrganizationDetail = ( await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { headers: { "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data[0]; @@ -1459,13 +1488,13 @@ const syncSecretsCircleCI = async ({ `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, { name: key, - value: secrets[key], + value: secrets[key].value }, { headers: { "Circle-Token": accessToken, - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } } ) ); @@ -1477,8 +1506,8 @@ const syncSecretsCircleCI = async ({ { headers: { "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ) ).data?.items; @@ -1491,8 +1520,8 @@ const syncSecretsCircleCI = async ({ { headers: { "Circle-Token": accessToken, - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } } ); } @@ -1500,7 +1529,7 @@ const syncSecretsCircleCI = async ({ }; /** - * Sync/push [secrets] to TravisCI project + * Sync/push [secrets] to TravisCI project * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) @@ -1509,30 +1538,30 @@ const syncSecretsCircleCI = async ({ const syncSecretsTravisCI = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { - // get secrets from travis-ci + // get secrets from travis-ci const getSecretsRes = ( await standardRequest.get( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, { headers: { - "Authorization": `token ${accessToken}`, - "Accept-Encoding": "application/json", - }, + Authorization: `token ${accessToken}`, + "Accept-Encoding": "application/json" + } } ) - ) - .data - ?.env_vars - .reduce((obj: any, secret: any) => ({ + ).data?.env_vars.reduce( + (obj: any, secret: any) => ({ ...obj, - [secret.name]: secret, - }), {}); + [secret.name]: secret + }), + {} + ); // add secrets for await (const key of Object.keys(secrets)) { @@ -1544,15 +1573,15 @@ const syncSecretsTravisCI = async ({ { env_var: { name: key, - value: secrets[key], - }, + value: secrets[key].value + } }, { headers: { - "Authorization": `token ${accessToken}`, + Authorization: `token ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } else { @@ -1563,15 +1592,15 @@ const syncSecretsTravisCI = async ({ { env_var: { name: key, - value: secrets[key], - }, + value: secrets[key].value + } }, { headers: { - "Authorization": `token ${accessToken}`, + Authorization: `token ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -1584,15 +1613,15 @@ const syncSecretsTravisCI = async ({ `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, { headers: { - "Authorization": `token ${accessToken}`, + Authorization: `token ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } } -} +}; /** * Sync/push [secrets] to GitLab repo with name [integration.app] @@ -1605,10 +1634,10 @@ const syncSecretsTravisCI = async ({ const syncSecretsGitLab = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface GitLabSecret { @@ -1620,8 +1649,8 @@ const syncSecretsGitLab = async ({ const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => { const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`; const headers = { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" }; let allEnvVariables: GitLabSecret[] = []; @@ -1645,8 +1674,8 @@ const syncSecretsGitLab = async ({ }; const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); - const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => - secret.environment_scope === integration.targetEnvironment + const getSecretsRes: GitLabSecret[] = allEnvVariables.filter( + (secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment ); for await (const key of Object.keys(secrets)) { @@ -1656,55 +1685,55 @@ const syncSecretsGitLab = async ({ `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, { key: key, - value: secrets[key], + value: secrets[key].value, protected: false, masked: false, raw: false, - environment_scope: integration.targetEnvironment, + environment_scope: integration.targetEnvironment }, { headers: { - "Authorization": `Bearer ${accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } - ) + ); } else { - // update secret - if (secrets[key] !== existingSecret.value) { + // update secret + if (secrets[key].value !== existingSecret.value) { await standardRequest.put( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, { ...existingSecret, - value: secrets[existingSecret.key], + value: secrets[existingSecret.key].value }, { headers: { - "Authorization": `Bearer ${accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } } } - // delete secrets + // delete secrets for await (const sec of getSecretsRes) { if (!(sec.key in secrets)) { await standardRequest.delete( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`, { headers: { - "Authorization": `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } } ); } } -} +}; /** * Sync/push [secrets] to Supabase with name [integration.app] @@ -1717,10 +1746,10 @@ const syncSecretsGitLab = async ({ const syncSecretsSupabase = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { const { data: getSecretsRes } = await standardRequest.get( @@ -1728,20 +1757,18 @@ const syncSecretsSupabase = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); // convert the secrets to [{}] format - const modifiedFormatForSecretInjection = Object.keys(secrets).map( - (key) => { - return { - name: key, - value: secrets[key], - }; - } - ); + const modifiedFormatForSecretInjection = Object.keys(secrets).map((key) => { + return { + name: key, + value: secrets[key].value + }; + }); await standardRequest.post( `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, @@ -1749,8 +1776,8 @@ const syncSecretsSupabase = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); @@ -1767,14 +1794,13 @@ const syncSecretsSupabase = async ({ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", + "Accept-Encoding": "application/json" }, - data: secretsToDelete, + data: secretsToDelete } ); }; - /** * Sync/push [secrets] to Checkly app * @param {Object} obj @@ -1785,30 +1811,28 @@ const syncSecretsSupabase = async ({ const syncSecretsCheckly = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { - // get secrets from travis-ci + // get secrets from travis-ci const getSecretsRes = ( - await standardRequest.get( - `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, - { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - "X-Checkly-Account": integration.appId, - }, + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + "X-Checkly-Account": integration.appId } - ) - ) - .data - .reduce((obj: any, secret: any) => ({ + }) + ).data.reduce( + (obj: any, secret: any) => ({ ...obj, - [secret.key]: secret.value, - }), {}); + [secret.key]: secret.value + }), + {} + ); // add secrets for await (const key of Object.keys(secrets)) { @@ -1820,15 +1844,15 @@ const syncSecretsCheckly = async ({ `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { key, - value: secrets[key], + value: secrets[key].value }, { headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", "Content-Type": "application/json", - "X-Checkly-Account": integration.appId, - }, + "X-Checkly-Account": integration.appId + } } ); } else { @@ -1839,15 +1863,15 @@ const syncSecretsCheckly = async ({ await standardRequest.put( `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { - value: secrets[key], + value: secrets[key].value }, { headers: { - "Authorization": `Bearer ${accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept": "application/json", - "X-Checkly-Account": integration.appId, - }, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } } ); } @@ -1857,16 +1881,13 @@ const syncSecretsCheckly = async ({ for await (const key of Object.keys(getSecretsRes)) { if (!(key in secrets)) { // delete secret - await standardRequest.delete( - `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, - { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", - "X-Checkly-Account": integration.appId, - }, + await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId } - ); + }); } } }; @@ -1881,29 +1902,31 @@ const syncSecretsCheckly = async ({ const syncSecretsTerraformCloud = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { // get secrets from Terraform Cloud const getSecretsRes = ( - await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, - } - )) - .data - .data - .reduce((obj: any, secret: any) => ({ + await standardRequest.get( + `${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.data.reduce( + (obj: any, secret: any) => ({ ...obj, [secret.attributes.key]: secret - }), {}); - + }), + {} + ); + // create or update secrets on Terraform Cloud for await (const key of Object.keys(secrets)) { if (!(key in getSecretsRes)) { @@ -1916,22 +1939,22 @@ const syncSecretsTerraformCloud = async ({ type: "vars", attributes: { key, - value: secrets[key], - category: integration.targetService, - }, - }, + value: secrets[key].value, + category: integration.targetService + } + } }, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/vnd.api+json", - Accept: "application/vnd.api+json", - }, + Accept: "application/vnd.api+json" + } } ); } else { // case: secret exists in Terraform Cloud - if (secrets[key] !== getSecretsRes[key].attributes.value) { + if (secrets[key].value !== getSecretsRes[key].attributes.value) { // -> update secret await standardRequest.patch( `${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`, @@ -1941,16 +1964,16 @@ const syncSecretsTerraformCloud = async ({ id: getSecretsRes[key].id, attributes: { ...getSecretsRes[key], - value: secrets[key] - }, - }, + value: secrets[key].value + } + } }, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/vnd.api+json", - Accept: "application/vnd.api+json", - }, + Accept: "application/vnd.api+json" + } } ); } @@ -1960,13 +1983,16 @@ const syncSecretsTerraformCloud = async ({ for await (const key of Object.keys(getSecretsRes)) { if (!(key in secrets)) { // case: delete secret - await standardRequest.delete(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/vnd.api+json", - Accept: "application/vnd.api+json", - }, - }) + await standardRequest.delete( + `${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json" + } + } + ); } } }; @@ -1983,11 +2009,11 @@ const syncSecretsHashiCorpVault = async ({ integrationAuth, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { @@ -1996,20 +2022,20 @@ const syncSecretsHashiCorpVault = async ({ interface LoginAppRoleRes { auth: { client_token: string; - } + }; } // get Vault client token (could be optimized) const { data }: { data: LoginAppRoleRes } = await standardRequest.post( `${integrationAuth.url}/v1/auth/approle/login`, { - "role_id": accessId, - "secret_id": accessToken, + role_id: accessId, + secret_id: accessToken }, { headers: { - "X-Vault-Namespace": integrationAuth.namespace, - }, + "X-Vault-Namespace": integrationAuth.namespace + } } ); @@ -2018,16 +2044,16 @@ const syncSecretsHashiCorpVault = async ({ await standardRequest.post( `${integrationAuth.url}/v1/${integration.app}/data/${integration.path}`, { - data: secrets, + data: getSecretKeyValuePair(secrets) }, { headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", "Content-Type": "application/json", "X-Vault-Token": clientToken, - "X-Vault-Namespace": integrationAuth.namespace, - }, + "X-Vault-Namespace": integrationAuth.namespace + } } ); }; @@ -2043,14 +2069,13 @@ const syncSecretsCloudflarePages = async ({ integration, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { - // get secrets from cloudflare pages const getSecretsRes = ( await standardRequest.get( @@ -2058,15 +2083,14 @@ const syncSecretsCloudflarePages = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + Accept: "application/json" + } } ) - ) - .data.result["deployment_configs"][integration.targetEnvironment]["env_vars"]; + ).data.result["deployment_configs"][integration.targetEnvironment]["env_vars"]; // copy the secrets object, so we can set deleted keys to null - const secretsObj: any = { ...secrets }; + const secretsObj: any = getSecretKeyValuePair(secrets); for (const [key, val] of Object.entries(secretsObj)) { secretsObj[key] = { type: "secret_text", value: val }; @@ -2083,9 +2107,9 @@ const syncSecretsCloudflarePages = async ({ } const data = { - "deployment_configs": { + deployment_configs: { [integration.targetEnvironment]: { - "env_vars": secretsObj + env_vars: secretsObj } } }; @@ -2096,11 +2120,11 @@ const syncSecretsCloudflarePages = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + Accept: "application/json" + } } ); -} +}; /** * Sync/push [secrets] to BitBucket repo with name [integration.app] @@ -2113,10 +2137,10 @@ const syncSecretsCloudflarePages = async ({ const syncSecretsBitBucket = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface VariablesResponse { @@ -2139,18 +2163,15 @@ const syncSecretsBitBucket = async ({ const res: { [key: string]: BitbucketVariable } = {}; let hasNextPage = true; - let variablesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables` + let variablesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`; while (hasNextPage) { - const { data }: { data: VariablesResponse } = await standardRequest.get( - variablesUrl, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, - } - ); + const { data }: { data: VariablesResponse } = await standardRequest.get(variablesUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); if (data?.values.length > 0) { data.values.forEach((variable) => { @@ -2159,9 +2180,9 @@ const syncSecretsBitBucket = async ({ } if (data.next) { - variablesUrl = data.next + variablesUrl = data.next; } else { - hasNextPage = false + hasNextPage = false; } } @@ -2169,34 +2190,34 @@ const syncSecretsBitBucket = async ({ if (key in res) { // update existing secret await standardRequest.put( - `${variablesUrl}/${res[key].uuid}`, - { - key, - value: secrets[key], - secured: true - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + `${variablesUrl}/${res[key].uuid}`, + { + key, + value: secrets[key].value, + secured: true + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } + } ); } else { // create new secret await standardRequest.post( - variablesUrl, - { - key, - value: secrets[key], - secured: true - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + variablesUrl, + { + key, + value: secrets[key].value, + secured: true + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } + } ); } } @@ -2204,18 +2225,15 @@ const syncSecretsBitBucket = async ({ for await (const key of Object.keys(res)) { if (!(key in secrets)) { // delete secret - await standardRequest.delete( - `${variablesUrl}/${res[key].uuid}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - } + await standardRequest.delete(`${variablesUrl}/${res[key].uuid}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } - ); + }); } } -} +}; /** * Sync/push [secrets] to Codefresh project with name [integration.app] @@ -2228,10 +2246,10 @@ const syncSecretsBitBucket = async ({ const syncSecretsCodefresh = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { await standardRequest.patch( @@ -2239,16 +2257,16 @@ const syncSecretsCodefresh = async ({ { variables: Object.keys(secrets).map((key) => ({ key, - value: secrets[key] + value: secrets[key].value })) }, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + Accept: "application/json" + } } - ); + ); }; /** @@ -2265,7 +2283,7 @@ const syncSecretsDigitalOceanAppPlatform = async ({ accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { // get current app settings @@ -2282,8 +2300,8 @@ const syncSecretsDigitalOceanAppPlatform = async ({ `${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`, { spec: { - ...appSettings, - envs: Object.entries(secrets).map(([key, value]) => ({ key, value })) + name: integration.app, + envs: Object.entries(secrets).map(([key, data]) => ({ key, value: data.value })) } }, { @@ -2307,13 +2325,11 @@ const syncSecretsDigitalOceanAppPlatform = async ({ const syncSecretsWindmill = async ({ integration, secrets, - accessToken, - secretComments + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; - secretComments: any; }) => { interface WindmillSecret { path: string; @@ -2323,69 +2339,69 @@ const syncSecretsWindmill = async ({ } // get secrets stored in windmill workspace - const res = (await standardRequest.get( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/list`, - { - headers: { + const res = ( + await standardRequest.get( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/list`, + { + headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - )) - .data - .reduce( + "Accept-Encoding": "application/json" + } + } + ) + ).data.reduce( (obj: any, secret: WindmillSecret) => ({ ...obj, [secret.path]: secret }), {} ); - + // eslint-disable-next-line no-useless-escape - const pattern = new RegExp("^(u\/|f\/)[a-zA-Z0-9_-]+\/([a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]*[^\/]$"); - + const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); + for await (const key of Object.keys(secrets)) { - if((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { - if(!(key in res)) { - // case: secret does not exist in windmill - // -> create secret - - await standardRequest.post( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/create`, - { - path: key, - value: secrets[key], - is_secret: true, - description: secretComments[key] || "" - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ); - } else { - // -> update secret - await standardRequest.post( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, - { - path: key, - value: secrets[key], - is_secret: true, - description: secretComments[key] || "" - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ); + if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { + if (!(key in res)) { + // case: secret does not exist in windmill + // -> create secret + + await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/create`, + { + path: key, + value: secrets[key].value, + is_secret: true, + description: secrets[key]?.comment || "" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } + ); + } else { + // -> update secret + await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, + { + path: key, + value: secrets[key].value, + is_secret: true, + description: secrets[key]?.comment || "" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); } + } } - + for await (const key of Object.keys(res)) { if (!(key in secrets)) { // -> delete secret @@ -2395,13 +2411,13 @@ const syncSecretsWindmill = async ({ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", + "Accept-Encoding": "application/json" } } ); } } -} +}; /** * Sync/push [secrets] to Cloud66 application with name [integration.app] @@ -2417,10 +2433,9 @@ const syncSecretsCloud66 = async ({ accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { - interface Cloud66Secret { id: number; key: string; @@ -2444,49 +2459,47 @@ const syncSecretsCloud66 = async ({ } } ) - ) - .data - .response - .filter((secret: Cloud66Secret) => !secret.readonly || !secret.is_generated) - .reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), - {} - ); + ).data.response + .filter((secret: Cloud66Secret) => !secret.readonly || !secret.is_generated) + .reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), + {} + ); for await (const key of Object.keys(secrets)) { if (key in res) { // update existing secret await standardRequest.put( - `${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`, - { - key, - value: secrets[key] - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + `${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } - ); + } + ); } else { // create new secret await standardRequest.post( - `${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`, - { - key, - value: secrets[key] - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + `${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } - ); + } + ); } } @@ -2494,14 +2507,14 @@ const syncSecretsCloud66 = async ({ if (!(key in secrets)) { // delete secret await standardRequest.delete( - `${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + `${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } - ); + } + ); } } }; @@ -2518,20 +2531,20 @@ const syncSecretsNorthflank = async ({ accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { await standardRequest.patch( `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${integration.targetServiceId}`, { secrets: { - variables: secrets + variables: getSecretKeyValuePair(secrets) } }, { headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } } ); From 086652a89f117db632415d9c5a995d0e2cf61ce5 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 1 Aug 2023 15:05:09 +0530 Subject: [PATCH 02/11] fix: resolved infinite recursion cases --- backend/src/helpers/secrets.ts | 21 ++++++++++++++++----- backend/src/integrations/sync.ts | 10 ++++++---- backend/tsconfig.json | 19 +++++-------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 818fdf85b..a693b88a3 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -991,11 +991,16 @@ const recursivelyExpandSecret = async ( expandedSec: Record, interpolatedSec: Record, fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise, + recursionChainBreaker: Record, key: string ) => { if (expandedSec?.[key]) { return expandedSec[key]; } + if (recursionChainBreaker?.[key]) { + return ""; + } + recursionChainBreaker[key] = true; let interpolatedValue = interpolatedSec[key]; if (!interpolatedValue) { @@ -1013,12 +1018,13 @@ const recursivelyExpandSecret = async ( expandedSec, interpolatedSec, fetchCrossEnv, + recursionChainBreaker, interpolationKey ); if (val) { - interpolatedValue = interpolatedValue.replace(interpolationSyntax, val); + interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); } - return; + continue; } if (entities.length > 1) { @@ -1027,11 +1033,12 @@ const recursivelyExpandSecret = async ( const secRefKey = entities[entities.length - 1]; const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey); - interpolatedValue = interpolatedValue.replace(interpolationSyntax, val); + interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); } } } + expandedSec[key] = interpolatedValue; return interpolatedValue; }; @@ -1057,17 +1064,21 @@ export const expandSecrets = async ( for (const key of Object.keys(secrets)) { if (expandedSec?.[key]) { secrets[key].value = expandedSec[key]; - return; + continue; } + // this is to avoid recursion loop. So the graph should be direct graph rather than cyclic + // so for any recursion building if there is an entity two times same key meaning it will be looped + const recursionChainBreaker: Record = {}; const expandedVal = await recursivelyExpandSecret( expandedSec, interpolatedSec, crossSecEnvFetch, + recursionChainBreaker, key ); - secrets[key].value = expandedVal || ""; + secrets[key].value = expandedVal; } return secrets; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index e178af833..7ff8e71b4 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -59,9 +59,11 @@ import _ from "lodash"; import sodium from "libsodium-wrappers"; import { standardRequest } from "../config/request"; -const getSecretKeyValuePair = (secrets: Record) => +const getSecretKeyValuePair = ( + secrets: Record +) => Object.keys(secrets).reduce>((prev, key) => { - prev[key] = secrets[key].value; + if (secrets[key]) prev[key] = secrets[key]?.value || ""; return prev; }, {}); @@ -667,7 +669,7 @@ const syncSecretsHeroku = async ({ accessToken }: { integration: IIntegration; - secrets: Record; + secrets: Record; accessToken: string; }) => { const herokuSecrets = ( @@ -682,7 +684,7 @@ const syncSecretsHeroku = async ({ Object.keys(herokuSecrets).forEach((key) => { if (!(key in secrets)) { - delete secrets[key]; + secrets[key] = null; } }); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 98ea0808a..d2f7a1b89 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,9 +1,7 @@ { "compilerOptions": { "target": "es2016", - "lib": [ - "es6" - ], + "lib": ["es6", "es2021"], "module": "commonjs", "rootDir": "src", "resolveJsonModule": true, @@ -15,15 +13,8 @@ "strict": true, "noImplicitAny": true, "skipLibCheck": true, - "typeRoots": [ - "./src/types", - "./node_modules/@types" - ] + "typeRoots": ["./src/types", "./node_modules/@types"] }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules"] +} From b1981df8f0833b6b951e2cdd028782b919bf99d0 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 1 Aug 2023 15:29:34 +0530 Subject: [PATCH 03/11] chore: resolved merge conflict --- backend/src/integrations/sync.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 7ff8e71b4..5de205195 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2303,6 +2303,7 @@ const syncSecretsDigitalOceanAppPlatform = async ({ { spec: { name: integration.app, + ...appSettings, envs: Object.entries(secrets).map(([key, data]) => ({ key, value: data.value })) } }, From cc5ca30057349dca9367bfc78a062072e80890c0 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Wed, 2 Aug 2023 16:12:58 +0530 Subject: [PATCH 04/11] feat: updated multi line format on integrations sync secrets --- backend/src/helpers/secrets.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index a693b88a3..17e5c93ce 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -1042,6 +1042,13 @@ const recursivelyExpandSecret = async ( return interpolatedValue; }; +// used to convert multi line ones to quotes ones with \n +const formatMultiValueEnv = (val?: string) => { + if (!val) return ""; + if (!val.match("\n")) return val; + return `"${val.replace(/\n/g, "\\n")}"`; +}; + export const expandSecrets = async ( workspaceId: string, rootEncKey: string, @@ -1063,7 +1070,7 @@ export const expandSecrets = async ( for (const key of Object.keys(secrets)) { if (expandedSec?.[key]) { - secrets[key].value = expandedSec[key]; + secrets[key].value = formatMultiValueEnv(expandedSec[key]); continue; } @@ -1078,7 +1085,7 @@ export const expandSecrets = async ( key ); - secrets[key].value = expandedVal; + secrets[key].value = formatMultiValueEnv(expandedVal); } return secrets; From bd80c2ccc3195aca721be7023a70cb442a6db876 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 2 Aug 2023 10:11:14 -0400 Subject: [PATCH 05/11] swap out keyring package --- cli/go.mod | 18 +-- cli/go.sum | 33 ++-- cli/internal/keyringwrapper.go | 68 ++++++++ cli/packages/cmd/init.go | 4 + cli/packages/cmd/login.go | 5 +- cli/packages/cmd/reset.go | 8 - cli/packages/cmd/secrets.go | 8 + cli/packages/cmd/vault.go | 103 ------------ cli/packages/models/cli.go | 11 +- cli/packages/util/config.go | 1 - cli/packages/util/credentials.go | 66 ++++---- cli/packages/util/log.go | 2 +- cli/packages/util/secrets.go | 4 + cli/packages/util/secrets_test.go | 258 ------------------------------ cli/packages/util/vault.go | 73 --------- 15 files changed, 141 insertions(+), 521 deletions(-) create mode 100644 cli/internal/keyringwrapper.go delete mode 100644 cli/packages/cmd/vault.go delete mode 100644 cli/packages/util/secrets_test.go delete mode 100644 cli/packages/util/vault.go diff --git a/cli/go.mod b/cli/go.mod index ce4cf6808..86b7df377 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,7 +3,6 @@ module github.com/Infisical/infisical-merge go 1.19 require ( - github.com/99designs/keyring v1.2.2 github.com/charmbracelet/lipgloss v0.5.0 github.com/denisbrodbeck/machineid v1.0.1 github.com/fatih/semgroup v1.2.0 @@ -15,43 +14,40 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a + github.com/rs/cors v1.9.0 github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.8.1 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/term v0.9.0 ) require ( - github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/alessio/shellescape v1.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect - github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/mtibben/percent v0.2.1 // indirect github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml v1.9.3 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rs/cors v1.9.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -62,6 +58,7 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.7.0 // indirect + gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -74,4 +71,5 @@ require ( github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/manifoldco/promptui v0.9.0 github.com/spf13/pflag v1.0.5 // indirect + github.com/zalando/go-keyring v0.2.3 ) diff --git a/cli/go.sum b/cli/go.sum index ea16f2d37..f35e9988d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -37,12 +37,10 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= -github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -72,15 +70,13 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= -github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= -github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -107,9 +103,9 @@ github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtK github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -179,8 +175,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -257,8 +251,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4= github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -326,8 +318,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -335,8 +328,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -354,6 +348,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -527,15 +523,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/cli/internal/keyringwrapper.go b/cli/internal/keyringwrapper.go new file mode 100644 index 000000000..de4eb317d --- /dev/null +++ b/cli/internal/keyringwrapper.go @@ -0,0 +1,68 @@ +package keyringwrapper + +import ( + "time" + + "github.com/zalando/go-keyring" +) + +const MAIN_KEYRING_SERVICE = "infisical-cli" + +type TimeoutError struct { + message string +} + +func (e *TimeoutError) Error() string { + return e.message +} + +func Set(key, value string) error { + ch := make(chan error, 1) + go func() { + defer close(ch) + ch <- keyring.Set(MAIN_KEYRING_SERVICE, key, value) + }() + select { + case err := <-ch: + return err + case <-time.After(3 * time.Second): + return &TimeoutError{"timeout while trying to set secret in keyring"} + } +} + +func Get(key string) (string, error) { + ch := make(chan struct { + val string + err error + }, 1) + + go func() { + defer close(ch) + val, err := keyring.Get(MAIN_KEYRING_SERVICE, key) + ch <- struct { + val string + err error + }{val, err} + }() + + select { + case res := <-ch: + return res.val, res.err + case <-time.After(3 * time.Second): + return "", &TimeoutError{"timeout while trying to get secret from keyring"} + } +} + +func Delete(key string) error { + ch := make(chan error, 1) + go func() { + defer close(ch) + ch <- keyring.Delete(MAIN_KEYRING_SERVICE, key) + }() + select { + case err := <-ch: + return err + case <-time.After(3 * time.Second): + return &TimeoutError{"timeout while trying to delete secret from keyring"} + } +} diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index 64387a837..070074fa9 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -46,6 +46,10 @@ var initCmd = &cobra.Command{ util.HandleError(err, "Unable to get your login details") } + if userCreds.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + httpClient := resty.New() httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f8d397233..a2daf90dd 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -57,7 +57,7 @@ var loginCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() // if the key can't be found or there is an error getting current credentials from key ring, allow them to override - if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring") || strings.Contains(err.Error(), "GetUserCredsFromKeyRing")) { + if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { log.Debug().Err(err) } else if err != nil { util.HandleError(err) @@ -117,8 +117,7 @@ var loginCmd = &cobra.Command{ err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) if err != nil { - currentVault, _ := util.GetCurrentVaultBackend() - log.Error().Msgf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault) + log.Error().Msgf("Unable to store your credentials in system vault [%s]") log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq") log.Debug().Err(err) //return here diff --git a/cli/packages/cmd/reset.go b/cli/packages/cmd/reset.go index 0007f2687..9ea1f36bb 100644 --- a/cli/packages/cmd/reset.go +++ b/cli/packages/cmd/reset.go @@ -26,14 +26,6 @@ var resetCmd = &cobra.Command{ os.RemoveAll(pathToDir) - // delete keyring - keyringInstance, err := util.GetKeyRing() - if err != nil { - util.HandleError(err) - } - - keyringInstance.Remove(util.KEYRING_SERVICE_NAME) - // delete secrets backup util.DeleteBackupSecrets() diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 4d55f4307..c42023cbb 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -128,6 +128,10 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to authenticate") } + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + httpClient := resty.New(). SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). SetHeader("Accept", "application/json") @@ -334,6 +338,10 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to authenticate") } + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + workspaceFile, err := util.GetWorkSpaceFromFile() if err != nil { util.HandleError(err, "Unable to get local project details") diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go deleted file mode 100644 index edf3befc7..000000000 --- a/cli/packages/cmd/vault.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "fmt" - - "github.com/99designs/keyring" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set pass`, - Use: "set [vault-name]", - Short: "Used to set the vault backend to store your login details securely at rest", - DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - wantedVaultTypeName := args[0] - currentVaultBackend, err := util.GetCurrentVaultBackend() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - - if wantedVaultTypeName == string(currentVaultBackend) { - log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) - return - } - - if isVaultToSwitchToValid(wantedVaultTypeName) { - configFile, err := util.GetConfigFile() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - - configFile.VaultBackendType = keyring.BackendType(wantedVaultTypeName) // save selected vault - configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login - - err = util.WriteConfigFile(&configFile) - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) - return - } - - fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) - - Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) - } else { - log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, keyring.AvailableBackends()) - } - }, -} - -// runCmd represents the run command -var vaultCmd = &cobra.Command{ - Use: "vault", - Short: "Used to manage where your Infisical login token is saved on your machine", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - printAvailableVaultBackends() - }, -} - -func printAvailableVaultBackends() { - fmt.Printf("The following vaults are available on your system:") - for _, backend := range keyring.AvailableBackends() { - fmt.Printf("\n- %s", backend) - } - - currentVaultBackend, err := util.GetCurrentVaultBackend() - if err != nil { - log.Error().Msgf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err) - } - - Telemetry.CaptureEvent("cli-command:vault", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("version", util.CLI_VERSION)) - - fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend)) -} - -// Checks if the vault that the user wants to switch to is a valid available vault -func isVaultToSwitchToValid(vaultNameToSwitchTo string) bool { - isFound := false - for _, backend := range keyring.AvailableBackends() { - if vaultNameToSwitchTo == string(backend) { - isFound = true - break - } - } - - return isFound -} - -func init() { - vaultCmd.AddCommand(vaultSetCmd) - rootCmd.AddCommand(vaultCmd) -} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index c388d2d9e..d71981876 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -1,9 +1,5 @@ package models -import ( - "github.com/99designs/keyring" -) - type UserCredentials struct { Email string `json:"email"` PrivateKey string `json:"privateKey"` @@ -13,10 +9,9 @@ type UserCredentials struct { // The file struct for Infisical config file type ConfigFile struct { - LoggedInUserEmail string `json:"loggedInUserEmail"` - LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` - VaultBackendType keyring.BackendType `json:"vaultBackendType"` - LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` + LoggedInUserEmail string `json:"loggedInUserEmail"` + LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` + LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` } type LoggedInUser struct { diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go index e6c6fddb4..37d522efb 100644 --- a/cli/packages/util/config.go +++ b/cli/packages/util/config.go @@ -52,7 +52,6 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error { configFile := models.ConfigFile{ LoggedInUserEmail: userCredentials.Email, LoggedInUserDomain: config.INFISICAL_URL, - VaultBackendType: existingConfigFile.VaultBackendType, LoggedInUsers: existingConfigFile.LoggedInUsers, } diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 6b203c2e3..718cc4831 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -2,14 +2,16 @@ package util import ( "encoding/json" + "errors" "fmt" + "strings" - "github.com/99designs/keyring" + keyringwrapper "github.com/Infisical/infisical-merge/internal" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" - "github.com/rs/zerolog/log" + "github.com/zalando/go-keyring" ) type LoggedInUserDetails struct { @@ -24,17 +26,7 @@ func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { return fmt.Errorf("StoreUserCredsInKeyRing: something went wrong when marshalling user creds [err=%s]", err) } - // Get keyring - configuredKeyring, err := GetKeyRing() - if err != nil { - return fmt.Errorf("StoreUserCredsInKeyRing: unable to get keyring instance with [err=%s]", err) - } - - err = configuredKeyring.Set(keyring.Item{ - Key: userCred.Email, - Data: []byte(string(userCredMarshalled)), - }) - + err = keyringwrapper.Set(userCred.Email, string(userCredMarshalled)) if err != nil { return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err) } @@ -43,20 +35,20 @@ func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { } func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentials, err error) { - // Get keyring - configuredKeyring, err := GetKeyRing() + credentialsValue, err := keyringwrapper.Get(userEmail) if err != nil { - return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get keyring instance with [err=%s]", err) - } - - credentialsValue, err := configuredKeyring.Get(userEmail) - if err != nil { - return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get key from Keyring. could not find login credentials in your Keyring. This is common if you have switched vault backend recently. If so, please login in again and retry [err=%s]", err) + if err == keyring.ErrUnsupportedPlatform { + return models.UserCredentials{}, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") + } else if err == keyring.ErrNotFound { + return models.UserCredentials{}, errors.New("credentials not found in system keyring") + } else { + return models.UserCredentials{}, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) + } } var userCredentials models.UserCredentials - err = json.Unmarshal([]byte(credentialsValue.Data), &userCredentials) + err = json.Unmarshal([]byte(credentialsValue), &userCredentials) if err != nil { return models.UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err) } @@ -81,7 +73,11 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) if err != nil { - return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to your credentials from Keyring [err=%s]", err) + if strings.Contains(err.Error(), "credentials not found in system keyring") { + return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again") + } else { + return LoggedInUserDetails{}, fmt.Errorf("failed to fetch creditnals from keyring because [err=%s]", err) + } } // check to to see if the JWT is still valid @@ -97,19 +93,19 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } isAuthenticated := api.CallIsAuthenticated(httpClient) + // TODO: add refresh token + // if !isAuthenticated { + // accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) + // if err == nil && accessTokenResponse.Token != "" { + // isAuthenticated = true + // userCreds.JTWToken = accessTokenResponse.Token + // } + // } - if !isAuthenticated { - accessTokenResponse, _ := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) - if accessTokenResponse.Token != "" { - isAuthenticated = true - userCreds.JTWToken = accessTokenResponse.Token - } - } - - err = StoreUserCredsInKeyRing(&userCreds) - if err != nil { - log.Debug().Msg("unable to store your user credentials with new access token") - } + // err = StoreUserCredsInKeyRing(&userCreds) + // if err != nil { + // log.Debug().Msg("unable to store your user credentials with new access token") + // } if !isAuthenticated { return LoggedInUserDetails{ diff --git a/cli/packages/util/log.go b/cli/packages/util/log.go index a9bf75ec1..9e6e558ea 100644 --- a/cli/packages/util/log.go +++ b/cli/packages/util/log.go @@ -45,5 +45,5 @@ func PrintErrorMessageAndExit(messages ...string) { } func printError(e error) { - color.New(color.FgRed).Fprintf(os.Stderr, "Hmm, we ran into an error: %v\n", e) + color.New(color.FgRed).Fprintf(os.Stderr, "error: %v\n", e) } diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index cde0a2efb..e0eaa084e 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -207,6 +207,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models return nil, err } + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + workspaceFile, err := GetWorkSpaceFromFile() if err != nil { return nil, err diff --git a/cli/packages/util/secrets_test.go b/cli/packages/util/secrets_test.go deleted file mode 100644 index c63b96be9..000000000 --- a/cli/packages/util/secrets_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package util - -import ( - "io" - "os" - "path" - "testing" - - "github.com/Infisical/infisical-merge/packages/models" -) - -// References to self should return the value unaltered -func Test_SubstituteSecrets_When_ReferenceToSelf(t *testing.T) { - - var tests = []struct { - Key string - Value string - ExpectedValue string - }{ - {Key: "A", Value: "${A}", ExpectedValue: "${A}"}, - {Key: "A", Value: "${A} ${A}", ExpectedValue: "${A} ${A}"}, - {Key: "A", Value: "${A}${A}", ExpectedValue: "${A}${A}"}, - } - - for _, test := range tests { - secret := models.SingleEnvironmentVariable{ - Key: test.Key, - Value: test.Value, - } - - secrets := []models.SingleEnvironmentVariable{secret} - result := SubstituteSecrets(secrets) - - if result[0].Value != test.ExpectedValue { - t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected %s but got %s for input %s", test.ExpectedValue, result[0].Value, test.Value) - } - - } -} - -func Test_SubstituteSecrets_When_ReferenceDoesNotExist(t *testing.T) { - - var tests = []struct { - Key string - Value string - ExpectedValue string - }{ - {Key: "A", Value: "${X}", ExpectedValue: "${X}"}, - {Key: "A", Value: "${H}HELLO", ExpectedValue: "${H}HELLO"}, - {Key: "A", Value: "${L}${S}", ExpectedValue: "${L}${S}"}, - } - - for _, test := range tests { - secret := models.SingleEnvironmentVariable{ - Key: test.Key, - Value: test.Value, - } - - secrets := []models.SingleEnvironmentVariable{secret} - result := SubstituteSecrets(secrets) - - if result[0].Value != test.ExpectedValue { - t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected %s but got %s for input %s", test.ExpectedValue, result[0].Value, test.Value) - } - - } -} - -func Test_SubstituteSecrets_When_ReferenceDoesNotExist_And_Self_Referencing(t *testing.T) { - - tests := []struct { - Key string - Value string - ExpectedValue string - }{ - { - Key: "O", - Value: "${P} ==$$ ${X} ${UNKNOWN} ${A}", - ExpectedValue: "DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A}", - }, - { - Key: "X", - Value: "DOMAIN", - ExpectedValue: "DOMAIN", - }, - { - Key: "A", - Value: "*${A}* ${X}", - ExpectedValue: "*${A}* DOMAIN", - }, - { - Key: "H", - Value: "${X} >>>", - ExpectedValue: "DOMAIN >>>", - }, - { - Key: "P", - Value: "DOMAIN === ${A} ${H}", - ExpectedValue: "DOMAIN === ${A} DOMAIN >>>", - }, - { - Key: "T", - Value: "${P} ==$$ ${X} ${UNKNOWN} ${A} ${P} ==$$ ${X} ${UNKNOWN} ${A}", - ExpectedValue: "DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A} DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A}", - }, - { - Key: "S", - Value: "${ SSS$$ ${HEY}", - ExpectedValue: "${ SSS$$ ${HEY}", - }, - } - - secrets := []models.SingleEnvironmentVariable{} - for _, test := range tests { - secrets = append(secrets, models.SingleEnvironmentVariable{Key: test.Key, Value: test.Value}) - } - - results := SubstituteSecrets(secrets) - - for index, expanded := range results { - if expanded.Value != tests[index].ExpectedValue { - t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected [%s] but got [%s] for input [%s]", tests[index].ExpectedValue, expanded.Value, tests[index].Value) - } - } -} - -func Test_SubstituteSecrets_When_No_SubstituteNeeded(t *testing.T) { - - tests := []struct { - Key string - Value string - ExpectedValue string - }{ - { - Key: "DOMAIN", - Value: "infisical.com", - ExpectedValue: "infisical.com", - }, - { - Key: "API_KEY", - Value: "hdgsvjshcgkdckhevdkd", - ExpectedValue: "hdgsvjshcgkdckhevdkd", - }, - { - Key: "ENV", - Value: "PROD", - ExpectedValue: "PROD", - }, - } - - secrets := []models.SingleEnvironmentVariable{} - for _, test := range tests { - secrets = append(secrets, models.SingleEnvironmentVariable{Key: test.Key, Value: test.Value}) - } - - results := SubstituteSecrets(secrets) - - for index, expanded := range results { - if expanded.Value != tests[index].ExpectedValue { - t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected [%s] but got [%s] for input [%s]", tests[index].ExpectedValue, expanded.Value, tests[index].Value) - } - } -} - -func Test_Read_Env_From_File(t *testing.T) { - type testCase struct { - TestFile string - ExpectedEnv string - } - - var cases = []testCase{ - { - TestFile: "testdata/infisical-default-env.json", - ExpectedEnv: "myDefaultEnv", - }, - { - TestFile: "testdata/infisical-branch-env.json", - ExpectedEnv: "myMainEnv", - }, - { - TestFile: "testdata/infisical-no-matching-branch-env.json", - ExpectedEnv: "myDefaultEnv", - }, - } - - // create a tmp directory for testing - testDir, err := os.MkdirTemp(os.TempDir(), "infisical-test") - if err != nil { - t.Errorf("Test_Read_DefaultEnv_From_File: Failed to create temp directory: %s", err) - } - - // safe the current working directory - originalDir, err := os.Getwd() - if err != nil { - t.Errorf("Test_Read_DefaultEnv_From_File: Failed to get current working directory: %s", err) - } - - // backup the original git command - originalGitCmd := getCurrentBranchCmd - - // make sure to clean up after the test - t.Cleanup(func() { - os.Chdir(originalDir) - os.RemoveAll(testDir) - getCurrentBranchCmd = originalGitCmd - }) - - // mock the git command to return "main" as the current branch - getCurrentBranchCmd = execCmd{cmd: "echo", args: []string{"main"}} - - for _, c := range cases { - // make sure we start in the original directory - err = os.Chdir(originalDir) - if err != nil { - t.Errorf("Test_Read_DefaultEnv_From_File: Failed to change working directory: %s", err) - } - - // remove old test file if it exists - err = os.Remove(path.Join(testDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) - if err != nil && !os.IsNotExist(err) { - t.Errorf("Test_Read_DefaultEnv_From_File: Failed to remove old test file: %s", err) - } - - // deploy the test file - copyTestFile(t, c.TestFile, path.Join(testDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) - - // change the working directory to the tmp directory - err = os.Chdir(testDir) - if err != nil { - t.Errorf("Test_Read_DefaultEnv_From_File: Failed to change working directory: %s", err) - } - - // get env from file - env := GetEnvFromWorkspaceFile() - if env != c.ExpectedEnv { - t.Errorf("Test_Read_DefaultEnv_From_File: Expected env to be %s but got %s", c.ExpectedEnv, env) - } - } -} - -func copyTestFile(t *testing.T, src, dst string) { - srcFile, err := os.Open(src) - if err != nil { - t.Errorf("Test_Read_Env_From_File_By_Branch: Failed to open source file: %s", err) - } - defer srcFile.Close() - - dstFile, err := os.Create(dst) - if err != nil { - t.Errorf("Test_Read_Env_From_File_By_Branch: Failed to create destination file: %s", err) - } - defer dstFile.Close() - - _, err = io.Copy(dstFile, srcFile) - if err != nil { - t.Errorf("Test_Read_Env_From_File_By_Branch: Failed to copy file: %s", err) - } -} diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go deleted file mode 100644 index ff45be1ca..000000000 --- a/cli/packages/util/vault.go +++ /dev/null @@ -1,73 +0,0 @@ -package util - -import ( - "fmt" - "os" - - "github.com/99designs/keyring" - "golang.org/x/term" -) - -func GetCurrentVaultBackend() (keyring.BackendType, error) { - configFile, err := GetConfigFile() - if err != nil { - return "", fmt.Errorf("getCurrentVaultBackend: unable to get config file [err=%s]", err) - } - - if configFile.VaultBackendType == "" { - return keyring.AvailableBackends()[0], nil - } - - return configFile.VaultBackendType, nil -} - -func GetKeyRing() (keyring.Keyring, error) { - currentVaultBackend, err := GetCurrentVaultBackend() - if err != nil { - return nil, fmt.Errorf("GetKeyRing: unable to get the current vault backend, [err=%s]", err) - } - - keyringInstanceConfig := keyring.Config{ - FilePasswordFunc: fileKeyringPassphrasePrompt, - ServiceName: KEYRING_SERVICE_NAME, - LibSecretCollectionName: KEYRING_SERVICE_NAME, - KWalletAppID: KEYRING_SERVICE_NAME, - KWalletFolder: KEYRING_SERVICE_NAME, - KeychainName: "login", // default so user will not be prompted - KeychainTrustApplication: true, - WinCredPrefix: KEYRING_SERVICE_NAME, - FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME), - KeychainAccessibleWhenUnlocked: true, - } - - // if the user explicitly sets a vault backend, then only use that - if currentVaultBackend != "" { - keyringInstanceConfig.AllowedBackends = []keyring.BackendType{keyring.BackendType(currentVaultBackend)} - } - - keyringInstance, err := keyring.Open(keyringInstanceConfig) - if err != nil { - return nil, fmt.Errorf("GetKeyRing: Unable to create instance of Keyring because of [err=%s]", err) - } - - return keyringInstance, nil -} - -func fileKeyringPassphrasePrompt(prompt string) (string, error) { - if password, ok := os.LookupEnv("VAULT_PASS"); ok { - return password, nil - } else if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok { - return password, nil - } else { - fmt.Println("To avoid repeatedly typing your password, set the environment variable `VAULT_PASS` to your password") - } - - fmt.Fprintf(os.Stderr, "%s:", prompt) - b, err := term.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return "", err - } - - fmt.Println("") - return string(b), nil -} From 9dac06744b8abb8e73087bd70a5a68be97cbd179 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 2 Aug 2023 10:57:04 -0400 Subject: [PATCH 06/11] delay cli update notification --- cli/packages/util/check-for-update.go | 42 +++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/cli/packages/util/check-for-update.go b/cli/packages/util/check-for-update.go index e59c04326..9921cef0d 100644 --- a/cli/packages/util/check-for-update.go +++ b/cli/packages/util/check-for-update.go @@ -11,6 +11,7 @@ import ( "os/exec" "runtime" "strings" + "time" "github.com/fatih/color" "github.com/rs/zerolog/log" @@ -20,13 +21,16 @@ func CheckForUpdate() { if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" { return } - latestVersion, err := getLatestTag("Infisical", "infisical") + latestVersion, publishedDate, err := getLatestTag("Infisical", "infisical") if err != nil { log.Debug().Err(err) // do nothing and continue return } - if latestVersion != CLI_VERSION { + + daysSinceRelease, _ := daysSinceDate(publishedDate) + + if latestVersion != CLI_VERSION && daysSinceRelease > 2 { yellow := color.New(color.FgYellow).SprintFunc() blue := color.New(color.FgCyan).SprintFunc() black := color.New(color.FgBlack).SprintFunc() @@ -50,37 +54,38 @@ func CheckForUpdate() { } } -func getLatestTag(repoOwner string, repoName string) (string, error) { +func getLatestTag(repoOwner string, repoName string) (string, string, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName) resp, err := http.Get(url) if err != nil { - return "", err + return "", "", err } if resp.StatusCode != 200 { - return "", errors.New(fmt.Sprintf("gitHub API returned status code %d", resp.StatusCode)) + return "", "", errors.New(fmt.Sprintf("gitHub API returned status code %d", resp.StatusCode)) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return "", err + return "", "", err } - var releaseTag struct { - TagName string `json:"tag_name"` + var releaseDetails struct { + TagName string `json:"tag_name"` + PublishedAt string `json:"published_at"` } - if err := json.Unmarshal(body, &releaseTag); err != nil { - return "", fmt.Errorf("failed to unmarshal github response: %w", err) + if err := json.Unmarshal(body, &releaseDetails); err != nil { + return "", "", fmt.Errorf("failed to unmarshal github response: %w", err) } tag_prefix := "infisical-cli/v" // Extract the version from the first valid tag - version := strings.TrimPrefix(releaseTag.TagName, tag_prefix) + version := strings.TrimPrefix(releaseDetails.TagName, tag_prefix) - return version, nil + return version, releaseDetails.PublishedAt, nil } func GetUpdateInstructions() string { @@ -145,3 +150,16 @@ func IsRunningInDocker() bool { return strings.Contains(string(cgroup), "docker") } + +func daysSinceDate(dateString string) (int, error) { + layout := "2006-01-02T15:04:05Z" + parsedDate, err := time.Parse(layout, dateString) + if err != nil { + return 0, err + } + + currentTime := time.Now() + difference := currentTime.Sub(parsedDate) + days := int(difference.Hours() / 24) + return days, nil +} From bd8397bda757894cd0d7c83aef92a91ae2c315c0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 2 Aug 2023 11:23:22 -0400 Subject: [PATCH 07/11] add status code and url to CallGetAccessibleEnvironments --- cli/packages/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 89c8db76a..06617afd3 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -196,7 +196,7 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib } if response.IsError() { - return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response) + return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v] [response-code=%v] [url=%s]", response, response.StatusCode(), response.Request.URL) } return accessibleEnvironmentsResponse, nil From d1749deff0ba78853f006758468286829e3b2b1e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 2 Aug 2023 12:41:04 -0400 Subject: [PATCH 08/11] enable checkIPAllowlist --- backend/src/routes/v3/secrets.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 5a33e30ff..6d3b4911d 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -57,7 +57,7 @@ router.get( requiredPermissions: [PERMISSION_READ_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: true, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.getSecretByNameRaw ); @@ -86,7 +86,7 @@ router.post( requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: true, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.createSecretRaw ); @@ -115,7 +115,7 @@ router.patch( requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: true, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.updateSecretByNameRaw ); @@ -143,7 +143,7 @@ router.delete( requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: true, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.deleteSecretByNameRaw ); @@ -169,7 +169,7 @@ router.get( requiredPermissions: [PERMISSION_READ_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: false, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.getSecrets ); @@ -205,7 +205,7 @@ router.post( requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: false, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.createSecret ); @@ -232,7 +232,7 @@ router.get( locationEnvironment: "query", requiredPermissions: [PERMISSION_READ_SECRETS], requireBlindIndicesEnabled: true, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.getSecretByName ); @@ -263,7 +263,7 @@ router.patch( requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: false, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.updateSecretByName ); @@ -291,7 +291,7 @@ router.delete( requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, requireE2EEOff: false, - checkIPAllowlist: false + checkIPAllowlist: true }), secretsController.deleteSecretByName ); From 23e40e523ae3d85c04296611bbf3c587a08e7c66 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 2 Aug 2023 17:41:45 -0400 Subject: [PATCH 09/11] highlight infisical version in k8 docs --- .../deployment-options/kubernetes-helm.mdx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 36b3c5213..e0843c4fa 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -7,9 +7,6 @@ description: "Use our Helm chart to Install Infisical on your Kubernetes cluster - Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater - You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster - - - By deploying Infisical on Kubernetes, you can take advantage of its features to ensure that the application is fault-tolerant, highly available, and scalable. To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes. @@ -34,10 +31,11 @@ By default, the application will use the latest tag to retrieve the required Doc However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment. View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters). - -To determine the appropriate versions to use for the docker images, follow the links bellow -- [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags) -- [backend Docker image](https://hub.docker.com/r/infisical/backend/tags) + + To find the latest version number of Infisical, follow the links bellow + - [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags) + - [backend Docker image](https://hub.docker.com/r/infisical/backend/tags) + ```yaml simple-values-example.yaml frontend: @@ -45,14 +43,14 @@ frontend: replicaCount: 2 image: repository: infisical/frontend - tag: "v0.1.3" + tag: "v0.26.0" # <--- frontend version pullPolicy: Always backend: replicaCount: 2 image: repository: infisical/backend - tag: "v0.1.3" + tag: "v0.26.0" # <--- backend version pullPolicy: Always ``` From a3b8de2e84df9d0f7f5e2bd1a71e337842f481ac Mon Sep 17 00:00:00 2001 From: vmatsiiako <78047717+vmatsiiako@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:37:59 -0700 Subject: [PATCH 10/11] Update mint.json --- docs/mint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/mint.json b/docs/mint.json index b1ee77ba5..4d5af11d0 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -23,7 +23,8 @@ }, "feedback": { "suggestEdit": true, - "raiseIssue": true + "raiseIssue": true, + "thumbsRating": true }, "api": { "baseUrl": ["https://app.infisical.com", "http://localhost:8080"], From 3990b6dc49232ba7c0605575dbd5ec00c66990e2 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 2 Aug 2023 20:01:06 -0700 Subject: [PATCH 11/11] fixed the autocapitalization ability --- frontend/src/components/v2/Input/Input.tsx | 13 ++++++++++++- frontend/src/views/DashboardPage/DashboardPage.tsx | 1 + .../components/SecretInputRow/SecretInputRow.tsx | 5 ++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx index 5dd11642d..17573e527 100644 --- a/frontend/src/components/v2/Input/Input.tsx +++ b/frontend/src/components/v2/Input/Input.tsx @@ -1,4 +1,4 @@ -import { forwardRef, InputHTMLAttributes, ReactNode } from "react"; +import { ChangeEvent, forwardRef, InputHTMLAttributes, ReactNode } from "react"; import { cva, VariantProps } from "cva"; import { twMerge } from "tailwind-merge"; @@ -10,6 +10,7 @@ type Props = { rightIcon?: ReactNode; isDisabled?: boolean; isReadOnly?: boolean; + autoCapitalization?: boolean; }; const inputVariants = cva( @@ -80,10 +81,19 @@ export const Input = forwardRef( variant = "filled", size = "md", isReadOnly, + autoCapitalization, ...props }, ref ): JSX.Element => { + const handleInput = (event: ChangeEvent) => { + console.log(123, props, autoCapitalization) + if (autoCapitalization) { + // eslint-disable-next-line no-param-reassign + event.target.value = event.target.value.toUpperCase(); + } + }; + return (
{leftIcon && {leftIcon}} @@ -93,6 +103,7 @@ export const Input = forwardRef( ref={ref} readOnly={isReadOnly} disabled={isDisabled} + onInput={handleInput} className={twMerge( leftIcon ? "pl-10" : "pl-2.5", rightIcon ? "pr-10" : "pr-2.5", diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 6d794cbec..5763128e4 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -971,6 +971,7 @@ export const DashboardPage = () => { register={register} control={control} setValue={setValue} + autoCapitalization={currentWorkspace?.autoCapitalization} /> ))} {!isReadOnly && !isRollbackMode && ( diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 899e0197f..a2f95e4a4 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -79,6 +79,7 @@ type Props = { setValue: UseFormSetValue; isKeyError?: boolean; keyError?: string; + autoCapitalization?: boolean; }; export const SecretInputRow = memo( @@ -98,7 +99,8 @@ export const SecretInputRow = memo( setValue, isKeyError, keyError, - secUniqId + secUniqId, + autoCapitalization }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); // comment management in a row @@ -243,6 +245,7 @@ export const SecretInputRow = memo( isKeySubDisabled.current = false; field.onBlur(); }} + autoCapitalization={autoCapitalization} />