diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 7f5432321..b473bcbd2 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -137,6 +137,12 @@ export const addOrganizationPmtMethod = async (req: Request, res: Response) => { }); } +/** + * Delete payment method with id [pmtMethodId] for organization + * @param req + * @param res + * @returns + */ export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { const { pmtMethodId } = req.params; @@ -206,4 +212,18 @@ export const getOrganizationInvoices = async (req: Request, res: Response) => { ); return res.status(200).send(invoices); +} + +/** + * Return organization's licenses on file + * @param req + * @param res + * @returns + */ +export const getOrganizationLicenses = async (req: Request, res: Response) => { + const { data: { licenses } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses` + ); + + return res.status(200).send(licenses); } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index c9104d964..5c308466d 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -220,4 +220,18 @@ router.get( organizationsController.getOrganizationInvoices ); +router.get( + "/:organizationId/licenses", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + validateRequest, + organizationsController.getOrganizationLicenses +); + export default router; \ No newline at end of file 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..17e5c93ce 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,164 @@ 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, + recursionChainBreaker: Record, + key: string +) => { + if (expandedSec?.[key]) { + return expandedSec[key]; + } + if (recursionChainBreaker?.[key]) { + return ""; + } + recursionChainBreaker[key] = true; + + 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, + recursionChainBreaker, + interpolationKey + ); + if (val) { + interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); + } + continue; + } + + 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.replaceAll(interpolationSyntax, val); + } + } + } + + expandedSec[key] = interpolatedValue; + 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, + 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 = formatMultiValueEnv(expandedSec[key]); + 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 = formatMultiValueEnv(expandedVal); + } + + return secrets; +}; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 956e8e1bf..f928d3029 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -52,7 +52,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"; @@ -60,6 +60,14 @@ import _ from "lodash"; import sodium from "libsodium-wrappers"; import { standardRequest } from "../config/request"; +const getSecretKeyValuePair = ( + secrets: Record +) => + Object.keys(secrets).reduce>((prev, key) => { + if (secrets[key]) prev[key] = secrets[key]?.value || ""; + return prev; + }, {}); + /** * Sync/push [secrets] to [app] in integration named [integration] * @param {Object} obj @@ -75,22 +83,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: @@ -98,7 +104,7 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_AWS_SECRET_MANAGER: @@ -106,14 +112,14 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_HEROKU: await syncSecretsHeroku({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_VERCEL: @@ -121,7 +127,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }); break; case INTEGRATION_NETLIFY: @@ -129,49 +135,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: @@ -179,35 +185,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: @@ -216,7 +222,7 @@ const syncSecrets = async ({ integrationAuth, secrets, accessId, - accessToken, + accessToken }); break; case INTEGRATION_CLOUDFLARE_PAGES: @@ -231,7 +237,7 @@ const syncSecrets = async ({ await syncSecretsCodefresh({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_TEAMCITY: @@ -246,14 +252,14 @@ const syncSecrets = async ({ await syncSecretsBitBucket({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: await syncSecretsDigitalOceanAppPlatform({ integration, secrets, - accessToken, + accessToken }); break; case INTEGRATION_CLOUD_66: @@ -272,13 +278,12 @@ const syncSecrets = async ({ break; case INTEGRATION_WINDMILL: await syncSecretsWindmill({ - integration, - secrets, - accessToken, - secretComments + integration, + secrets, + accessToken }); break; - } + } }; /** @@ -291,21 +296,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 { @@ -315,15 +320,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); @@ -332,31 +337,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; @@ -369,14 +385,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 }); } } @@ -395,7 +411,7 @@ const syncSecretsAzureKeyVault = async ({ key, value, integration, - accessToken, + accessToken }: { key: string; value: string; @@ -411,36 +427,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) { @@ -449,7 +465,7 @@ const syncSecretsAzureKeyVault = async ({ key, value, integration, - accessToken, + accessToken }); } @@ -457,8 +473,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}` + } }); } }; @@ -475,10 +491,10 @@ const syncSecretsAWSParameterStore = async ({ integration, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { @@ -487,31 +503,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 @@ -519,24 +538,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(); } } }); @@ -544,20 +567,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 @@ -571,34 +596,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 }) ); @@ -608,32 +634,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] @@ -645,23 +675,20 @@ 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) => { @@ -672,13 +699,13 @@ const syncSecretsHeroku = async ({ 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" + } } ); }; @@ -693,11 +720,11 @@ const syncSecretsVercel = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessToken: string; }) => { interface VercelSecret { @@ -714,52 +741,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 { @@ -777,12 +806,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 + } + : {}) }); } }); @@ -790,19 +821,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 { @@ -813,9 +846,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 + } + : {}) }); } }); @@ -829,8 +864,8 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -839,14 +874,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" + } } ); } @@ -859,8 +894,8 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -878,11 +913,11 @@ const syncSecretsNetlify = async ({ integration, integrationAuth, secrets, - accessToken, + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessToken: string; }) => { interface NetlifyValue { @@ -896,13 +931,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 = ( @@ -912,14 +943,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 }), {} ); @@ -937,17 +968,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 }), {} ); @@ -962,9 +993,9 @@ const syncSecretsNetlify = async ({ values: [ { context: integration.targetEnvironment, - value: secrets[key], - }, - ], + value: secrets[key].value + } + ] }); } } else { @@ -975,9 +1006,9 @@ const syncSecretsNetlify = async ({ values: [ { context: integration.targetEnvironment, - value: secrets[key], - }, - ], + value: secrets[key].value + } + ] }); } } @@ -1005,9 +1036,9 @@ const syncSecretsNetlify = async ({ { id: value.id, context: integration.targetEnvironment, - value: value.value, - }, - ], + value: value.value + } + ] }); } } @@ -1016,7 +1047,7 @@ const syncSecretsNetlify = async ({ }); const syncParams = new URLSearchParams({ - site_id: integration.appId, + site_id: integration.appId }); if (newSecrets.length > 0) { @@ -1027,8 +1058,8 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); } @@ -1039,14 +1070,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" + } } ); }); @@ -1060,8 +1091,8 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }); @@ -1075,8 +1106,8 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); }); @@ -1094,10 +1125,10 @@ const syncSecretsNetlify = async ({ const syncSecretsGitHub = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface GitHubRepoKey { @@ -1115,47 +1146,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 + }); } }); @@ -1163,31 +1186,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 + }); }); }); }; @@ -1202,23 +1216,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" + } } ); }; @@ -1234,33 +1248,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" + } } ); }; @@ -1275,10 +1288,10 @@ const syncSecretsLaravelForge = async ({ const syncSecretsRailway = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { const query = ` @@ -1292,22 +1305,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 @@ -1319,10 +1336,10 @@ const syncSecretsRailway = async ({ const syncSecretsFlyio = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { // set secrets @@ -1346,23 +1363,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 { @@ -1381,18 +1402,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)) @@ -1417,21 +1444,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" + } + } + ); }; /** @@ -1444,18 +1475,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]; @@ -1468,13 +1499,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" + } } ) ); @@ -1486,8 +1517,8 @@ const syncSecretsCircleCI = async ({ { headers: { "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ) ).data?.items; @@ -1500,8 +1531,8 @@ const syncSecretsCircleCI = async ({ { headers: { "Circle-Token": accessToken, - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } } ); } @@ -1509,7 +1540,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) @@ -1518,30 +1549,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)) { @@ -1553,15 +1584,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 { @@ -1572,15 +1603,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" + } } ); } @@ -1593,15 +1624,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] @@ -1614,10 +1645,10 @@ const syncSecretsTravisCI = async ({ const syncSecretsGitLab = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface GitLabSecret { @@ -1629,8 +1660,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[] = []; @@ -1654,8 +1685,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)) { @@ -1665,55 +1696,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] @@ -1726,10 +1757,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( @@ -1737,20 +1768,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`, @@ -1758,8 +1787,8 @@ const syncSecretsSupabase = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); @@ -1776,14 +1805,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 @@ -1794,30 +1822,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)) { @@ -1829,15 +1855,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 { @@ -1848,15 +1874,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 + } } ); } @@ -1866,16 +1892,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 } - ); + }); } } }; @@ -1890,29 +1913,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)) { @@ -1925,22 +1950,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}`, @@ -1950,16 +1975,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" + } } ); } @@ -1969,13 +1994,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" + } + } + ); } } }; @@ -2075,11 +2103,11 @@ const syncSecretsHashiCorpVault = async ({ integrationAuth, secrets, accessId, - accessToken, + accessToken }: { integration: IIntegration; integrationAuth: IIntegrationAuth; - secrets: any; + secrets: Record; accessId: string | null; accessToken: string; }) => { @@ -2088,20 +2116,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 + } } ); @@ -2110,16 +2138,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 + } } ); }; @@ -2135,14 +2163,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( @@ -2150,15 +2177,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 }; @@ -2175,9 +2201,9 @@ const syncSecretsCloudflarePages = async ({ } const data = { - "deployment_configs": { + deployment_configs: { [integration.targetEnvironment]: { - "env_vars": secretsObj + env_vars: secretsObj } } }; @@ -2188,11 +2214,11 @@ const syncSecretsCloudflarePages = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + Accept: "application/json" + } } ); -} +}; /** * Sync/push [secrets] to BitBucket repo with name [integration.app] @@ -2205,10 +2231,10 @@ const syncSecretsCloudflarePages = async ({ const syncSecretsBitBucket = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { interface VariablesResponse { @@ -2231,18 +2257,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) => { @@ -2251,9 +2274,9 @@ const syncSecretsBitBucket = async ({ } if (data.next) { - variablesUrl = data.next + variablesUrl = data.next; } else { - hasNextPage = false + hasNextPage = false; } } @@ -2261,34 +2284,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" } + } ); } } @@ -2296,18 +2319,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] @@ -2320,10 +2340,10 @@ const syncSecretsBitBucket = async ({ const syncSecretsCodefresh = async ({ integration, secrets, - accessToken, + accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { await standardRequest.patch( @@ -2331,16 +2351,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" + } } - ); + ); }; /** @@ -2357,7 +2377,7 @@ const syncSecretsDigitalOceanAppPlatform = async ({ accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { // get current app settings @@ -2374,8 +2394,9 @@ const syncSecretsDigitalOceanAppPlatform = async ({ `${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`, { spec: { + name: integration.app, ...appSettings, - envs: Object.entries(secrets).map(([key, value]) => ({ key, value })) + envs: Object.entries(secrets).map(([key, data]) => ({ key, value: data.value })) } }, { @@ -2399,13 +2420,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; @@ -2415,69 +2434,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 @@ -2487,13 +2506,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] @@ -2509,10 +2528,9 @@ const syncSecretsCloud66 = async ({ accessToken }: { integration: IIntegration; - secrets: any; + secrets: Record; accessToken: string; }) => { - interface Cloud66Secret { id: number; key: string; @@ -2536,49 +2554,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" } - ); + } + ); } } @@ -2586,14 +2602,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" } - ); + } + ); } } }; @@ -2610,20 +2626,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" } } ); 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 ); 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"] +} 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 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"], 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 ``` 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/ee/components/ActivityTable.tsx b/frontend/src/ee/components/ActivityTable.tsx index 185314791..b98f2dd7e 100644 --- a/frontend/src/ee/components/ActivityTable.tsx +++ b/frontend/src/ee/components/ActivityTable.tsx @@ -47,9 +47,10 @@ const ActivityLogsRow = ({ const { t } = useTranslation(); const renderUser = () => { + if (row?.user) return `${row.user}`; if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`; - if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`; + if (row?.serviceTokenData?.name) return `Service Token: ${row.serviceTokenData.name}`; return ""; }; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 847fb4bb9..ded8c3f29 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -7,6 +7,7 @@ export { useGetOrganization, useGetOrgBillingDetails, useGetOrgInvoices, + useGetOrgLicenses, useGetOrgPlanBillingInfo, useGetOrgPlansTable, useGetOrgPlanTable, @@ -14,5 +15,4 @@ export { useGetOrgTaxIds, useGetOrgTrialUrl, useRenameOrg, - useUpdateOrgBillingDetails -} from "./queries"; + useUpdateOrgBillingDetails} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 63de2773d..66065c6ef 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -5,14 +5,14 @@ import { apiRequest } from "@app/config/request"; import { BillingDetails, Invoice, + License, Organization, OrgPlanTable, PlanBillingInfo, PmtMethod, ProductsTable, RenameOrgDTO, - TaxID -} from "./types"; + TaxID} from "./types"; const organizationKeys = { getUserOrganization: ["organization"] as const, @@ -23,6 +23,7 @@ const organizationKeys = { getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const, getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const, getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const, + getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const }; export const useGetOrganization = () => { @@ -311,4 +312,20 @@ export const useCreateCustomerPortalSession = () => { return data; } }); -}; \ No newline at end of file +}; + +export const useGetOrgLicenses = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgLicenses(organizationId), + queryFn: async () => { + if (organizationId === "") return undefined; + + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/licenses` + ); + + return data; + }, + enabled: true + }); +} \ No newline at end of file diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index a33a7f875..cc4ad8e70 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -49,6 +49,17 @@ export type TaxID = { value: string; } +export type License = { + _id: string; + customerId: string; + prefix: string; + licenseKey: string; + isActivated: boolean; + expiresAt: string; + createdAt: string; + updatedAt: string; +} + export type OrgPlanTableHead = { name: string; } 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} />
diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx index e90d6ca8b..75fdf2f99 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx @@ -33,7 +33,7 @@ export const CurrentPlanSection = () => { return (
-

Current Usage

+

Current usage

diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx new file mode 100644 index 000000000..1ea4ca36b --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx @@ -0,0 +1,9 @@ +import { LicensesSection } from "./LicensesSection"; + +export const BillingSelfHostedTab = () => { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/LicensesSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/LicensesSection.tsx new file mode 100644 index 000000000..1603d4486 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/LicensesSection.tsx @@ -0,0 +1,68 @@ +import { faFileContract } from "@fortawesome/free-solid-svg-icons"; + +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgLicenses +} from "@app/hooks/api"; + +export const LicensesSection = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgLicenses(currentOrg?._id ?? ""); + + return ( +
+

Enterprise licenses

+ +
+ + + + + + + + + + {!isLoading && data && data?.length > 0 && data.map(({ + _id, + licenseKey, + isActivated, + createdAt, + expiresAt + }) => { + const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0]; + const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0]; + return ( + + + + + + + ); + })} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
License KeyStatusIssued DateExpiry Date
{licenseKey}{isActivated ? "Active" : "Inactive"}{formattedCreatedAt}{formattedExpiresAt}
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx new file mode 100644 index 000000000..3e443c2df --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx @@ -0,0 +1 @@ +export * from "./BillingSelfHostedTab"; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx index 417f06165..d30fa6ffa 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx @@ -4,9 +4,11 @@ import { Tab } from "@headlessui/react" import { BillingCloudTab } from "../BillingCloudTab"; import { BillingDetailsTab } from "../BillingDetailsTab"; import { BillingReceiptsTab } from "../BillingReceiptsTab"; +import { BillingSelfHostedTab } from "../BillingSelfHostedTab"; const tabs = [ { name: "Infisical Cloud", key: "tab-infisical-cloud" }, + { name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" }, { name: "Receipts", key: "tab-receipts" }, { name: "Billing details", key: "tab-billing-details" } ]; @@ -32,6 +34,9 @@ export const BillingTabGroup = () => { + + +