From 7e5c3e8163fbb0a8c34263d3360b04e0b847faa6 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 18 Jul 2024 03:00:43 +0800 Subject: [PATCH] misc: partial project kms switch --- backend/src/@types/fastify.d.ts | 2 + backend/src/ee/routes/v1/project-router.ts | 69 +++++++++++++ backend/src/server/routes/index.ts | 4 +- backend/src/services/kms/kms-service.ts | 99 ++++++++++++++++++- backend/src/services/kms/kms-types.ts | 6 ++ frontend/src/hooks/api/kms/index.tsx | 9 +- frontend/src/hooks/api/kms/mutations.tsx | 14 +++ frontend/src/hooks/api/kms/queries.tsx | 22 ++++- .../EncryptionTab/EncryptionTab.tsx | 69 +++++++++++-- 9 files changed, 277 insertions(+), 17 deletions(-) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8ae892560..7b6ef592b 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -48,6 +48,7 @@ import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-a import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TProjectServiceFactory } from "@app/services/project/project-service"; @@ -164,6 +165,7 @@ declare module "fastify" { secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; + kms: TKmsServiceFactory; externalKms: TExternalKmsServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index 8639ef1a1..c31293516 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -171,4 +171,73 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), handler: async () => ({ actors: [] }) }); + + server.route({ + method: "GET", + url: "/:workspaceId/kms", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + secretManagerKmsKey: z.object({ + id: z.string(), + slug: z.string(), + isExternal: z.boolean() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretManagerKmsKey = await server.services.kms.getProjectSecretManagerKmsKey(req.params.workspaceId); + return { + secretManagerKmsKey + }; + } + }); + + server.route({ + method: "PATCH", + url: "/:workspaceId/kms", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + secretManagerKmsKeyId: z.string() + }), + response: { + 200: z.object({ + secretManagerKmsKey: z.object({ + id: z.string(), + slug: z.string(), + isExternal: z.boolean() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretManagerKmsKey } = await server.services.kms.updateProjectKmsKey({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + ...req.body + }); + + return { + secretManagerKmsKey + }; + } + }); }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 39fdd3c25..c2a80a530 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -310,7 +310,8 @@ export const registerRoutes = async ( kmsDAL, internalKmsDAL, orgDAL, - projectDAL + projectDAL, + permissionService }); const externalKmsService = externalKmsServiceFactory({ kmsDAL, @@ -1053,6 +1054,7 @@ export const registerRoutes = async ( identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, secretSharing: secretSharingService, userEngagement: userEngagementService, + kms: kmsService, externalKms: externalKmsService }); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index d8597af77..6e2c830a8 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,8 +1,11 @@ import crypto from "node:crypto"; +import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; @@ -21,7 +24,8 @@ import { TDecryptWithKmsDTO, TEncryptionWithKeyDTO, TEncryptWithKmsDTO, - TGenerateKMSDTO + TGenerateKMSDTO, + TUpdateProjectKmsDTO } from "./kms-types"; type TKmsServiceFactoryDep = { @@ -30,6 +34,7 @@ type TKmsServiceFactoryDep = { orgDAL: Pick; kmsRootConfigDAL: Pick; keyStore: Pick; + permissionService: Pick; internalKmsDAL: Pick; }; @@ -49,7 +54,8 @@ export const kmsServiceFactory = ({ keyStore, internalKmsDAL, orgDAL, - projectDAL + projectDAL, + permissionService }: TKmsServiceFactoryDep) => { let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); @@ -272,6 +278,13 @@ export const kmsServiceFactory = ({ return project.kmsSecretManagerKeyId; }; + const getProjectSecretManagerKmsKey = async (projectId: string) => { + const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId); + const kmsKey = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId); + + return kmsKey; + }; + const getProjectSecretManagerKmsDataKey = async (projectId: string) => { const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId); let project = await projectDAL.findById(projectId); @@ -329,6 +342,82 @@ export const kmsServiceFactory = ({ }); }; + const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => { + const currentKms = await getProjectSecretManagerKmsKey(projectId); + const dataKey = await getProjectSecretManagerKmsDataKey(projectId); + + if (currentKms.isReserved && kmsId === "internal") { + return currentKms; + } + + return kmsDAL.transaction(async (tx) => { + const project = await projectDAL.findById(projectId, tx); + let newKmsId = kmsId; + + if (newKmsId === "internal") { + const key = await generateKmsKey({ + isReserved: true, + orgId: project.orgId, + tx + }); + + newKmsId = key.id; + } + + const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }); + const { cipherTextBlob } = kmsEncryptor({ plainText: dataKey }); + await projectDAL.updateById( + projectId, + { + kmsSecretManagerKeyId: newKmsId, + kmsSecretManagerEncryptedDataKey: cipherTextBlob + }, + tx + ); + + if (currentKms.isReserved) { + await kmsDAL.deleteById(currentKms.id, tx); + } + + return kmsDAL.findByIdWithAssociatedKms(newKmsId); + }); + }; + + const updateProjectKmsKey = async ({ + projectId, + secretManagerKmsKeyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TUpdateProjectKmsDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + if (secretManagerKmsKeyId !== "internal") { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(secretManagerKmsKeyId); + if (!kmsDoc) { + throw new BadRequestError({ message: "KMS ID not found." }); + } + + if (kmsDoc.orgId !== actorOrgId) { + throw new BadRequestError({ + message: "KMS ID does not belong in the organization." + }); + } + } + + return { + secretManagerKmsKey: await updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId) + }; + }; + const startService = async () => { const appCfg = getConfig(); // This will switch to a seal process and HMS flow in future @@ -383,6 +472,10 @@ export const kmsServiceFactory = ({ decryptWithInputKey, getOrgKmsKeyId, getProjectSecretManagerKmsKeyId, - getOrgKmsDataKey + getOrgKmsDataKey, + getProjectSecretManagerKmsDataKey, + getProjectSecretManagerKmsKey, + updateProjectKmsKey, + updateProjectSecretManagerKmsKey }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 5ba6c1343..3f4170930 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,5 +1,7 @@ import { Knex } from "knex"; +import { TProjectPermission } from "@app/lib/types"; + export type TGenerateKMSDTO = { orgId: string; isReserved?: boolean; @@ -26,3 +28,7 @@ export type TDecryptWithKeyDTO = { key: Buffer; cipherTextBlob: Buffer; }; + +export type TUpdateProjectKmsDTO = { + secretManagerKmsKeyId: string; +} & TProjectPermission; diff --git a/frontend/src/hooks/api/kms/index.tsx b/frontend/src/hooks/api/kms/index.tsx index 9c00dae9f..7702b9fcd 100644 --- a/frontend/src/hooks/api/kms/index.tsx +++ b/frontend/src/hooks/api/kms/index.tsx @@ -1,2 +1,7 @@ -export { useAddAwsExternalKms, useRemoveExternalKms, useUpdateAwsExternalKms } from "./mutations"; -export { useGetExternalKmsById, useGetExternalKmsList } from "./queries"; +export { + useAddAwsExternalKms, + useRemoveExternalKms, + useUpdateAwsExternalKms, + useUpdateProjectKms +} from "./mutations"; +export { useGetActiveProjectKms, useGetExternalKmsById, useGetExternalKmsList } from "./queries"; diff --git a/frontend/src/hooks/api/kms/mutations.tsx b/frontend/src/hooks/api/kms/mutations.tsx index 5e0c9f43e..8a92af8d4 100644 --- a/frontend/src/hooks/api/kms/mutations.tsx +++ b/frontend/src/hooks/api/kms/mutations.tsx @@ -126,3 +126,17 @@ export const useRemoveExternalKms = (orgId: string) => { } }); }; + +export const useUpdateProjectKms = (projectId: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (updatedData: { secretManagerKmsKeyId: string }) => { + const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, updatedData); + + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries(kmsKeys.getActiveProjectKms(projectId)); + } + }); +}; diff --git a/frontend/src/hooks/api/kms/queries.tsx b/frontend/src/hooks/api/kms/queries.tsx index 572a69e06..4b84db74e 100644 --- a/frontend/src/hooks/api/kms/queries.tsx +++ b/frontend/src/hooks/api/kms/queries.tsx @@ -6,7 +6,8 @@ import { Kms, KmsListEntry } from "./types"; export const kmsKeys = { getExternalKmsList: (orgId: string) => ["get-all-external-kms", { orgId }], - getExternalKmsById: (id: string) => ["get-external-kms", { id }] + getExternalKmsById: (id: string) => ["get-external-kms", { id }], + getActiveProjectKms: (projectId: string) => ["get-active-project-kms", { projectId }] }; export const useGetExternalKmsList = (orgId: string) => { @@ -33,3 +34,22 @@ export const useGetExternalKmsById = (kmsId: string) => { } }); }; + +export const useGetActiveProjectKms = (projectId: string) => { + return useQuery({ + queryKey: kmsKeys.getActiveProjectKms(projectId), + enabled: Boolean(projectId), + queryFn: async () => { + const { + data: { secretManagerKmsKey } + } = await apiRequest.get<{ + secretManagerKmsKey: { + id: string; + slug: string; + isExternal: string; + }; + }>(`/api/v1/workspace/${projectId}/kms`); + return secretManagerKmsKey; + } + }); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx index b531399c4..7e4ae1713 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx @@ -1,19 +1,62 @@ import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, ContentLoader, FormControl, Select, SelectItem } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useOrganization } from "@app/context"; -import { useGetExternalKmsList } from "@app/hooks/api"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useWorkspace +} from "@app/context"; +import { useGetActiveProjectKms, useGetExternalKmsList, useUpdateProjectKms } from "@app/hooks/api"; + +const formSchema = z.object({ + kmsKeyId: z.string() +}); + +type TForm = z.infer; export const EncryptionTab = () => { - const { handleSubmit, control } = useForm(); const { currentOrg } = useOrganization(); + const { currentWorkspace } = useWorkspace(); const { data: externalKmsList, isLoading: isExternalKmsListLoading } = useGetExternalKmsList( currentOrg?.id! ); + const { data: activeKms, isLoading: isActiveKmsLoading } = useGetActiveProjectKms( + currentWorkspace?.id! + ); - const onFormSubmit = () => {}; + const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + kmsKeyId: activeKms?.isExternal ? activeKms?.id : "internal" + } + }); + + const onFormSubmit = async (data: TForm) => { + try { + await updateProjectKms({ + secretManagerKmsKeyId: data.kmsKeyId + }); + + createNotification({ + text: "Successfully updated project KMS", + type: "success" + }); + } catch (err) { + console.error(err); + } + }; return (
{ >

Key Management

- Select which Key Management System to use for encrypting project data + Select which Key Management System to use for encrypting your project data

- {isExternalKmsListLoading ? ( + {isExternalKmsListLoading || isActiveKmsLoading ? ( ) : ( (