diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 5119761d0..63828ef14 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,10 +1,11 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { - Integration -} from '../../models'; -import { EventService } from '../../services'; -import { eventPushSecrets } from '../../events'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Integration } from "../../models"; +import { EventService } from "../../services"; +import { eventPushSecrets } from "../../events"; +import Folder from "../../models/folder"; +import { getFolderByPath } from "../../services/FolderService"; +import { BadRequestError } from "../../utils/errors"; /** * Create/initialize an (empty) integration for integration authorization @@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => { targetServiceId, owner, path, - region + region, + secretPath, } = req.body; - + + const folders = await Folder.findOne({ + workspace: req.integrationAuth.workspace._id, + environment: sourceEnvironment, + }); + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw BadRequestError({ + message: "Path for service token does not exist", + }); + } + } + // TODO: validate [sourceEnvironment] and [targetEnvironment] // initialize new integration after saving integration access token @@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => { owner, path, region, + secretPath, integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId) + integrationAuth: new Types.ObjectId(integrationAuthId), }).save(); - + if (integration) { // trigger event - push secrets EventService.handleEvent({ event: eventPushSecrets({ workspaceId: integration.workspace, - environment: sourceEnvironment - }) + environment: sourceEnvironment, + }), }); } @@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => { * @returns */ export const updateIntegration = async (req: Request, res: Response) => { - // TODO: add integration-specific validation to ensure that each // integration has the correct fields populated in [Integration] @@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, // github-specific integration param + secretPath, } = req.body; + const folders = await Folder.findOne({ + workspace: req.integration.workspace, + environment, + }); + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw BadRequestError({ + message: "Path for service token does not exist", + }); + } + } + const integration = await Integration.findOneAndUpdate( { _id: req.integration._id, @@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, + secretPath, }, { new: true, @@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => { EventService.handleEvent({ event: eventPushSecrets({ workspaceId: integration.workspace, - environment + environment, }), }); } diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 439f7c531..8c3b5b816 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -1,29 +1,21 @@ import { Types } from "mongoose"; -import { - Bot, - BotKey, - Secret, - ISecret, - IUser -} from "../models"; +import { Bot, BotKey, Secret, ISecret, IUser } from "../models"; import { generateKeyPair, encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8, - decryptAsymmetric -} from '../utils/crypto'; + decryptAsymmetric, +} from "../utils/crypto"; import { SECRET_SHARED, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + ENCODING_SCHEME_BASE64, } from "../variables"; -import { - getEncryptionKey, - getRootEncryptionKey, - client -} from "../config"; +import { getEncryptionKey, getRootEncryptionKey, client } from "../config"; import { InternalServerError } from "../utils/errors"; +import Folder from "../models/folder"; +import { getFolderByPath } from "../services/FolderService"; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -40,15 +32,14 @@ export const createBot = async ({ }) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); - + 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, @@ -59,9 +50,8 @@ 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, @@ -77,12 +67,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", }); }; @@ -92,11 +82,11 @@ export const createBot = async ({ */ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { const botKey = await BotKey.exists({ - workspace: workspaceId - }); - + workspace: workspaceId, + }); + return botKey ? false : true; -} +}; /** * Return decrypted secrets for workspace with id [workspaceId] @@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { export const getSecretsBotHelper = 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, + }); + + if (!folders && secretPath !== "/") { + throw InternalServerError({ message: "Folder not found" }); + } + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw InternalServerError({ message: "Folder not found" }); + } + folderId = folder.id; + } + const secrets = await Secret.find({ workspace: workspaceId, environment, type: SECRET_SHARED, + folder: folderId, }); secrets.forEach((secret: ISecret) => { @@ -148,14 +160,17 @@ 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, - }) - .populate<{ sender: IUser }>("sender", "publicKey"); + }).populate<{ sender: IUser }>("sender", "publicKey"); if (!botKey) throw new Error("Failed to find bot key"); @@ -168,7 +183,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) = if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { // case: encoding scheme is base64 - const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); + const privateKeyBot = client.decryptSymmetric( + bot.encryptedPrivateKey, + rootEncryptionKey, + bot.iv, + bot.tag + ); return decryptAsymmetric({ ciphertext: botKey.encryptedKey, @@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) = privateKey: privateKeyBot, }); } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { - // case: encoding scheme is utf8 const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: encryptionKey + key: encryptionKey, }); - + return decryptAsymmetric({ ciphertext: botKey.encryptedKey, nonce: botKey.nonce, @@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) = } 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", }); }; @@ -254,4 +274,4 @@ export const decryptSymmetricHelper = async ({ }); return plaintext; -}; \ No newline at end of file +}; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index b6f4b0915..baf41d762 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -1,26 +1,20 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; +import { Bot, Integration, IntegrationAuth } from "../models"; +import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations"; +import { BotService } from "../services"; import { - Bot, - Integration, - IntegrationAuth -} from '../models'; -import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; -import { BotService } from '../services'; -import { - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8 -} from '../variables'; -import { - UnauthorizedRequestError, -} from '../utils/errors'; + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, +} from "../variables"; +import { UnauthorizedRequestError } from "../utils/errors"; interface Update { - workspace: string; - integration: string; - teamId?: string; - accountId?: string; + workspace: string; + integration: string; + teamId?: string; + accountId?: string; } /** @@ -31,78 +25,83 @@ interface Update { * - Create bot sequence for integration * @param {Object} obj * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.integration - name of integration + * @param {String} obj.integration - name of integration * @param {String} obj.code - code * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange -*/ + */ export const handleOAuthExchangeHelper = async ({ - workspaceId, + workspaceId, + integration, + code, + environment, +}: { + workspaceId: string; + integration: string; + code: string; + environment: string; +}) => { + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true, + }); + + 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, - environment -}: { - workspaceId: string; - integration: string; - code: string; - environment: string; -}) => { - const bot = await Bot.findOne({ - workspace: workspaceId, - isActive: true + }); + + const update: Update = { + workspace: workspaceId, + integration, + }; + + switch (integration) { + case INTEGRATION_VERCEL: + update.teamId = res.teamId; + break; + case INTEGRATION_NETLIFY: + update.accountId = res.accountId; + break; + } + + const integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + workspace: workspaceId, + integration, + }, + update, + { + new: true, + upsert: true, + } + ); + + if (res.refreshToken) { + // case: refresh token returned from exchange + // set integration auth refresh token + await setIntegrationAuthRefreshHelper({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: res.refreshToken, }); - - 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 + } + + if (res.accessToken) { + // case: access token returned from exchange + // set integration auth access token + await setIntegrationAuthAccessHelper({ + integrationAuthId: integrationAuth._id.toString(), + accessId: null, + accessToken: res.accessToken, + accessExpiresAt: res.accessExpiresAt, }); - - const update: Update = { - workspace: workspaceId, - integration - } - - switch (integration) { - case INTEGRATION_VERCEL: - update.teamId = res.teamId; - break; - case INTEGRATION_NETLIFY: - update.accountId = res.accountId; - break; - } - - const integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: workspaceId, - integration - }, update, { - new: true, - upsert: true - }); - - if (res.refreshToken) { - // case: refresh token returned from exchange - // set integration auth refresh token - await setIntegrationAuthRefreshHelper({ - integrationAuthId: integrationAuth._id.toString(), - refreshToken: res.refreshToken - }); - } - - if (res.accessToken) { - // case: access token returned from exchange - // set integration auth access token - await setIntegrationAuthAccessHelper({ - integrationAuthId: integrationAuth._id.toString(), - accessId: null, - accessToken: res.accessToken, - accessExpiresAt: res.accessExpiresAt - }); - } - - return integrationAuth; -} + } + + return integrationAuth; +}; /** * Sync/push environment variables in workspace with id [workspaceId] to * all active integrations for that workspace @@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({ * @param {Object} obj.workspaceId - id of workspace */ export const syncIntegrationsHelper = async ({ - workspaceId, - environment + workspaceId, + environment, }: { - workspaceId: Types.ObjectId; - environment?: string; + workspaceId: Types.ObjectId; + environment?: string; }) => { - const integrations = await Integration.find({ - workspace: workspaceId, - ...(environment ? { - environment - } : {}), - isActive: true, - app: { $ne: null } + const integrations = await Integration.find({ + workspace: workspaceId, + ...(environment + ? { + environment, + } + : {}), + isActive: true, + app: { $ne: null }, + }); + + // for each workspace integration, sync/push secrets + // to that integration + for await (const integration of integrations) { + // get workspace, environment (shared) secrets + const secrets = await BotService.getSecrets({ + // issue here? + workspaceId: integration.workspace, + environment: integration.environment, + secretPath: integration.secretPath, }); - // for each workspace integration, sync/push secrets - // to that integration - for await (const integration of integrations) { - // get workspace, environment (shared) secrets - const secrets = await BotService.getSecrets({ // issue here? - workspaceId: integration.workspace, - environment: integration.environment - }); + const integrationAuth = await IntegrationAuth.findById( + integration.integrationAuth + ); + if (!integrationAuth) throw new Error("Failed to find integration auth"); - 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 - }); + // get integration auth access token + const access = await getIntegrationAuthAccessHelper({ + integrationAuthId: integration.integrationAuth, + }); - // sync secrets to integration - await syncSecrets({ - integration, - integrationAuth, - secrets, - accessId: access.accessId === undefined ? null : access.accessId, - accessToken: access.accessToken - }); - } -} + // sync secrets to integration + await syncSecrets({ + integration, + integrationAuth, + secrets, + accessId: access.accessId === undefined ? null : access.accessId, + accessToken: access.accessToken, + }); + } +}; /** * Return decrypted refresh token using the bot's copy @@ -161,22 +166,29 @@ export const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ -export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('+refreshCiphertext +refreshIV +refreshTag'); +export const getIntegrationAuthRefreshHelper = async ({ + integrationAuthId, +}: { + integrationAuthId: Types.ObjectId; +}) => { + const integrationAuth = await IntegrationAuth.findById( + integrationAuthId + ).select("+refreshCiphertext +refreshIV +refreshTag"); - if (!integrationAuth) throw UnauthorizedRequestError({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 + if (!integrationAuth) + throw UnauthorizedRequestError({ + message: "Failed to locate Integration Authentication credentials", }); - - return refreshToken; -} + + const refreshToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.refreshCiphertext as string, + iv: integrationAuth.refreshIV as string, + tag: integrationAuth.refreshTag as string, + }); + + return refreshToken; +}; /** * Return decrypted access token using the bot's copy @@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i * @param {String} obj.integrationAuthId - id of integration auth * @returns {String} accessToken - decrypted access token */ -export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - let accessId; - let accessToken; - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); +export const getIntegrationAuthAccessHelper = async ({ + integrationAuthId, +}: { + integrationAuthId: Types.ObjectId; +}) => { + let accessId; + let accessToken; + 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'}); - - accessToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessCiphertext as string, - iv: integrationAuth.accessIV as string, - tag: integrationAuth.accessTag as string + if (!integrationAuth) + throw UnauthorizedRequestError({ + message: "Failed to locate Integration Authentication credentials", }); - if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { - // there is a access token expiration date - // and refresh token to exchange with the OAuth2 server - - if (integrationAuth.accessExpiresAt < new Date()) { - // access token is expired - const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); - accessToken = await exchangeRefresh({ - integrationAuth, - refreshToken - }); - } + accessToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessCiphertext as string, + iv: integrationAuth.accessIV as string, + tag: integrationAuth.accessTag as string, + }); + + if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { + // there is a access token expiration date + // and refresh token to exchange with the OAuth2 server + + if (integrationAuth.accessExpiresAt < new Date()) { + // access token is expired + const refreshToken = await getIntegrationAuthRefreshHelper({ + integrationAuthId, + }); + accessToken = await exchangeRefresh({ + integrationAuth, + refreshToken, + }); } - - if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { - accessId = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessIdCiphertext as string, - iv: integrationAuth.accessIdIV as string, - tag: integrationAuth.accessIdTag as string - }); - } - - return ({ - accessId, - accessToken + } + + if ( + integrationAuth?.accessIdCiphertext && + integrationAuth?.accessIdIV && + integrationAuth?.accessIdTag + ) { + accessId = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessIdCiphertext as string, + iv: integrationAuth.accessIdIV as string, + tag: integrationAuth.accessIdTag as string, }); -} + } + + return { + accessId, + accessToken, + }; +}; /** * Encrypt refresh token [refreshToken] using the bot's copy @@ -240,41 +267,43 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in * @param {String} obj.refreshToken - refresh token */ export const setIntegrationAuthRefreshHelper = async ({ - integrationAuthId, - refreshToken + integrationAuthId, + refreshToken, }: { - integrationAuthId: string; - refreshToken: string; + integrationAuthId: string; + refreshToken: string; }) => { - - let integrationAuth = await IntegrationAuth - .findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const obj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: refreshToken - }); - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - refreshCiphertext: obj.ciphertext, - refreshIV: obj.iv, - refreshTag: obj.tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - - return integrationAuth; -} + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error("Failed to find integration auth"); + + const obj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: refreshToken, + }); + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + _id: integrationAuthId, + }, + { + refreshCiphertext: obj.ciphertext, + refreshIV: obj.iv, + refreshTag: obj.tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + { + new: true, + } + ); + + return integrationAuth; +}; /** * Encrypt access token [accessToken] and (optionally) access id [accessId] - * using the bot's copy of the workspace key for workspace belonging to + * using the bot's copy of the workspace key for workspace belonging to * integration auth with id [integrationAuthId] and store it along with [accessExpiresAt] * @param {Object} obj * @param {String} obj.integrationAuthId - id of integration auth @@ -282,48 +311,52 @@ export const setIntegrationAuthRefreshHelper = async ({ * @param {Date} obj.accessExpiresAt - expiration date of access token */ export const setIntegrationAuthAccessHelper = async ({ - integrationAuthId, - accessId, - accessToken, - accessExpiresAt + integrationAuthId, + accessId, + accessToken, + accessExpiresAt, }: { - integrationAuthId: string; - accessId: string | null; - accessToken: string; - accessExpiresAt: Date | undefined; + integrationAuthId: string; + accessId: string | null; + accessToken: string; + accessExpiresAt: Date | undefined; }) => { - let integrationAuth = await IntegrationAuth.findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const encryptedAccessTokenObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessToken + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error("Failed to find integration auth"); + + const encryptedAccessTokenObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessToken, + }); + + let encryptedAccessIdObj; + if (accessId) { + encryptedAccessIdObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessId, }); - - let encryptedAccessIdObj; - if (accessId) { - encryptedAccessIdObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessId - }); + } + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + _id: integrationAuthId, + }, + { + accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, + accessIdIV: encryptedAccessIdObj?.iv ?? undefined, + accessIdTag: encryptedAccessIdObj?.tag ?? undefined, + accessCiphertext: encryptedAccessTokenObj.ciphertext, + accessIV: encryptedAccessTokenObj.iv, + accessTag: encryptedAccessTokenObj.tag, + accessExpiresAt, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + { + new: true, } - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, - accessIdIV: encryptedAccessIdObj?.iv ?? undefined, - accessIdTag: encryptedAccessIdObj?.tag ?? undefined, - accessCiphertext: encryptedAccessTokenObj.ciphertext, - accessIV: encryptedAccessTokenObj.iv, - accessTag: encryptedAccessTokenObj.tag, - accessExpiresAt, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - - return integrationAuth; -} \ No newline at end of file + ); + + return integrationAuth; +}; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d4fbae807..c23cf69f1 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -15,7 +15,7 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, } from "../variables"; export interface IIntegration { @@ -33,23 +33,24 @@ export interface IIntegration { targetServiceId: string; path: string; region: string; + secretPath: string; integration: - | 'azure-key-vault' - | 'aws-parameter-store' - | 'aws-secret-manager' - | 'heroku' - | 'vercel' - | 'netlify' - | 'github' - | 'gitlab' - | 'render' - | 'railway' - | 'flyio' - | 'circleci' - | 'travisci' - | 'supabase' - | 'checkly' - | 'hashicorp-vault'; + | "azure-key-vault" + | "aws-parameter-store" + | "aws-secret-manager" + | "heroku" + | "vercel" + | "netlify" + | "github" + | "gitlab" + | "render" + | "railway" + | "flyio" + | "circleci" + | "travisci" + | "supabase" + | "checkly" + | "hashicorp-vault"; integrationAuth: Types.ObjectId; } @@ -71,7 +72,7 @@ const integrationSchema = new Schema( url: { // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) type: String, - default: null + default: null, }, app: { // name of app in provider @@ -90,17 +91,17 @@ const integrationSchema = new Schema( }, targetEnvironmentId: { type: String, - default: null + default: null, }, targetService: { // railway-specific service type: String, - default: null + default: null, }, targetServiceId: { // railway-specific service type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login @@ -111,12 +112,12 @@ const integrationSchema = new Schema( // aws-parameter-store-specific path // (also) vercel preview-branch type: String, - default: null + default: null, }, region: { // aws-parameter-store-specific path type: String, - default: null + default: null, }, integration: { type: String, @@ -136,7 +137,7 @@ const integrationSchema = new Schema( INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, ], required: true, }, @@ -145,6 +146,11 @@ const integrationSchema = new Schema( ref: "IntegrationAuth", required: true, }, + secretPath: { + type: String, + required: true, + default: "/", + }, }, { timestamps: true, diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index b8e0b38bd..c4ba329c0 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -1,75 +1,77 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireAuth, - requireIntegrationAuth, - requireIntegrationAuthorizationAuth, - validateRequest -} from '../../middleware'; + requireAuth, + requireIntegrationAuth, + requireIntegrationAuthorizationAuth, + validateRequest, +} from "../../middleware"; import { - ADMIN, - MEMBER, - AUTH_MODE_JWT, - AUTH_MODE_API_KEY -} from '../../variables'; -import { body, param } from 'express-validator'; -import { integrationController } from '../../controllers/v1'; + ADMIN, + MEMBER, + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, +} from "../../variables"; +import { body, param } from "express-validator"; +import { integrationController } from "../../controllers/v1"; router.post( - '/', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - location: 'body' - }), - body('integrationAuthId').exists().isString().trim(), - body('app').trim(), - body('isActive').exists().isBoolean(), - body('appId').trim(), - body('sourceEnvironment').trim(), - body('targetEnvironment').trim(), - body('targetEnvironmentId').trim(), - body('targetService').trim(), - body('targetServiceId').trim(), - body('owner').trim(), - body('path').trim(), - body('region').trim(), - validateRequest, - integrationController.createIntegration + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER], + location: "body", + }), + body("integrationAuthId").exists().isString().trim(), + body("app").trim(), + body("isActive").exists().isBoolean(), + body("appId").trim(), + body("secretPath").default("/").isString().trim(), + body("sourceEnvironment").trim(), + body("targetEnvironment").trim(), + body("targetEnvironmentId").trim(), + body("targetService").trim(), + body("targetServiceId").trim(), + body("owner").trim(), + body("path").trim(), + body("region").trim(), + validateRequest, + integrationController.createIntegration ); router.patch( - '/:integrationId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('integrationId').exists().trim(), - body('isActive').exists().isBoolean(), - body('app').exists().trim(), - body('environment').exists().trim(), - body('appId').exists(), - body('targetEnvironment').exists(), - body('owner').exists(), - validateRequest, - integrationController.updateIntegration + "/:integrationId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationId").exists().trim(), + body("isActive").exists().isBoolean(), + body("app").exists().trim(), + body("secretPath").default("/").isString().trim(), + body("environment").exists().trim(), + body("appId").exists(), + body("targetEnvironment").exists(), + body("owner").exists(), + validateRequest, + integrationController.updateIntegration ); router.delete( - '/:integrationId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('integrationId').exists().trim(), - validateRequest, - integrationController.deleteIntegration + "/:integrationId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationId").exists().trim(), + validateRequest, + integrationController.deleteIntegration ); export default router; diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index 7081a64cb..0e3768a9c 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -1,110 +1,112 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - getSecretsBotHelper, - encryptSymmetricHelper, - decryptSymmetricHelper, - getKey, - getIsWorkspaceE2EEHelper -} from '../helpers/bot'; + getSecretsBotHelper, + encryptSymmetricHelper, + decryptSymmetricHelper, + getKey, + getIsWorkspaceE2EEHelper, +} from "../helpers/bot"; /** * Class to handle bot actions */ class BotService { - - /** - * Return whether or not workspace with id [workspaceId] is end-to-end encrypted - * @param workspaceId - id of workspace - * @returns {Boolean} - */ - static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { - return await getIsWorkspaceE2EEHelper(workspaceId); - } + /** + * Return whether or not workspace with id [workspaceId] is end-to-end encrypted + * @param workspaceId - id of workspace + * @returns {Boolean} + */ + static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { + return await getIsWorkspaceE2EEHelper(workspaceId); + } - /** - * Get workspace key for workspace with id [workspaceId] shared to bot. - * @param {Object} obj - * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for - * @returns - */ - static async getWorkspaceKeyWithBot({ - workspaceId - }: { - workspaceId: Types.ObjectId; - }) { - return await getKey({ - workspaceId - }); - } - - /** - * Return decrypted secrets for workspace with id [workspaceId] and - * environment [environmen] shared to bot. - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace of secrets - * @param {String} obj.environment - environment for secrets - * @returns {Object} secretObj - object where keys are secret keys and values are secret values - */ - static async getSecrets({ - workspaceId, - environment - }: { - workspaceId: Types.ObjectId; - environment: string; - }) { - return await getSecretsBotHelper({ - workspaceId, - environment - }); - } - - /** - * Return symmetrically encrypted [plaintext] using the - * bot's copy of the workspace key for workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.plaintext - plaintext to encrypt - */ - static async encryptSymmetric({ - workspaceId, - plaintext - }: { - workspaceId: Types.ObjectId; - plaintext: string; - }) { - return await encryptSymmetricHelper({ - workspaceId, - plaintext - }); - } - - /** - * Return symmetrically decrypted [ciphertext] using the - * bot's copy of the workspace key for workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.ciphertext - ciphertext to decrypt - * @param {String} obj.iv - iv - * @param {String} obj.tag - tag - */ - static async decryptSymmetric({ - workspaceId, - ciphertext, - iv, - tag - }: { - workspaceId: Types.ObjectId; - ciphertext: string; - iv: string; - tag: string; - }) { - return await decryptSymmetricHelper({ - workspaceId, - ciphertext, - iv, - tag - }); - } + /** + * Get workspace key for workspace with id [workspaceId] shared to bot. + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for + * @returns + */ + static async getWorkspaceKeyWithBot({ + workspaceId, + }: { + workspaceId: Types.ObjectId; + }) { + return await getKey({ + workspaceId, + }); + } + + /** + * Return decrypted secrets for workspace with id [workspaceId] and + * environment [environmen] shared to bot. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace of secrets + * @param {String} obj.environment - environment for secrets + * @returns {Object} secretObj - object where keys are secret keys and values are secret values + */ + static async getSecrets({ + workspaceId, + environment, + secretPath, + }: { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; + }) { + return await getSecretsBotHelper({ + workspaceId, + environment, + secretPath, + }); + } + + /** + * Return symmetrically encrypted [plaintext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.plaintext - plaintext to encrypt + */ + static async encryptSymmetric({ + workspaceId, + plaintext, + }: { + workspaceId: Types.ObjectId; + plaintext: string; + }) { + return await encryptSymmetricHelper({ + workspaceId, + plaintext, + }); + } + + /** + * Return symmetrically decrypted [ciphertext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.iv - iv + * @param {String} obj.tag - tag + */ + static async decryptSymmetric({ + workspaceId, + ciphertext, + iv, + tag, + }: { + workspaceId: Types.ObjectId; + ciphertext: string; + iv: string; + tag: string; + }) { + return await decryptSymmetricHelper({ + workspaceId, + ciphertext, + iv, + tag, + }); + } } -export default BotService; \ No newline at end of file +export default BotService; diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 4bb163aae..af05cfab7 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -13,6 +13,7 @@ import { BackupPrivateKey, IntegrationAuth, ServiceTokenData, + Integration, } from "../../models"; import { generateKeyPair } from "../../utils/crypto"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; @@ -424,3 +425,19 @@ export const backfillServiceToken = async () => { ); console.log("Migration: Service token migration v1 complete"); }; + +export const backfillIntegration = async () => { + await Integration.updateMany( + { + secretPath: { + $exists: false, + }, + }, + { + $set: { + secretPath: "/", + }, + } + ); + console.log("Migration: Integration migration v1 complete"); +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index bec142e9e..d7f333b17 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -13,6 +13,7 @@ import { backfillEncryptionMetadata, backfillSecretFolders, backfillServiceToken, + backfillIntegration, } from "./backfillData"; import { reencryptBotPrivateKeys, @@ -77,6 +78,7 @@ export const setup = async () => { await backfillEncryptionMetadata(); await backfillSecretFolders(); await backfillServiceToken(); + await backfillIntegration(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY diff --git a/frontend/public/images/integrations/Terraform.png b/frontend/public/images/integrations/Terraform.png new file mode 100644 index 000000000..166edd32a Binary files /dev/null and b/frontend/public/images/integrations/Terraform.png differ diff --git a/frontend/public/json/frameworkIntegrations.json b/frontend/public/json/frameworkIntegrations.json index 32863bc07..1a6ff94f2 100644 --- a/frontend/public/json/frameworkIntegrations.json +++ b/frontend/public/json/frameworkIntegrations.json @@ -17,6 +17,12 @@ "image": "Kubernetes", "docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes" }, + { + "name": "Terraform", + "slug": "terraform", + "image": "Terraform", + "docsLink": "https://infisical.com/docs/integrations/frameworks/terraform" + }, { "name": "React", "slug": "react", diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx index 6a657c1a0..7bfd0d0c0 100644 --- a/frontend/src/components/integrations/Integration.tsx +++ b/frontend/src/components/integrations/Integration.tsx @@ -4,7 +4,11 @@ import { useRouter } from 'next/router'; import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; // TODO: This needs to be moved from public folder -import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants'; +import { + contextNetlifyMapping, + integrationSlugNameMapping, + reverseContextNetlifyMapping +} from 'public/data/frequentConstants'; import Button from '@app/components/basic/buttons/Button'; import ListBox from '@app/components/basic/Listbox'; @@ -27,6 +31,7 @@ interface Integration { targetEnvironment: string; workspace: string; integrationAuth: string; + secretPath: string; } interface IntegrationApp { @@ -55,7 +60,6 @@ const IntegrationTile = ({ environments = [], handleDeleteIntegration }: Props) => { - const [integrationEnvironment, setIntegrationEnvironment] = useState( environments.find(({ slug }) => slug === integration?.environment) || { name: '', @@ -74,16 +78,16 @@ const IntegrationTile = ({ }); setApps(tempApps); - + if (integration?.app) { setIntegrationApp(integration.app); } else if (integration?.path && integration?.region) { setIntegrationApp(`${integration.path} (${integration.region})`); } else if (tempApps.length > 0) { - setIntegrationApp(tempApps[0].name) - } else { - setIntegrationApp(''); - } + setIntegrationApp(tempApps[0].name); + } else { + setIntegrationApp(''); + } switch (integration.integration) { case 'vercel': @@ -212,12 +216,15 @@ const IntegrationTile = ({ return
; }; - if (!integrationApp && integration.integration !== "checkly") return
; - - const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp; + if (!integrationApp && integration.integration !== 'checkly') return
; + + const isSelected = + integration.integration === 'hashicorp-vault' + ? `${integration.app} - path: ${integration.path}` + : integrationApp; return ( -
+

ENVIRONMENT

@@ -235,33 +242,46 @@ const IntegrationTile = ({ isFull />
+
+

SECRET PATH

+
+ {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */} + {integration.secretPath} +
+
-

INTEGRATION

-
+

INTEGRATION

+
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */} {integrationSlugNameMapping[integration.integration]}
APP
- {integrationApp ?
- app.name) : null} - isSelected={isSelected} - onChange={(app) => { - setIntegrationApp(app); - }} - /> -
:
-
} + {integrationApp ? ( +
+ app.name) : null} + isSelected={isSelected} + onChange={(app) => { + setIntegrationApp(app); + }} + /> +
+ ) : ( +
+ - +
+ )}
{renderIntegrationSpecificParams(integration)}
-
+
{integration.isActive ? ( -
+
In Sync
diff --git a/frontend/src/components/integrations/IntegrationSection.tsx b/frontend/src/components/integrations/IntegrationSection.tsx index 9b8799b6a..cf1d7e9c4 100644 --- a/frontend/src/components/integrations/IntegrationSection.tsx +++ b/frontend/src/components/integrations/IntegrationSection.tsx @@ -23,6 +23,7 @@ interface Integration { targetEnvironment: string; workspace: string; integrationAuth: string; + secretPath: string; } const ProjectIntegrationSection = ({ diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts index e3e7c010a..331adff51 100644 --- a/frontend/src/pages/api/integrations/createIntegration.ts +++ b/frontend/src/pages/api/integrations/createIntegration.ts @@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient'; interface Props { integrationAuthId: string; isActive: boolean; + secretPath: string; app: string | null; appId: string | null; sourceEnvironment: string; @@ -20,19 +21,20 @@ interface Props { * @param {String} obj.accessToken - id of integration authorization for which to create the integration * @returns */ -const createIntegration = ({ - integrationAuthId, - isActive, - app, - appId, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region +const createIntegration = ({ + integrationAuthId, + isActive, + app, + appId, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + secretPath }: Props) => SecurityClient.fetchCall('/api/v1/integration', { method: 'POST', @@ -40,18 +42,19 @@ const createIntegration = ({ 'Content-Type': 'application/json' }, body: JSON.stringify({ - integrationAuthId, - isActive, - app, - appId, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region + integrationAuthId, + isActive, + app, + appId, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + secretPath }) }).then(async (res) => { if (res && res.status === 200) { @@ -61,4 +64,4 @@ const createIntegration = ({ return undefined; }); -export default createIntegration; \ No newline at end of file +export default createIntegration; diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx index 435c904f5..947cc159d 100644 --- a/frontend/src/pages/integrations/[id].tsx +++ b/frontend/src/pages/integrations/[id].tsx @@ -44,6 +44,7 @@ interface Integration { integration: string; targetEnvironment: string; workspace: string; + secretPath:string; integrationAuth: string; } @@ -436,7 +437,15 @@ export default function Integrations() { handleDeleteIntegrationAuth={handleDeleteIntegrationAuth} /> ) : ( -
+ <> +
+

{t('integrations.cloud-integrations')}

+

{t('integrations.click-to-start')}

+
+
+ {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem =>
)} +
+ )}
diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx index d06ac0061..4f84ff476 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx @@ -56,6 +56,7 @@ export default function AWSParameterStoreCreateIntegrationPage() { const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); + const [secretPath, setSecretPath] = useState('/'); const [selectedAWSRegion, setSelectedAWSRegion] = useState(''); const [path, setPath] = useState(''); const [pathErrorText, setPathErrorText] = useState(''); @@ -69,9 +70,9 @@ export default function AWSParameterStoreCreateIntegrationPage() { } }, [workspace]); - const isValidAWSParameterStorePath = (secretPath: string) => { + const isValidAWSParameterStorePath = (awsStorePath: string) => { const pattern = /^\/([\w-]+\/)*[\w-]+\/$/; - return pattern.test(secretPath) && secretPath.length <= 2048; + return pattern.test(awsStorePath) && awsStorePath.length <= 2048; }; const handleButtonClick = async () => { @@ -101,7 +102,8 @@ export default function AWSParameterStoreCreateIntegrationPage() { targetServiceId: null, owner: null, path, - region: selectedAWSRegion + region: selectedAWSRegion, + secretPath }); setIsLoading(false); @@ -133,6 +135,13 @@ export default function AWSParameterStoreCreateIntegrationPage() { ))} + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + - - Checkly Integration + + + Checkly Integration + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setTargetEnvironment(e.target.value)} + + setTargetEnvironment(e.target.value)} /> diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx index 53f9b3666..639f774d7 100644 --- a/frontend/src/pages/integrations/heroku/create.tsx +++ b/frontend/src/pages/integrations/heroku/create.tsx @@ -2,7 +2,15 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import queryString from 'query-string'; -import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from '../../../components/v2'; import { useGetIntegrationAuthApps, useGetIntegrationAuthById @@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() { const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [targetApp, setTargetApp] = useState(''); + const [secretPath, setSecretPath] = useState('/'); const [isLoading, setIsLoading] = useState(false); @@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() { targetServiceId: null, owner: null, path: null, - region: null + region: null, + secretPath }); setIsLoading(false); @@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() { ))} + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> +