From 6b6fd9735c15bd0c93075b8f2d0197370d8325d8 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 20 Jul 2024 00:47:48 +0800 Subject: [PATCH] misc: added ability for users to select KMS during project creation --- backend/src/server/routes/index.ts | 3 +- .../src/server/routes/v2/project-router.ts | 6 +- backend/src/services/kms/kms-service.ts | 2 +- .../src/services/project/project-service.ts | 28 +++++- backend/src/services/project/project-types.ts | 1 + frontend/src/hooks/api/kms/types.ts | 2 + frontend/src/hooks/api/workspace/queries.tsx | 10 +- frontend/src/hooks/api/workspace/types.ts | 1 + frontend/src/layouts/AppLayout/AppLayout.tsx | 99 ++++++++++++++----- .../src/pages/org/[id]/overview/index.tsx | 99 ++++++++++++++----- .../EncryptionTab/EncryptionTab.tsx | 3 +- 11 files changed, 197 insertions(+), 57 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9fc47c046..222420346 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -627,7 +627,8 @@ export const registerRoutes = async ( projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, keyStore, - kmsService + kmsService, + kmsDAL }); const projectEnvService = projectEnvServiceFactory({ diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index e59291664..c48b53d82 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -161,7 +161,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { message: "Slug must be a valid slug" }) .optional() - .describe(PROJECTS.CREATE.slug) + .describe(PROJECTS.CREATE.slug), + kmsKeyId: z.string().optional() }), response: { 200: z.object({ @@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, workspaceName: req.body.projectName, - slug: req.body.slug + slug: req.body.slug, + kmsKeyId: req.body.kmsKeyId }); await server.services.telemetry.sendPostHogEvents({ diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index f977c01d7..51ba93d9b 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -40,7 +40,7 @@ type TKmsServiceFactoryDep = { export type TKmsServiceFactory = ReturnType; -const INTERNAL_KMS_KEY_ID = "internal"; +export const INTERNAL_KMS_KEY_ID = "internal"; const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 3a58d1052..4876f8348 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; +import { TKmsKeyDALFactory } from "../kms/kms-key-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; @@ -83,6 +84,7 @@ type TProjectServiceFactoryDep = { TKmsServiceFactory, "updateProjectSecretManagerKmsKey" | "getProjectKeyBackup" | "loadProjectKeyBackup" >; + kmsDAL: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -108,7 +110,8 @@ export const projectServiceFactory = ({ certificateAuthorityDAL, certificateDAL, keyStore, - kmsService + kmsService, + kmsDAL }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -119,7 +122,8 @@ export const projectServiceFactory = ({ actorOrgId, actorAuthMethod, workspaceName, - slug: projectSlug + slug: projectSlug, + kmsKeyId }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); @@ -147,16 +151,34 @@ export const projectServiceFactory = ({ const results = await projectDAL.transaction(async (tx) => { const ghostUser = await orgService.addGhostUser(organization.id, tx); + if (kmsKeyId) { + const kms = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId, tx); + + if (!kms.id) { + throw new NotFoundError({ + message: "KMS not found" + }); + } + + if (kms.orgId !== organization.id) { + throw new BadRequestError({ + message: "KMS does not belong in the organization" + }); + } + } + const project = await projectDAL.create( { name: workspaceName, orgId: organization.id, slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), version: ProjectVersion.V2, - pitVersionLimit: 10 + pitVersionLimit: 10, + kmsSecretManagerKeyId: kmsKeyId }, tx ); + // set ghost user as admin of project const projectMembership = await projectMembershipDAL.create( { diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 38d436018..4b2fb21e2 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -27,6 +27,7 @@ export type TCreateProjectDTO = { actorOrgId?: string; workspaceName: string; slug?: string; + kmsKeyId?: string; }; export type TDeleteProjectBySlugDTO = { diff --git a/frontend/src/hooks/api/kms/types.ts b/frontend/src/hooks/api/kms/types.ts index a869d0410..c51c359f3 100644 --- a/frontend/src/hooks/api/kms/types.ts +++ b/frontend/src/hooks/api/kms/types.ts @@ -29,3 +29,5 @@ export type KmsListEntry = { export enum ExternalKmsProvider { AWS = "aws" } + +export const INTERNAL_KMS_KEY_ID = "internal"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 11579b760..679f1bc7d 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -226,18 +226,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => }); export const createWorkspace = ({ - projectName + projectName, + kmsKeyId }: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => { - return apiRequest.post("/api/v2/workspace", { projectName }); + return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId }); }; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ projectName }) => + mutationFn: async ({ projectName, kmsKeyId }) => createWorkspace({ - projectName + projectName, + kmsKeyId }), onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 53994e88c..e2a31725d 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -48,6 +48,7 @@ export type TGetUpgradeProjectStatusDTO = { // mutation dto export type CreateWorkspaceDTO = { projectName: string; + kmsKeyId?: string; }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index be4cf2086..76f0813f4 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage"; import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, Button, Checkbox, DropdownMenu, @@ -66,11 +70,13 @@ import { useAddUserToWsNonE2EE, useCreateWorkspace, useGetAccessRequestsCount, + useGetExternalKmsList, useGetOrgTrialUrl, useGetSecretApprovalRequestCount, useLogoutUser, useSelectOrganization } from "@app/hooks/api"; +import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { Workspace } from "@app/hooks/api/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; @@ -113,7 +119,8 @@ const formSchema = yup.object({ .label("Project Name") .trim() .max(64, "Too long, maximum length is 64 characters"), - addMembers: yup.bool().required().label("Add Members") + addMembers: yup.bool().required().label("Add Members"), + kmsKeyId: yup.string().label("KMS Key ID") }); type TAddProjectFormData = yup.InferType; @@ -147,6 +154,7 @@ export const AppLayout = ({ children }: LayoutProps) => { const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); + const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!); const pendingRequestsCount = useMemo(() => { return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); @@ -172,7 +180,10 @@ export const AppLayout = ({ children }: LayoutProps) => { reset, handleSubmit } = useForm({ - resolver: yupResolver(formSchema) + resolver: yupResolver(formSchema), + defaultValues: { + kmsKeyId: INTERNAL_KMS_KEY_ID + } }); const { t } = useTranslation(); @@ -245,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => { putUserInOrg(); }, [router.query.id]); - const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { + const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => { // type check if (!currentOrg) return; if (!user) return; @@ -255,7 +266,8 @@ export const AppLayout = ({ children }: LayoutProps) => { project: { id: newProjectId } } } = await createWs.mutateAsync({ - projectName: name + projectName: name, + kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined }); if (addMembers) { @@ -889,24 +901,67 @@ export const AppLayout = ({ children }: LayoutProps) => { )} /> -
- - +
+ + + +
Advanced Settings
+
+ + ( + + + + )} + control={control} + name="kmsKeyId" + /> + +
+
+
+ + +
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 6956f71a3..dc5c20a70 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, Button, Checkbox, FormControl, @@ -43,6 +47,8 @@ import { Input, Modal, ModalContent, + Select, + SelectItem, Skeleton, UpgradePlanModal } from "@app/components/v2"; @@ -59,8 +65,10 @@ import { fetchOrgUsers, useAddUserToWsNonE2EE, useCreateWorkspace, + useGetExternalKmsList, useRegisterUserAction } from "@app/hooks/api"; +import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { Workspace } from "@app/hooks/api/types"; @@ -473,7 +481,8 @@ const formSchema = yup.object({ .label("Project Name") .trim() .max(64, "Too long, maximum length is 64 characters"), - addMembers: yup.bool().required().label("Add Members") + addMembers: yup.bool().required().label("Add Members"), + kmsKeyId: yup.string().label("KMS Key ID") }); type TAddProjectFormData = yup.InferType; @@ -506,7 +515,10 @@ const OrganizationPage = withPermission( reset, handleSubmit } = useForm({ - resolver: yupResolver(formSchema) + resolver: yupResolver(formSchema), + defaultValues: { + kmsKeyId: INTERNAL_KMS_KEY_ID + } }); const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); @@ -521,7 +533,9 @@ const OrganizationPage = withPermission( (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID ); - const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { + const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!); + + const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => { // type check if (!currentOrg) return; if (!user) return; @@ -531,7 +545,8 @@ const OrganizationPage = withPermission( project: { id: newProjectId } } } = await createWs.mutateAsync({ - projectName: name + projectName: name, + kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined }); if (addMembers) { @@ -1064,24 +1079,64 @@ const OrganizationPage = withPermission( )} />
-
- - +
+ + + +
Advanced Settings
+
+ + ( + + + + )} + control={control} + name="kmsKeyId" + /> + +
+
+
+ + +
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx index 7acb5b069..b5fa6fe5f 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx @@ -31,6 +31,7 @@ import { useUpdateProjectKms } from "@app/hooks/api"; import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries"; +import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { Organization, Workspace } from "@app/hooks/api/types"; const formSchema = z.object({ @@ -39,8 +40,6 @@ const formSchema = z.object({ type TForm = z.infer; -const INTERNAL_KMS_KEY_ID = "internal"; - const BackupConfirmationModal = ({ isOpen, onOpenChange,