From 079e005f49bbc26cb1bbf6bbea3ef14ea696bac1 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 9 Jul 2024 21:46:12 +0800 Subject: [PATCH] misc: added audit log and overwrite feature --- .../ee/services/audit-log/audit-log-types.ts | 13 +++++++ backend/src/server/routes/v3/secret-router.ts | 36 ++++++++++++++----- backend/src/services/secret/secret-service.ts | 36 +++++++++++++------ backend/src/services/secret/secret-types.ts | 1 + frontend/src/hooks/api/secrets/mutations.tsx | 20 ++++++++--- frontend/src/hooks/api/secrets/types.ts | 1 + .../components/ActionBar/ActionBar.tsx | 24 +++++++++++-- .../components/ActionBar/MoveSecretsModal.tsx | 35 +++++++++++++++--- 8 files changed, 136 insertions(+), 30 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 18930cfe88..92cf6c7014 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -45,6 +45,7 @@ export enum EventType { CREATE_SECRETS = "create-secrets", UPDATE_SECRET = "update-secret", UPDATE_SECRETS = "update-secrets", + MOVE_SECRETS = "move-secrets", DELETE_SECRET = "delete-secret", DELETE_SECRETS = "delete-secrets", GET_WORKSPACE_KEY = "get-workspace-key", @@ -228,6 +229,17 @@ interface UpdateSecretBatchEvent { }; } +interface MoveSecretsEvent { + type: EventType.MOVE_SECRETS; + metadata: { + sourceEnvironment: string; + sourceSecretPath: string; + destinationEnvironment: string; + destinationSecretPath: string; + secretIds: string[]; + }; +} + interface DeleteSecretEvent { type: EventType.DELETE_SECRET; metadata: { @@ -1030,6 +1042,7 @@ export type Event = | CreateSecretBatchEvent | UpdateSecretEvent | UpdateSecretBatchEvent + | MoveSecretsEvent | DeleteSecretEvent | DeleteSecretBatchEvent | GetWorkspaceKeyEvent diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 8010ff57a1..2a4f9b4641 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1338,27 +1338,45 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), destinationEnvironment: z.string().trim(), destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), - secretIds: z.string().array() + secretIds: z.string().array(), + shouldOverwrite: z.boolean().default(false) }), response: { - 200: z.union([ - z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() - }), - z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") - ]) + 200: z.object({ + isSourceUpdated: z.boolean(), + isDestinationUpdated: z.boolean() + }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - // TODO: publish audit log - return server.services.secret.moveSecrets({ + const { projectId, isSourceUpdated, isDestinationUpdated } = await server.services.secret.moveSecrets({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.MOVE_SECRETS, + metadata: { + sourceEnvironment: req.body.sourceEnvironment, + sourceSecretPath: req.body.sourceSecretPath, + destinationEnvironment: req.body.destinationEnvironment, + destinationSecretPath: req.body.destinationSecretPath, + secretIds: req.body.secretIds + } + } + }); + + return { + isSourceUpdated, + isDestinationUpdated + }; } }); diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 56f7f904aa..efe5af03e9 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1705,6 +1705,7 @@ export const secretServiceFactory = ({ destinationSecretPath, secretIds, projectSlug, + shouldOverwrite, actor, actorId, actorAuthMethod, @@ -1793,8 +1794,8 @@ export const secretServiceFactory = ({ }) })); - let isSourceFolderUpdated = false; - let isDestinationFolderUpdated = false; + let isSourceUpdated = false; + let isDestinationUpdated = false; // Moving secrets is a two-step process. await secretDAL.transaction(async (tx) => { @@ -1831,7 +1832,7 @@ export const secretServiceFactory = ({ const locallyCreatedSecrets = decryptedSourceSecrets .filter(({ secretBlindIndex }) => !destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]) - .map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create + .map((el) => ({ ...el, operation: SecretOperations.Create })); const locallyUpdatedSecrets = decryptedSourceSecrets .filter( @@ -1841,16 +1842,25 @@ export const secretServiceFactory = ({ (destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretKey !== secretKey || destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretValue !== secretValue) ) - .map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create + .map((el) => ({ ...el, operation: SecretOperations.Update })); + + if (locallyUpdatedSecrets.length > 0 && !shouldOverwrite) { + const existingKeys = locallyUpdatedSecrets.map((s) => s.secretKey); + + throw new BadRequestError({ + message: `Failed to move secrets. The following secrets already exist in the destination: ${existingKeys.join( + "," + )}` + }); + } const isEmpty = locallyCreatedSecrets.length + locallyUpdatedSecrets.length === 0; if (isEmpty) { throw new BadRequestError({ - message: "No changes were made. Secrets already exist in the destination." + message: "Selected secrets already exist in the destination." }); } - const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( project.id, destinationFolder.environment.slug, @@ -1974,7 +1984,7 @@ export const secretServiceFactory = ({ }); } - isDestinationFolderUpdated = true; + isDestinationUpdated = true; } // Next step is to delete the secrets from the source folder: @@ -2042,11 +2052,11 @@ export const secretServiceFactory = ({ tx ); - isSourceFolderUpdated = true; + isSourceUpdated = true; } }); - if (isDestinationFolderUpdated) { + if (isDestinationUpdated) { await snapshotService.performSnapshot(destinationFolder.id); await secretQueueService.syncSecrets({ projectId: project.id, @@ -2057,7 +2067,7 @@ export const secretServiceFactory = ({ }); } - if (isSourceFolderUpdated) { + if (isSourceUpdated) { await snapshotService.performSnapshot(sourceFolder.id); await secretQueueService.syncSecrets({ projectId: project.id, @@ -2067,6 +2077,12 @@ export const secretServiceFactory = ({ actor }); } + + return { + projectId: project.id, + isSourceUpdated, + isDestinationUpdated + }; }; return { diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 339deb9fba..d806eab11e 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -405,4 +405,5 @@ export type TMoveSecretsDTO = { destinationEnvironment: string; destinationSecretPath: string; secretIds: string[]; + shouldOverwrite: boolean; } & Omit; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 898d99b8d4..9c3fd23f0d 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -383,22 +383,34 @@ export const useMoveSecrets = ({ } = {}) => { const queryClient = useQueryClient(); - return useMutation<{}, {}, TMoveSecretsDTO>({ + return useMutation< + { + isSourceUpdated: boolean; + isDestinationUpdated: boolean; + }, + {}, + TMoveSecretsDTO + >({ mutationFn: async ({ sourceEnvironment, sourceSecretPath, projectSlug, destinationEnvironment, destinationSecretPath, - secretIds + secretIds, + shouldOverwrite }) => { - const { data } = await apiRequest.post("/api/v3/secrets/move", { + const { data } = await apiRequest.post<{ + isSourceUpdated: boolean; + isDestinationUpdated: boolean; + }>("/api/v3/secrets/move", { sourceEnvironment, sourceSecretPath, projectSlug, destinationEnvironment, destinationSecretPath, - secretIds + secretIds, + shouldOverwrite }); return data; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index f10907654f..de7e1d5031 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -185,6 +185,7 @@ export type TMoveSecretsDTO = { destinationEnvironment: string; destinationSecretPath: string; secretIds: string[]; + shouldOverwrite: boolean; }; export type CreateSecretDTO = { diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index a04d61b36f..c8806ac5dc 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -239,15 +239,18 @@ export const ActionBar = ({ const handleSecretsMove = async ({ destinationEnvironment, - destinationSecretPath + destinationSecretPath, + shouldOverwrite }: { destinationEnvironment: string; destinationSecretPath: string; + shouldOverwrite: boolean; }) => { try { const secretsToMove = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id])); - await moveSecrets({ + const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({ projectSlug, + shouldOverwrite, sourceEnvironment: environment, sourceSecretPath: secretPath, destinationEnvironment, @@ -256,10 +259,25 @@ export const ActionBar = ({ secretIds: secretsToMove.map((sec) => sec.id) }); + let successMessage = ""; + if (isDestinationUpdated && isSourceUpdated) { + successMessage = "Successfully moved selected secrets"; + } else if (isDestinationUpdated) { + successMessage = + "Successfully created secrets in destination. A secret approval request has been generated for the source."; + } else if (isSourceUpdated) { + successMessage = "A secret approval request has been generated in the destination"; + } else { + successMessage = + "A secret approval request has been generated in both the source and the destination."; + } + createNotification({ type: "success", - text: "Successfully moved selected secrets" + text: successMessage }); + + resetSelectedSecret(); } catch (error) { createNotification({ type: "error", diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/MoveSecretsModal.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/MoveSecretsModal.tsx index 00615b184f..606f23fac8 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/MoveSecretsModal.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/MoveSecretsModal.tsx @@ -2,7 +2,15 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { + Button, + Checkbox, + FormControl, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { useWorkspace } from "@app/context"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -13,6 +21,7 @@ type Props = { onMoveApproved: (moveParams: { destinationEnvironment: string; destinationSecretPath: string; + shouldOverwrite: boolean; }) => void; }; @@ -23,7 +32,8 @@ const formSchema = z.object({ .trim() .transform((val) => typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val - ) + ), + shouldOverwrite: z.boolean().default(false) }); type TFormSchema = z.infer; @@ -44,7 +54,8 @@ export const MoveSecretsModal = ({ popUp, handlePopUpToggle, onMoveApproved }: P const handleFormSubmit = (data: TFormSchema) => { onMoveApproved({ destinationEnvironment: data.environment, - destinationSecretPath: data.secretPath + destinationSecretPath: data.secretPath, + shouldOverwrite: data.shouldOverwrite }); handlePopUpToggle("moveSecrets", false); @@ -60,7 +71,7 @@ export const MoveSecretsModal = ({ popUp, handlePopUpToggle, onMoveApproved }: P >
)} /> + ( + + Overwrite existing secrets + + )} + />