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" } } );