diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index 93e5a2538..ff150fe67 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -5,7 +5,6 @@ import { SecretApprovalRequestsSecretsSchema, TableName, TSecretApprovalRequestsSecrets, - TSecretApprovalRequestsSecretsUpdate, TSecretTags } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; @@ -17,22 +16,32 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); - const bulkUpdateNoVersionIncrement = async ( - data: Array<{ filter: Partial; data: TSecretApprovalRequestsSecretsUpdate }>, - tx?: Knex - ) => { + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { try { - const secs = await Promise.all( - data.map(async ({ filter, data: updateData }) => { - const [doc] = await (tx || db)(TableName.SecretApprovalRequestSecret) - .where(filter) - .update(updateData) - .returning("*"); - if (!doc) throw new BadRequestError({ message: "Failed to update document" }); - return doc; - }) + const existingApprovalSecrets = await secretApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } ); - return secs; + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + if (!updatedApprovalSecrets || updatedApprovalSecrets.length === 0) { + throw new BadRequestError({ message: "Failed to bulk update secret approvals" }); + } + + return updatedApprovalSecrets; } catch (error) { throw new DatabaseError({ error, name: "bulk update secret" }); } diff --git a/backend/src/lib/secret/index.ts b/backend/src/lib/secret/index.ts index 4292c7945..036b49970 100644 --- a/backend/src/lib/secret/index.ts +++ b/backend/src/lib/secret/index.ts @@ -1,46 +1,45 @@ import crypto from "crypto"; import { z } from "zod"; -import { TProjectKeys } from "@app/db/schemas"; -import { logger } from "@app/lib/logger"; +import { + SecretApprovalRequestsSecretsSchema, + SecretsSchema, + SecretVersionsSchema, + TProjectKeys, + TSecretApprovalRequestsSecrets, + TSecrets, + TSecretVersions +} from "@app/db/schemas"; import { decryptAsymmetric } from "../crypto"; -export enum SecretDocType { - Secret = "secret", - SecretVersion = "secretVersion", - ApprovalSecret = "approvalSecret" -} - -export interface TPartialSecret { - id: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - - secretCommentCiphertext?: string | null; - secretCommentIV?: string | null; - secretCommentTag?: string | null; - - docType: SecretDocType; - keyEncoding: string; -} - -const PartialDecryptedSecretSchema = z.object({ +const DecryptedValuesSchema = z.object({ id: z.string(), secretKey: z.string(), secretValue: z.string(), - secretComment: z.string().optional(), - - docType: z.nativeEnum(SecretDocType) + secretComment: z.string().optional() }); -export type TPartialDecryptedSecret = z.infer; -const decryptSecret = ({ +const DecryptedSecretSchema = z.object({ + decrypted: DecryptedValuesSchema, + original: SecretsSchema +}); + +const DecryptedSecretVersionsSchema = z.object({ + decrypted: DecryptedValuesSchema, + original: SecretVersionsSchema +}); + +export const DecryptedSecretApprovalsSchema = z.object({ + decrypted: DecryptedValuesSchema, + original: SecretApprovalRequestsSecretsSchema +}); + +export type DecryptedSecret = z.infer; +export type DecryptedSecretVersions = z.infer; +export type DecryptedSecretApprovals = z.infer; + +const decryptCipher = ({ ciphertext, iv, tag, @@ -60,8 +59,62 @@ const decryptSecret = ({ return cleartext; }; +const getDecryptedValues = ({ + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + key +}: { + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext?: string | null; + secretCommentIV?: string | null; + secretCommentTag?: string | null; + key: string | Buffer; +}) => { + const secretKey = decryptCipher({ + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag, + key + }); + + const secretValue = decryptCipher({ + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag, + key + }); + + const secretComment = + secretCommentCiphertext && secretCommentIV && secretCommentTag + ? decryptCipher({ + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag, + key + }) + : ""; + + return { + secretKey, + secretValue, + secretComment + }; +}; export const decryptSecrets = ( - encryptedSecrets: TPartialSecret[], + encryptedSecrets: TSecrets[], privateKey: string, latestKey: TProjectKeys & { sender: { @@ -76,47 +129,123 @@ export const decryptSecrets = ( privateKey }); - const decryptedSecrets: TPartialDecryptedSecret[] = []; + const decryptedSecrets: DecryptedSecret[] = []; encryptedSecrets.forEach((encSecret) => { - try { - const secretKey = decryptSecret({ - ciphertext: encSecret.secretKeyCiphertext, - iv: encSecret.secretKeyIV, - tag: encSecret.secretKeyTag, - key - }); + const decrypted = getDecryptedValues({ + secretKeyCiphertext: encSecret.secretKeyCiphertext, + secretKeyIV: encSecret.secretKeyIV, + secretKeyTag: encSecret.secretKeyTag, + secretValueCiphertext: encSecret.secretValueCiphertext, + secretValueIV: encSecret.secretValueIV, + secretValueTag: encSecret.secretValueTag, + secretCommentCiphertext: encSecret.secretCommentCiphertext, + secretCommentIV: encSecret.secretCommentIV, + secretCommentTag: encSecret.secretCommentTag, + key + }); - const secretValue = decryptSecret({ - ciphertext: encSecret.secretValueCiphertext, - iv: encSecret.secretValueIV, - tag: encSecret.secretValueTag, - key - }); + const decryptedSecret: DecryptedSecret = { + decrypted: { + ...decrypted, + id: encSecret.id + }, + original: encSecret + }; - const secretComment = - encSecret.secretCommentCiphertext && encSecret.secretCommentIV && encSecret.secretCommentTag - ? decryptSecret({ - ciphertext: encSecret.secretCommentCiphertext, - iv: encSecret.secretCommentIV, - tag: encSecret.secretCommentTag, - key - }) - : ""; - - const decryptedSecret: TPartialDecryptedSecret = { - id: encSecret.id, - secretKey, - secretValue, - secretComment, - docType: encSecret.docType - }; - - decryptedSecrets.push(PartialDecryptedSecretSchema.parse(decryptedSecret)); - } catch (err) { - // This is ok, because we check that the decrypted secrets array length is the same as the encrypted secrets input array length. - logger.error(`[${encSecret.id}] - failed to decrypt`, err); - } + decryptedSecrets.push(DecryptedSecretSchema.parse(decryptedSecret)); + }); + + return decryptedSecrets; +}; + +export const decryptSecretVersions = ( + encryptedSecretVersions: TSecretVersions[], + privateKey: string, + latestKey: TProjectKeys & { + sender: { + publicKey: string; + }; + } +) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const decryptedSecrets: DecryptedSecretVersions[] = []; + + encryptedSecretVersions.forEach((encSecret) => { + const decrypted = getDecryptedValues({ + secretKeyCiphertext: encSecret.secretKeyCiphertext, + secretKeyIV: encSecret.secretKeyIV, + secretKeyTag: encSecret.secretKeyTag, + secretValueCiphertext: encSecret.secretValueCiphertext, + secretValueIV: encSecret.secretValueIV, + secretValueTag: encSecret.secretValueTag, + secretCommentCiphertext: encSecret.secretCommentCiphertext, + secretCommentIV: encSecret.secretCommentIV, + secretCommentTag: encSecret.secretCommentTag, + key + }); + + const decryptedSecret: DecryptedSecretVersions = { + decrypted: { + ...decrypted, + id: encSecret.id + }, + original: encSecret + }; + + decryptedSecrets.push(DecryptedSecretVersionsSchema.parse(decryptedSecret)); + }); + + return decryptedSecrets; +}; + +export const decryptSecretApprovals = ( + encryptedSecretApprovals: TSecretApprovalRequestsSecrets[], + privateKey: string, + latestKey: TProjectKeys & { + sender: { + publicKey: string; + }; + } +) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const decryptedSecrets: DecryptedSecretApprovals[] = []; + + encryptedSecretApprovals.forEach((encSecret) => { + const decrypted = getDecryptedValues({ + secretKeyCiphertext: encSecret.secretKeyCiphertext, + secretKeyIV: encSecret.secretKeyIV, + secretKeyTag: encSecret.secretKeyTag, + secretValueCiphertext: encSecret.secretValueCiphertext, + secretValueIV: encSecret.secretValueIV, + secretValueTag: encSecret.secretValueTag, + secretCommentCiphertext: encSecret.secretCommentCiphertext, + secretCommentIV: encSecret.secretCommentIV, + secretCommentTag: encSecret.secretCommentTag, + key + }); + + const decryptedSecret: DecryptedSecretApprovals = { + decrypted: { + ...decrypted, + id: encSecret.id + }, + original: encSecret + }; + + decryptedSecrets.push(DecryptedSecretApprovalsSchema.parse(decryptedSecret)); }); return decryptedSecrets; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 7a6e4c795..f499d2309 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -3,7 +3,6 @@ import slugify from "@sindresorhus/slugify"; import crypto from "crypto"; import jwt from "jsonwebtoken"; import { Knex } from "knex"; -import { nanoid } from "nanoid"; import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { TProjects } from "@app/db/schemas/projects"; @@ -133,7 +132,7 @@ export const orgServiceFactory = ({ }; const addGhostUser = async (orgId: string, tx?: Knex) => { - const email = `ghost-${nanoid(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. + const email = `ghost-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. const password = crypto.randomBytes(128).toString("hex"); const user = await userDAL.create( diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index 3f521a1e6..33aaaa361 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -3,8 +3,13 @@ import { ProjectMembershipRole, ProjectUpgradeStatus, ProjectVersion, + SecretApprovalRequestsSecretsSchema, SecretKeyEncoding, - TSecrets + SecretsSchema, + SecretVersionsSchema, + TSecretApprovalRequestsSecrets, + TSecrets, + TSecretVersions } from "@app/db/schemas"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -16,7 +21,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { logger } from "@app/lib/logger"; -import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret"; +import { decryptSecretApprovals, decryptSecrets, decryptSecretVersions } from "@app/lib/secret"; import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue"; import { TOrgDALFactory } from "../org/org-dal"; @@ -115,7 +120,9 @@ export const projectQueueFactory = ({ }); // Get all the secrets within the project (as encrypted) - const secrets: TPartialSecret[] = []; + const secrets: TSecrets[] = []; + const secretVersions: TSecretVersions[] = []; + const approvalSecrets: TSecretApprovalRequestsSecrets[] = []; for (const folder of projectFolders) { const folderSecrets = await secretDAL.find({ folderId: folder.id }); @@ -132,22 +139,30 @@ export const projectQueueFactory = ({ status: RequestState.Open, folderId: folder.id }); - const approvalSecrets = await secretApprovalSecretDAL.find({ + const secretApprovals = await secretApprovalSecretDAL.find({ $in: { requestId: approvalRequests.map((el) => el.id) } }); - secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret }))); - secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion }))); - secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret }))); + secrets.push(...folderSecrets); + secretVersions.push(...folderSecretVersions); + approvalSecrets.push(...secretApprovals); } const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey); + const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey); + const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey); if (secrets.length !== decryptedSecrets.length) { throw new Error("Failed to decrypt some secret versions"); } + if (secretVersions.length !== decryptedSecretVersions.length) { + throw new Error("Failed to decrypt some secret versions"); + } + if (approvalSecrets.length !== decryptedApprovalSecrets.length) { + throw new Error("Failed to decrypt some secret approvals"); + } // Get the existing bot and the existing project keys for the members of the project const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null); @@ -286,107 +301,137 @@ export const projectQueueFactory = ({ publicKey: ghostUser.keys.publicKey }); - type TPartialSecret = Pick< - TSecrets, - | "id" - | "secretKeyCiphertext" - | "secretKeyIV" - | "secretKeyTag" - | "secretValueCiphertext" - | "secretValueIV" - | "secretValueTag" - | "secretCommentCiphertext" - | "secretCommentIV" - | "secretCommentTag" - >; - - const updatedSecrets: TPartialSecret[] = []; - const updatedSecretVersions: TPartialSecret[] = []; - const updatedSecretApprovals: TPartialSecret[] = []; + const updatedSecrets: TSecrets[] = []; + const updatedSecretVersions: TSecretVersions[] = []; + const updatedSecretApprovals: TSecretApprovalRequestsSecrets[] = []; for (const rawSecret of decryptedSecrets) { - const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey); - const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey); - const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey); + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecret.decrypted.secretComment || "", + botKey + ); + + const payload: TSecrets = { + ...rawSecret.original, - const payload = { - id: rawSecret.id, 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 } as const; - if (rawSecret.docType === SecretDocType.Secret) { - updatedSecrets.push(payload); - } else if (rawSecret.docType === SecretDocType.SecretVersion) { - updatedSecretVersions.push(payload); - } else if (rawSecret.docType === SecretDocType.ApprovalSecret) { - updatedSecretApprovals.push(payload); - } else { - throw new Error("Unknown secret type"); + if (!SecretsSchema.safeParse(payload).success) { + throw new Error(`Invalid secret payload: ${JSON.stringify(payload)}`); } + + updatedSecrets.push(payload); } - const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement( - [ - ...updatedSecrets.map((secret) => ({ - filter: { id: secret.id }, - data: { - ...secret, - id: undefined - } - })) - ], - tx - ); + for (const rawSecretVersion of decryptedSecretVersions) { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretVersion.decrypted.secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretVersion.decrypted.secretValue || "", + botKey + ); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretVersion.decrypted.secretComment || "", + botKey + ); - const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement( - [ - ...updatedSecretVersions.map((version) => ({ - filter: { id: version.id }, - data: { - ...version, - id: undefined - } - })) - ], - tx - ); + const payload: TSecretVersions = { + ...rawSecretVersion.original, - const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement( - [ - ...updatedSecretApprovals.map((approval) => ({ - filter: { - id: approval.id - }, - data: { - ...approval, - id: undefined - } - })) - ], - tx - ); + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, - if (secretUpdates.length !== updatedSecrets.length) { + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag + } as const; + + if (!SecretVersionsSchema.safeParse(payload).success) { + throw new Error(`Invalid secret version payload: ${JSON.stringify(payload)}`); + } + + updatedSecretVersions.push(payload); + } + + for (const rawSecretApproval of decryptedApprovalSecrets) { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretApproval.decrypted.secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretApproval.decrypted.secretValue || "", + botKey + ); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretApproval.decrypted.secretComment || "", + botKey + ); + + const payload: TSecretApprovalRequestsSecrets = { + ...rawSecretApproval.original, + + 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 + } as const; + + if (!SecretApprovalRequestsSecretsSchema.safeParse(payload).success) { + throw new Error(`Invalid secret approval payload: ${JSON.stringify(payload)}`); + } + + updatedSecretApprovals.push(payload); + } + + if (updatedSecrets.length !== secrets.length) { throw new Error("Failed to update some secrets"); } - if (secretVersionUpdates.length !== updatedSecretVersions.length) { + if (updatedSecretVersions.length !== secretVersions.length) { throw new Error("Failed to update some secret versions"); } - if (secretApprovalUpdates.length !== updatedSecretApprovals.length) { + if (updatedSecretApprovals.length !== approvalSecrets.length) { throw new Error("Failed to update some secret approvals"); } + const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(updatedSecrets, tx); + const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(updatedSecretVersions, tx); + const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement( + updatedSecretApprovals, + tx + ); + + if ( + secretUpdates.length !== updatedSecrets.length || + secretVersionUpdates.length !== updatedSecretVersions.length || + secretApprovalUpdates.length !== updatedSecretApprovals.length + ) { + throw new Error("Parts of the upgrade failed. Some secrets were not updated"); + } + await projectDAL.setProjectUpgradeStatus(data.projectId, null, tx); // await new Promise((resolve) => setTimeout(resolve, 15_000)); - throw new Error("Transaction was successful!"); + // throw new Error("Transaction was successful!"); }); } catch (err) { const [project] = await projectDAL diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index b3b538168..9c880938c 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -45,19 +45,32 @@ export const secretDALFactory = (db: TDbClient) => { } }; - const bulkUpdateNoVersionIncrement = async ( - data: Array<{ filter: Partial; data: TSecretsUpdate }>, - tx?: Knex - ) => { + const bulkUpdateNoVersionIncrement = async (data: TSecrets[], tx?: Knex) => { try { - const secs = await Promise.all( - data.map(async ({ filter, data: updateData }) => { - const [doc] = await (tx || db)(TableName.Secret).where(filter).update(updateData).returning("*"); - if (!doc) throw new BadRequestError({ message: "Failed to update document" }); - return doc; - }) + const existingSecrets = await secretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } ); - return secs; + + if (existingSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secrets do not exist" }); + } + + const updatedSecrets = await (tx || db)(TableName.Secret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + if (!updatedSecrets || updatedSecrets.length === 0) { + throw new BadRequestError({ message: "Failed to bulk update secret approvals" }); + } + + return updatedSecrets; } catch (error) { throw new DatabaseError({ error, name: "bulk update secret" }); } diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 275150f46..c9d9cff91 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -58,19 +58,32 @@ export const secretVersionDALFactory = (db: TDbClient) => { } }; - const bulkUpdateNoVersionIncrement = async ( - data: Array<{ filter: Partial; data: TSecretVersionsUpdate }>, - tx?: Knex - ) => { + const bulkUpdateNoVersionIncrement = async (data: TSecretVersions[], tx?: Knex) => { try { - const secs = await Promise.all( - data.map(async ({ filter, data: updateData }) => { - const [doc] = await (tx || db)(TableName.SecretVersion).where(filter).update(updateData).returning("*"); - if (!doc) throw new BadRequestError({ message: "Failed to update document" }); - return doc; - }) + const existingSecretVersions = await secretVersionOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } ); - return secs; + + if (existingSecretVersions.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret versions do not exist" }); + } + + const updatedSecretVersions = await (tx || db)(TableName.SecretVersion) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + if (!updatedSecretVersions || updatedSecretVersions.length === 0) { + throw new BadRequestError({ message: "Failed to bulk update secret versions" }); + } + + return updatedSecretVersions; } catch (error) { throw new DatabaseError({ error, name: "bulk update secret" }); }