diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index caecf852c6..9394bad383 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -422,10 +422,11 @@ export const registerRoutes = async ( projectMembershipDAL, smtpService, projectDAL, + projectBotDAL, secretVersionDAL, secretBlindIndexDAL, secretTagDAL, - secretVersionTagDAL, + secretVersionTagDAL }); const secretBlindIndexService = secretBlindIndexServiceFactory({ permissionService, diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index b5d3b28d5b..ed1914ccc0 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -32,7 +32,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { .object({ secretPrefix: z.string().optional(), secretSuffix: z.string().optional(), - syncBehavior: z.string().optional(), + initialSyncBehavior: z.string().optional(), secretGCPLabel: z .object({ labelName: z.string(), diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index aa23547475..8d5d0e5ae9 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -43,7 +43,6 @@ import { import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list"; import { getTeams } from "./integration-team"; import { exchangeCode, exchangeRefresh } from "./integration-token"; -import { access } from "node:fs"; type TIntegrationAuthServiceFactoryDep = { integrationAuthDAL: TIntegrationAuthDALFactory; @@ -603,7 +602,7 @@ export const integrationAuthServiceFactory = ({ } } ); - + return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({ app: { appId }, stage, diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 816d6f4093..e49cd38627 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -37,7 +37,7 @@ export enum IntegrationType { OAUTH2 = "oauth2" } -export enum IntegrationSyncBehavior { +export enum IntegrationInitialSyncBehavior { OVERWRITE_TARGET = "overwrite-target", PREFER_TARGET = "prefer-target", PREFER_SOURCE = "prefer-source" diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 56d9dfb9ad..50fc602b03 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -20,23 +20,13 @@ import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; import { z } from "zod"; -import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; +import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TSecretDALFactory } from "@app/services/secret/secret-dal"; -import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; -import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; -import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; -import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; -import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; -import { - createManySecretsRawHelper - // updateManySecretsRawHelper -} from "../secret/secret-fns"; -import { Integrations, IntegrationSyncBehavior, IntegrationUrls } from "./integration-list"; +import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list"; const getSecretKeyValuePair = (secrets: Record) => Object.keys(secrets).reduce>((prev, key) => { @@ -594,35 +584,25 @@ const syncSecretsAWSSecretManager = async ({ * Sync/push [secrets] to Heroku app named [integration.app] */ const syncSecretsHeroku = async ({ - projectDAL, + createManySecretsRawFn, + updateManySecretsRawFn, integrationDAL, - secretDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretTagDAL, - secretVersionTagDAL, - folderDAL, - botKey, // TODO: consider getting botKey inside this fn - projectId, - environment, - secretPath, integration, secrets, accessToken }: { - projectDAL: TProjectDALFactory; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integrationDAL: Pick; - secretDAL: TSecretDALFactory; - secretVersionDAL: TSecretVersionDALFactory; - secretBlindIndexDAL: TSecretBlindIndexDALFactory; - secretTagDAL: TSecretTagDALFactory; - secretVersionTagDAL: TSecretVersionTagDALFactory; - folderDAL: TSecretFolderDALFactory; - botKey: string; - projectId: string; - environment: string; - secretPath: string; - integration: TIntegrations; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; secrets: Record; accessToken: string; }) => { @@ -644,13 +624,13 @@ const syncSecretsHeroku = async ({ Object.keys(herokuSecrets).forEach((key) => { if (!integration.lastUsed) { // first time using integration - // -> apply initial sync behavior rule - switch (metadata.syncBehavior) { - case IntegrationSyncBehavior.OVERWRITE_TARGET: { + // -> apply initial sync behavior + switch (metadata.initialSyncBehavior) { + case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: { if (!(key in secrets)) secrets[key] = null; break; } - case IntegrationSyncBehavior.PREFER_TARGET: { + case IntegrationInitialSyncBehavior.PREFER_TARGET: { if (!(key in secrets)) { secretsToAdd[key] = herokuSecrets[key]; } else if (secrets[key]?.value !== herokuSecrets[key]) { @@ -661,7 +641,7 @@ const syncSecretsHeroku = async ({ }; break; } - case IntegrationSyncBehavior.PREFER_SOURCE: { + case IntegrationInitialSyncBehavior.PREFER_SOURCE: { if (!(key in secrets)) { secrets[key] = herokuSecrets[key]; secretsToAdd[key] = herokuSecrets[key]; @@ -677,18 +657,10 @@ const syncSecretsHeroku = async ({ }); if (Object.keys(secretsToAdd).length) { - await createManySecretsRawHelper({ - botKey, - projectDAL, - secretDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretTagDAL, - secretVersionTagDAL, - folderDAL, - projectId, - environment, - path: secretPath, + await createManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, secrets: Object.keys(secretsToAdd).map((key) => ({ secretName: key, secretValue: secretsToAdd[key], @@ -698,27 +670,19 @@ const syncSecretsHeroku = async ({ }); } - // if (Object.keys(secretsToUpdate).length) { - // await updateManySecretsRawHelper({ - // projectId, - // environment, - // path: secretPath, - // secrets: Object.keys(secretsToUpdate).map((key) => ({ - // secretName: key, - // secretValue: secretsToUpdate[key], - // type: SecretType.Shared, - // secretComment: "" - // })), - // botKey, // TODO: consider getting botKey inside this fn - // projectDAL, - // secretDAL, - // secretVersionDAL, - // secretBlindIndexDAL, - // secretTagDAL, - // secretVersionTagDAL, - // folderDAL - // }); - // } + if (Object.keys(secretsToUpdate).length) { + await updateManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToUpdate).map((key) => ({ + secretName: key, + secretValue: secretsToUpdate[key], + type: SecretType.Shared, + secretComment: "" + })) + }); + } await request.patch( `${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`, @@ -3053,18 +3017,9 @@ const syncSecretsHasuraCloud = async ({ * */ export const syncIntegrationSecrets = async ({ - projectDAL, + createManySecretsRawFn, + updateManySecretsRawFn, integrationDAL, - secretDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretTagDAL, - secretVersionTagDAL, - folderDAL, - botKey, - projectId, - environment, - secretPath, integration, integrationAuth, secrets, @@ -3072,19 +3027,18 @@ export const syncIntegrationSecrets = async ({ accessToken, appendices }: { - projectDAL: TProjectDALFactory; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integrationDAL: Pick; - secretDAL: TSecretDALFactory; - secretVersionDAL: TSecretVersionDALFactory; - secretBlindIndexDAL: TSecretBlindIndexDALFactory; - secretTagDAL: TSecretTagDALFactory; - secretVersionTagDAL: TSecretVersionTagDALFactory; - folderDAL: TSecretFolderDALFactory; - botKey: string; - projectId: string; - environment: string; - secretPath: string; - integration: TIntegrations; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; integrationAuth: TIntegrationAuths; secrets: Record; accessId: string | null; @@ -3124,18 +3078,9 @@ export const syncIntegrationSecrets = async ({ break; case Integrations.HEROKU: await syncSecretsHeroku({ - projectDAL, + createManySecretsRawFn, + updateManySecretsRawFn, integrationDAL, - secretDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretTagDAL, - secretVersionTagDAL, - folderDAL, - botKey, - projectId, - environment, - secretPath, integration, secrets, accessToken diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index e69de29bb2..3f22b87043 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -0,0 +1,36 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; +import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { BadRequestError } from "@app/lib/errors"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; + +import { TGetPrivateKeyDTO } from "./project-bot-types"; + +export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => + infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + +export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => { + const getBotKeyFn = async (projectId: string) => { + const bot = await projectBotDAL.findOne({ projectId }); + + if (!bot) throw new BadRequestError({ message: "failed to find bot key" }); + if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" }); + if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey) + throw new BadRequestError({ message: "Encryption key missing" }); + + const botPrivateKey = getBotPrivateKey({ bot }); + + return decryptAsymmetric({ + ciphertext: bot.encryptedProjectKey, + privateKey: botPrivateKey, + nonce: bot.encryptedProjectKeyNonce, + publicKey: bot.sender.publicKey + }); + }; + + return getBotKeyFn; +}; diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 456a1e7a02..6e281e69d0 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -1,15 +1,16 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectVersion } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto"; -import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { generateAsymmetricKeyPair } from "@app/lib/crypto"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotDALFactory } from "./project-bot-dal"; -import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types"; +import { getBotKeyFnFactory, getBotPrivateKey } from "./project-bot-fns"; +import { TFindBotByProjectIdDTO, TSetActiveStateDTO } from "./project-bot-types"; type TProjectBotServiceFactoryDep = { permissionService: Pick; @@ -24,29 +25,10 @@ export const projectBotServiceFactory = ({ projectDAL, permissionService }: TProjectBotServiceFactoryDep) => { - const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => - infisicalSymmetricDecrypt({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); const getBotKey = async (projectId: string) => { - const bot = await projectBotDAL.findOne({ projectId }); - if (!bot) throw new BadRequestError({ message: "failed to find bot key" }); - if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" }); - if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey) - throw new BadRequestError({ message: "Encryption key missing" }); - - const botPrivateKey = getBotPrivateKey({ bot }); - - return decryptAsymmetric({ - ciphertext: bot.encryptedProjectKey, - privateKey: botPrivateKey, - nonce: bot.encryptedProjectKeyNonce, - publicKey: bot.sender.publicKey - }); + return getBotKeyFn(projectId); }; const findBotByProjectId = async ({ diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index b10b39f12e..212bb01f07 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -18,10 +18,12 @@ import { import { BadRequestError } from "@app/lib/errors"; import { groupBy, unique } from "@app/lib/fn"; +import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretDALFactory } from "./secret-dal"; import { - TCreateManySecretsRawHelper, + TCreateManySecretsRawFn, + TCreateManySecretsRawFnFactory, TFnSecretBlindIndexCheck, TFnSecretBulkInsert, TFnSecretBulkUpdate, @@ -405,112 +407,114 @@ export const fnSecretBulkUpdate = async ({ return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); }; -export const createManySecretsRawHelper = async ({ - projectId, - environment, - path: secretPath, - secrets, - userId, - botKey, // TODO: consider getting botKey inside this fn +export const createManySecretsRawFnFactory = ({ projectDAL, + projectBotDAL, secretDAL, secretVersionDAL, secretBlindIndexDAL, secretTagDAL, secretVersionTagDAL, folderDAL -}: TCreateManySecretsRawHelper) => { - await projectDAL.checkProjectUpgradeStatus(projectId); +}: TCreateManySecretsRawFnFactory) => { + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const createManySecretsRawFn = async ({ + projectId, + environment, + path: secretPath, + secrets, + userId + }: TCreateManySecretsRawFn) => { + const botKey = await getBotKeyFn(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); - const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); - const folderId = folder.id; + await projectDAL.checkProjectUpgradeStatus(projectId); - const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); - if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" }); + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + const folderId = folder.id; - // insert operation - const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ - inputSecrets: secrets, - folderId, - isNew: true, - blindIndexCfg, - secretDAL - }); + const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); + if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" }); - const inputSecrets = await Promise.all( - secrets.map(async (secret) => { - const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); - const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); - const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey); - - if (secret.type === SecretType.Personal) { - if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" }); - const sharedExist = await secretDAL.findOne({ - secretBlindIndex: keyName2BlindIndex[secret.secretName], - folderId, - type: SecretType.Shared - }); - - if (!sharedExist) - throw new BadRequestError({ - message: "Failed to create personal secret override for no corresponding shared secret" - }); - } - - const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : []; - if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); - - return { - type: secret.type, - userId: secret.type === SecretType.Personal ? userId : null, - secretName: secret.secretName, - secretKeyCiphertext: secretKeyEncrypted.ciphertext, - secretKeyIV: secretKeyEncrypted.iv, - secretKeyTag: secretKeyEncrypted.tag, - secretValueCiphertext: secretValueEncrypted.ciphertext, - secretValueIV: secretValueEncrypted.iv, - secretValueTag: secretValueEncrypted.tag, - secretCommentCiphertext: secretCommentEncrypted.ciphertext, - secretCommentIV: secretCommentEncrypted.iv, - secretCommentTag: secretCommentEncrypted.tag, - skipMultilineEncoding: secret.skipMultilineEncoding, - tags: secret.tags - }; - }) - ); - - const newSecrets = await secretDAL.transaction(async (tx) => - fnSecretBulkInsert({ - inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({ - ...el, - version: 0, - secretBlindIndex: keyName2BlindIndex[secretName], - algorithm: SecretEncryptionAlgo.AES_256_GCM, - keyEncoding: SecretKeyEncoding.UTF8 - })), + // insert operation + const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ + inputSecrets: secrets, folderId, - secretDAL, - secretVersionDAL, - secretTagDAL, - secretVersionTagDAL, - tx - }) - ); + isNew: true, + blindIndexCfg, + secretDAL + }); - return newSecrets; + const inputSecrets = await Promise.all( + secrets.map(async (secret) => { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey); + + if (secret.type === SecretType.Personal) { + if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" }); + const sharedExist = await secretDAL.findOne({ + secretBlindIndex: keyName2BlindIndex[secret.secretName], + folderId, + type: SecretType.Shared + }); + + if (!sharedExist) + throw new BadRequestError({ + message: "Failed to create personal secret override for no corresponding shared secret" + }); + } + + const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : []; + if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); + + return { + type: secret.type, + userId: secret.type === SecretType.Personal ? userId : null, + secretName: secret.secretName, + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag, + skipMultilineEncoding: secret.skipMultilineEncoding, + tags: secret.tags + }; + }) + ); + + const newSecrets = await secretDAL.transaction(async (tx) => + fnSecretBulkInsert({ + inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({ + ...el, + version: 0, + secretBlindIndex: keyName2BlindIndex[secretName], + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8 + })), + folderId, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + tx + }) + ); + + return newSecrets; + }; + + return createManySecretsRawFn; }; -// TOOD: potentially convert raw stuff - -// updateManySecretsRawFnFactory - -// updateManySecretsRawHelper - -export const updateManySecretsRawFnFactory = async ({ - // TODO: refactor - botKey, +export const updateManySecretsRawFnFactory = ({ projectDAL, + projectBotDAL, secretDAL, secretVersionDAL, secretBlindIndexDAL, @@ -518,14 +522,16 @@ export const updateManySecretsRawFnFactory = async ({ secretVersionTagDAL, folderDAL }: TUpdateManySecretsRawFnFactory) => { + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); const updateManySecretsRawFn = async ({ projectId, environment, path: secretPath, - secrets, // accept instead ciphertext secrets + secrets, // consider accepting instead ciphertext secrets userId - }: TUpdateManySecretsRawFn) => { - // TODO: fetch botKey from here + }: TUpdateManySecretsRawFn): Promise> => { + const botKey = await getBotKeyFn(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); await projectDAL.checkProjectUpgradeStatus(projectId); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index cc781c7dbd..eddf780e22 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -6,6 +6,8 @@ import { BadRequestError } from "@app/lib/errors"; import { isSamePath } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; @@ -29,8 +31,6 @@ import { TSecretDALFactory } from "./secret-dal"; import { interpolateSecrets } from "./secret-fns"; import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types"; -// import { updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns"; - export type TSecretQueueFactory = ReturnType; type TSecretQueueFactoryDep = { @@ -44,6 +44,7 @@ type TSecretQueueFactoryDep = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: TProjectDALFactory; + projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: Pick; smtpService: TSmtpService; orgDAL: Pick; @@ -72,12 +73,35 @@ export const secretQueueFactory = ({ orgDAL, smtpService, projectDAL, + projectBotDAL, projectMembershipDAL, secretVersionDAL, secretBlindIndexDAL, secretTagDAL, secretVersionTagDAL }: TSecretQueueFactoryDep) => { + const createManySecretsRawFn = createManySecretsRawFnFactory({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL + }); + + const updateManySecretsRawFn = updateManySecretsRawFnFactory({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL + }); + const syncIntegrations = async (dto: TGetSecrets) => { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { attempts: 5, @@ -320,30 +344,10 @@ export const secretQueueFactory = ({ }); } - // const updateManySecretsRawFn = updateManySecretsRawFnFactory({ - // botKey, // can move this out - // projectDAL, - // secretDAL, - // secretVersionDAL, - // secretBlindIndexDAL, - // secretTagDAL, - // secretVersionTagDAL, - // folderDAL - // }); - await syncIntegrationSecrets({ - projectDAL, + createManySecretsRawFn, + updateManySecretsRawFn, integrationDAL, - secretDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretTagDAL, - secretVersionTagDAL, - folderDAL, - botKey, - projectId, // service - environment, - secretPath, integration, integrationAuth, secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index b8faebbfa9..7ad4d65d72 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -3,6 +3,7 @@ import { Knex } from "knex"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; @@ -248,7 +249,18 @@ export type TRemoveSecretReminderDTO = { // --- -export type TCreateManySecretsRawHelper = { +export type TCreateManySecretsRawFnFactory = { + projectDAL: TProjectDALFactory; + projectBotDAL: TProjectBotDALFactory; + secretDAL: TSecretDALFactory; + secretVersionDAL: TSecretVersionDALFactory; + secretBlindIndexDAL: TSecretBlindIndexDALFactory; + secretTagDAL: TSecretTagDALFactory; + secretVersionTagDAL: TSecretVersionTagDALFactory; + folderDAL: TSecretFolderDALFactory; +}; + +export type TCreateManySecretsRawFn = { projectId: string; environment: string; path: string; @@ -264,19 +276,11 @@ export type TCreateManySecretsRawHelper = { }; }[]; userId?: string; // only relevant for personal secret(s) - botKey: string; - projectDAL: TProjectDALFactory; - secretDAL: TSecretDALFactory; - secretVersionDAL: TSecretVersionDALFactory; - secretBlindIndexDAL: TSecretBlindIndexDALFactory; - secretTagDAL: TSecretTagDALFactory; - secretVersionTagDAL: TSecretVersionTagDALFactory; - folderDAL: TSecretFolderDALFactory; }; export type TUpdateManySecretsRawFnFactory = { - botKey: string; projectDAL: TProjectDALFactory; + projectBotDAL: TProjectBotDALFactory; secretDAL: TSecretDALFactory; secretVersionDAL: TSecretVersionDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; diff --git a/docs/images/integrations/heroku/integrations-heroku-create.png b/docs/images/integrations/heroku/integrations-heroku-create.png index a2a8d4e767..452dc51593 100644 Binary files a/docs/images/integrations/heroku/integrations-heroku-create.png and b/docs/images/integrations/heroku/integrations-heroku-create.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku.png b/docs/images/integrations/heroku/integrations-heroku.png index 31c8284cd7..ead3324474 100644 Binary files a/docs/images/integrations/heroku/integrations-heroku.png and b/docs/images/integrations/heroku/integrations-heroku.png differ diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index 2d8fdc4456..903ab82701 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -30,6 +30,17 @@ description: "How to sync secrets from Infisical to Heroku" Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku. ![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png) + + Here's some guidance on each field: + + - Project Environment: The environment in the current Infisical project from which you want to sync secrets from. + - Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`). + - Heroku App: The application in Heroku that you want to sync secrets to. + - Initial Sync Behavior (default is **Import - Prefer values from Infisical**): The behavior of the first sync operation triggered after creating the integration. + - **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku. + - **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku. + - **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku. + ![integrations heroku](../../images/integrations/heroku/integrations-heroku.png) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 41005ba7f0..9255de2c6d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "npm-proj-1709146141702-0.772936286416932EMIzNi", + "name": "frontend", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 5eb8d0d57b..e50b914f16 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -61,7 +61,7 @@ export const useCreateIntegration = () => { metadata?: { secretPrefix?: string; secretSuffix?: string; - syncBehavior?: string; + initialSyncBehavior?: string; } }) => { const { data: { integration } } = await apiRequest.post("/api/v1/integration", { diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index 0470a2c581..73db6c07d9 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -45,10 +45,4 @@ export enum IntegrationSyncBehavior { OVERWRITE_TARGET = "overwrite-target", PREFER_TARGET = "prefer-target", PREFER_SOURCE = "prefer-source" -} - -export const syncBehaviors = [ - { label: "Overwrite target", value: IntegrationSyncBehavior.OVERWRITE_TARGET }, - { label: "Prefer target", value: IntegrationSyncBehavior.PREFER_TARGET }, - { label: "Prefer source", value: IntegrationSyncBehavior.PREFER_SOURCE } -]; \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx index bb0a144ae6..f0b66d52ee 100644 --- a/frontend/src/pages/integrations/heroku/create.tsx +++ b/frontend/src/pages/integrations/heroku/create.tsx @@ -4,15 +4,22 @@ import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { faArrowUpRightFromSquare, faBookOpen, faBugs, faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowUpRightFromSquare, + faBookOpen, + faBugs, + // faCircleInfo +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import queryString from "query-string"; -import { RadioGroup } from "@app/components/v2/RadioGroup"; -import { useCreateIntegration } from "@app/hooks/api"; -import { useGetIntegrationAuthHerokuPipelines } from "@app/hooks/api/integrationAuth/queries"; -import { App, Pipeline } from "@app/hooks/api/integrationAuth/types"; -import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; +import queryString from "query-string"; +// import { useGetIntegrationAuthHerokuPipelines } from "@app/hooks/api/integrationAuth/queries"; +// import { App, Pipeline } from "@app/hooks/api/integrationAuth/types"; +import * as yup from "yup"; + +// import { RadioGroup } from "@app/components/v2/RadioGroup"; +import { useCreateIntegration } from "@app/hooks/api"; +import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; import { Button, @@ -27,16 +34,24 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; -import { useCreateWsEnvironment, useGetWorkspaceById } from "../../../hooks/api/workspace"; -import { IntegrationSyncBehavior, syncBehaviors } from "@app/hooks/api/integrations/types"; +import { + // useCreateWsEnvironment, + useGetWorkspaceById +} from "../../../hooks/api/workspace"; + +const initialSyncBehaviors = [ + { label: "No Import - Overwrite all values in Heroku", value: IntegrationSyncBehavior.OVERWRITE_TARGET }, + { label: "Import - Prefer values from Heroku", value: IntegrationSyncBehavior.PREFER_TARGET }, + { label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE } +]; const schema = yup.object({ selectedSourceEnvironment: yup.string().required("Source environment is required"), secretPath: yup.string().required("Secret path is required"), targetApp: yup.string().required("Heroku app is required"), - syncBehavior: yup + initialSyncBehavior: yup .string() - .oneOf(syncBehaviors.map((b) => b.value), "Invalid sync behavior") + .oneOf(initialSyncBehaviors.map((b) => b.value), "Invalid initial sync behavior") .required("Initial sync behavior is required") }); @@ -49,7 +64,7 @@ export default function HerokuCreateIntegrationPage() { resolver: yupResolver(schema), defaultValues: { secretPath: "/", - syncBehavior: IntegrationSyncBehavior.PREFER_SOURCE + initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE } }); @@ -57,7 +72,7 @@ export default function HerokuCreateIntegrationPage() { const { mutateAsync } = useCreateIntegration(); - const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment(); + // const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -67,14 +82,14 @@ export default function HerokuCreateIntegrationPage() { integrationAuthId: (integrationAuthId as string) ?? "" }); - const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({ - integrationAuthId: (integrationAuthId as string) ?? "" - }); + // const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({ + // integrationAuthId: (integrationAuthId as string) ?? "" + // }); - const [uniquePipelines, setUniquePipelines] = useState(); - const [selectedPipeline, setSelectedPipeline] = useState(""); - const [selectedPipelineApps, setSelectedPipelineApps] = useState(); - const [integrationType, setIntegrationType] = useState("App"); + // const [uniquePipelines, setUniquePipelines] = useState(); + // const [selectedPipeline, setSelectedPipeline] = useState(""); + // const [selectedPipelineApps, setSelectedPipelineApps] = useState(); + // const [integrationType, setIntegrationType] = useState("App"); const [isLoading, setIsLoading] = useState(false); @@ -84,37 +99,37 @@ export default function HerokuCreateIntegrationPage() { } }, [workspace]); - useEffect(() => { - if (integrationAuthPipelineCouplings) { - const uniquePipelinesConst = Array.from( - new Set( - integrationAuthPipelineCouplings - .map(({ pipeline: { pipelineId, name } }) => ({ - name, - pipelineId - })) - .map((obj) => JSON.stringify(obj)) - )).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[] + // useEffect(() => { + // if (integrationAuthPipelineCouplings) { + // const uniquePipelinesConst = Array.from( + // new Set( + // integrationAuthPipelineCouplings + // .map(({ pipeline: { pipelineId, name } }) => ({ + // name, + // pipelineId + // })) + // .map((obj) => JSON.stringify(obj)) + // )).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[] - [... (new Set())] - setUniquePipelines(uniquePipelinesConst); - if (uniquePipelinesConst) { - if (uniquePipelinesConst!.length > 0) { - setSelectedPipeline(uniquePipelinesConst![0].name); - } else { - setSelectedPipeline("none"); - } - } - } - }, [integrationAuthPipelineCouplings]); + // [... (new Set())] + // setUniquePipelines(uniquePipelinesConst); + // if (uniquePipelinesConst) { + // if (uniquePipelinesConst!.length > 0) { + // setSelectedPipeline(uniquePipelinesConst![0].name); + // } else { + // setSelectedPipeline("none"); + // } + // } + // } + // }, [integrationAuthPipelineCouplings]); - useEffect(() => { - if (integrationAuthPipelineCouplings) { - setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings - .filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline) - .map(coupling => coupling.app.appId).includes(String(app.appId)))) - } - }, [selectedPipeline]); + // useEffect(() => { + // if (integrationAuthPipelineCouplings) { + // setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings + // .filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline) + // .map(coupling => coupling.app.appId).includes(String(app.appId)))) + // } + // }, [selectedPipeline]); useEffect(() => { if (integrationAuthApps) { @@ -167,10 +182,9 @@ export default function HerokuCreateIntegrationPage() { // }; const onFormSubmit = async ({ - selectedSourceEnvironment: sce, secretPath, targetApp, - syncBehavior, + initialSyncBehavior, }: FormData) => { try { if (!integrationAuth?.id) return; @@ -184,7 +198,7 @@ export default function HerokuCreateIntegrationPage() { sourceEnvironment: selectedSourceEnvironment, secretPath, metadata: { - syncBehavior + initialSyncBehavior } }); @@ -314,7 +328,7 @@ export default function HerokuCreateIntegrationPage() { /> (