diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index f47428fc7..e3b57802f 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal"; import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; import { TSecretQueueFactory } from "./secret-queue"; import { + TAttachSecretTagsDTO, TCreateBulkSecretDTO, TCreateSecretDTO, TCreateSecretRawDTO, @@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = { secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; folderDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; @@ -307,6 +308,7 @@ export const secretServiceFactory = ({ if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); const { secretName, ...el } = inputSecret; + const updatedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ folderId, @@ -442,6 +444,7 @@ export const secretServiceFactory = ({ const folderId = folder.id; const secrets = await secretDAL.findByFolderId(folderId, actorId); + if (includeImports) { const secretImports = await secretImportDAL.find({ folderId }); const allowedImports = secretImports.filter(({ importEnv, importPath }) => @@ -994,7 +997,209 @@ export const secretServiceFactory = ({ return secretVersions; }; + const attachTags = async ({ + secretName, + tagSlugs, + path: secretPath, + environment, + type, + projectSlug, + actor, + actorAuthMethod, + actorOrgId, + actorId + }: TAttachSecretTagsDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + await projectDAL.checkProjectUpgradeStatus(project.id); + + const secret = await getSecretByName({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId: project.id, + environment, + path: secretPath, + secretName, + type + }); + + if (!secret) { + throw new BadRequestError({ message: "Secret not found" }); + } + const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath); + + if (!folder) { + throw new BadRequestError({ message: "Folder not found" }); + } + + const tags = await secretTagDAL.find({ + projectId: project.id, + $in: { + slug: tagSlugs + } + }); + + if (tags.length !== tagSlugs.length) { + throw new BadRequestError({ message: "One or more tags not found." }); + } + + const secretTags = await secretDAL.getSecretTags(secret.id); + + if (secretTags.some((tag) => tagSlugs.includes(tag.slug))) { + throw new BadRequestError({ message: "One or more tags already exist on the secret" }); + } + + const combinedTags = new Set([...secretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]); + + const updatedSecret = await secretDAL.transaction(async (tx) => + fnSecretBulkUpdate({ + folderId: folder.id, + projectId: project.id, + inputSecrets: [ + { + filter: { id: secret.id }, + data: { + tags: Array.from(combinedTags) + } + } + ], + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + tx + }) + ); + + await snapshotService.performSnapshot(folder.id); + await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment }); + + return { + ...updatedSecret[0], + tags: [...secretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color })) + }; + }; + + const detachTags = async ({ + secretName, + tagSlugs, + path: secretPath, + environment, + type, + projectSlug, + actor, + actorAuthMethod, + actorOrgId, + actorId + }: TAttachSecretTagsDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + await projectDAL.checkProjectUpgradeStatus(project.id); + + const secret = await getSecretByName({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId: project.id, + environment, + path: secretPath, + secretName, + type + }); + + if (!secret) { + throw new BadRequestError({ message: "Secret not found" }); + } + const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath); + + if (!folder) { + throw new BadRequestError({ message: "Folder not found" }); + } + + const tags = await secretTagDAL.find({ + projectId: project.id, + $in: { + slug: tagSlugs + } + }); + + if (tags.length !== tagSlugs.length) { + throw new BadRequestError({ message: "One or more tags not found." }); + } + + const secretTags = await secretDAL.getSecretTags(secret.id); + + // Make sure all the tags exist on the secret + const tagIdsToRemove = tags.map((tag) => tag.id); + const secretTagIds = secretTags.map((tag) => tag.id); + + if (!tagIdsToRemove.every((el) => secretTagIds.includes(el))) { + throw new BadRequestError({ message: "One or more tags not found on the secret" }); + } + + const newTags = secretTags.filter((tag) => !tagIdsToRemove.includes(tag.id)); + + const updatedSecret = await secretDAL.transaction(async (tx) => + fnSecretBulkUpdate({ + folderId: folder.id, + projectId: project.id, + inputSecrets: [ + { + filter: { id: secret.id }, + data: { + tags: newTags.map((tag) => tag.id) + } + } + ], + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + tx + }) + ); + + await snapshotService.performSnapshot(folder.id); + await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment }); + + return { + ...updatedSecret[0], + tags: newTags + }; + }; + return { + attachTags, + detachTags, createSecret, deleteSecret, updateSecret, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 7ad4d65d7..efd4f0f8b 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = { tx?: Knex; }; +export type TAttachSecretTagsDTO = { + projectSlug: string; + secretName: string; + tagSlugs: string[]; + environment: string; + path: string; + type: SecretType; +} & Omit; + export type TFnSecretBulkDelete = { folderId: string; projectId: string;