From dd46a210359c105bae04879646c4d4ead3aff691 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 16 Jul 2024 21:29:20 +0800 Subject: [PATCH] feat: finalized kms settings in org-level --- .../src/ee/routes/v1/external-kms-router.ts | 42 +++++ .../services/external-kms/external-kms-dal.ts | 2 + frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/kms/index.tsx | 3 +- frontend/src/hooks/api/kms/mutations.tsx | 81 +++++++++- frontend/src/hooks/api/kms/queries.tsx | 35 +++++ frontend/src/hooks/api/kms/types.ts | 31 ++++ .../AddExternalKmsForm.tsx | 7 +- .../AddExternalKmsForm/index.tsx | 1 - .../{AddExternalKmsForm => }/AwsKmsForm.tsx | 111 ++++++++++---- .../OrgEncryptionTab/OrgEncryptionTab.tsx | 144 ++++++++++++++++-- .../UpdateExternalKmsForm.tsx | 29 ++++ 12 files changed, 435 insertions(+), 52 deletions(-) create mode 100644 frontend/src/hooks/api/kms/queries.tsx create mode 100644 frontend/src/hooks/api/kms/types.ts rename frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/{AddExternalKmsForm => }/AddExternalKmsForm.tsx (95%) delete mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/index.tsx rename frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/{AddExternalKmsForm => }/AwsKmsForm.tsx (73%) create mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/UpdateExternalKmsForm.tsx diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts index 712b8b456..233edd968 100644 --- a/backend/src/ee/routes/v1/external-kms-router.ts +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -19,6 +19,23 @@ const sanitizedExternalSchema = KmsKeysSchema.extend({ }) }); +const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({ + id: true, + description: true, + isDisabled: true, + createdAt: true, + updatedAt: true, + slug: true +}) + .extend({ + externalKms: ExternalKmsSchema.pick({ + provider: true, + status: true, + statusDetails: true + }) + }) + .array(); + const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({ external: ExternalKmsSchema.pick({ id: true, @@ -159,6 +176,31 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + externalKmsList: sanitizedExternalSchemaForGetAll + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKmsList = await server.services.externalKms.list({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + return { externalKmsList }; + } + }); + server.route({ method: "GET", url: "/slug/:slug", diff --git a/backend/src/ee/services/external-kms/external-kms-dal.ts b/backend/src/ee/services/external-kms/external-kms-dal.ts index bb9a6ce8d..7077b4aa9 100644 --- a/backend/src/ee/services/external-kms/external-kms-dal.ts +++ b/backend/src/ee/services/external-kms/external-kms-dal.ts @@ -31,6 +31,8 @@ export const externalKmsDALFactory = (db: TDbClient) => { isReserved: el.isReserved, orgId: el.orgId, slug: el.slug, + createdAt: el.createdAt, + updatedAt: el.updatedAt, externalKms: { id: el.externalKmsId, provider: el.externalKmsProvider, diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 08e0b59ba..ea3e7f560 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -16,6 +16,7 @@ export * from "./incidentContacts"; export * from "./integrationAuth"; export * from "./integrations"; export * from "./keys"; +export * from "./kms"; export * from "./ldapConfig"; export * from "./oidcConfig"; export * from "./organization"; diff --git a/frontend/src/hooks/api/kms/index.tsx b/frontend/src/hooks/api/kms/index.tsx index c702ecaf6..9c00dae9f 100644 --- a/frontend/src/hooks/api/kms/index.tsx +++ b/frontend/src/hooks/api/kms/index.tsx @@ -1 +1,2 @@ -export { useAddAwsExternalKms } from "./mutations"; +export { useAddAwsExternalKms, useRemoveExternalKms, useUpdateAwsExternalKms } from "./mutations"; +export { useGetExternalKmsById, useGetExternalKmsList } from "./queries"; diff --git a/frontend/src/hooks/api/kms/mutations.tsx b/frontend/src/hooks/api/kms/mutations.tsx index ee778198c..5e0c9f43e 100644 --- a/frontend/src/hooks/api/kms/mutations.tsx +++ b/frontend/src/hooks/api/kms/mutations.tsx @@ -1,8 +1,11 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -export const useAddAwsExternalKms = () => { +import { kmsKeys } from "./queries"; + +export const useAddAwsExternalKms = (orgId: string) => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ slug, @@ -48,6 +51,78 @@ export const useAddAwsExternalKms = () => { return data; }, - onSuccess() {} + onSuccess: () => { + queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId)); + } + }); +}; + +export const useUpdateAwsExternalKms = (orgId: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + kmsId, + slug, + description, + credentialType, + accessKey, + secretKey, + assumeRoleArn, + externalId, + awsRegion, + kmsKeyId + }: { + kmsId: string; + slug?: string; + description?: string; + credentialType?: string; + accessKey?: string; + secretKey?: string; + assumeRoleArn?: string; + externalId?: string; + awsRegion: string; + kmsKeyId?: string; + }) => { + const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, { + slug, + description, + provider: { + type: "aws", + inputs: { + credential: { + type: credentialType, + data: { + accessKey, + secretKey, + assumeRoleArn, + externalId + } + }, + awsRegion, + kmsKeyId + } + } + }); + + return data; + }, + onSuccess: (_, { kmsId }) => { + queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId)); + queryClient.invalidateQueries(kmsKeys.getExternalKmsById(kmsId)); + } + }); +}; + +export const useRemoveExternalKms = (orgId: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (kmsId: string) => { + const { data } = await apiRequest.delete(`/api/v1/external-kms/${kmsId}`); + + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId)); + } }); }; diff --git a/frontend/src/hooks/api/kms/queries.tsx b/frontend/src/hooks/api/kms/queries.tsx new file mode 100644 index 000000000..572a69e06 --- /dev/null +++ b/frontend/src/hooks/api/kms/queries.tsx @@ -0,0 +1,35 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { Kms, KmsListEntry } from "./types"; + +export const kmsKeys = { + getExternalKmsList: (orgId: string) => ["get-all-external-kms", { orgId }], + getExternalKmsById: (id: string) => ["get-external-kms", { id }] +}; + +export const useGetExternalKmsList = (orgId: string) => { + return useQuery({ + queryKey: kmsKeys.getExternalKmsList(orgId), + queryFn: async () => { + const { + data: { externalKmsList } + } = await apiRequest.get<{ externalKmsList: KmsListEntry[] }>("/api/v1/external-kms"); + return externalKmsList; + } + }); +}; + +export const useGetExternalKmsById = (kmsId: string) => { + return useQuery({ + queryKey: kmsKeys.getExternalKmsById(kmsId), + enabled: Boolean(kmsId), + queryFn: async () => { + const { + data: { externalKms } + } = await apiRequest.get<{ externalKms: Kms }>(`/api/v1/external-kms/${kmsId}`); + return externalKms; + } + }); +}; diff --git a/frontend/src/hooks/api/kms/types.ts b/frontend/src/hooks/api/kms/types.ts new file mode 100644 index 000000000..a869d0410 --- /dev/null +++ b/frontend/src/hooks/api/kms/types.ts @@ -0,0 +1,31 @@ +export type Kms = { + id: string; + description: string; + orgId: string; + slug: string; + external: { + id: string; + status: string; + statusDetails: string; + provider: string; + providerInput: Record; + }; +}; + +export type KmsListEntry = { + id: string; + description: string; + isDisabled: boolean; + createdAt: string; + updatedAt: string; + slug: string; + externalKms: { + provider: string; + status: string; + statusDetails: string; + }; +}; + +export enum ExternalKmsProvider { + AWS = "aws" +} diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/AddExternalKmsForm.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm.tsx similarity index 95% rename from frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/AddExternalKmsForm.tsx rename to frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm.tsx index 30d061028..70debab36 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/AddExternalKmsForm.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; +import { ExternalKmsProvider } from "@app/hooks/api/kms/types"; import { AwsKmsForm } from "./AwsKmsForm"; @@ -17,10 +18,6 @@ enum WizardSteps { ProviderInputs = "provider-inputs" } -enum ExternalKmsProvider { - AWS = "aws" -} - const EXTERNAL_KMS_LIST = [ { icon: faAws, @@ -90,7 +87,7 @@ export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: -30 }} > - {}} onCompleted={() => {}} /> + onToggle(false)} onCompleted={() => onToggle(false)} /> )} diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/index.tsx deleted file mode 100644 index 53bdefd3c..000000000 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AddExternalKmsForm } from "./AddExternalKmsForm"; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/AwsKmsForm.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx similarity index 73% rename from frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/AwsKmsForm.tsx rename to frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx index 812637279..45f2f58a5 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AddExternalKmsForm/AwsKmsForm.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx @@ -5,7 +5,9 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2"; -import { useAddAwsExternalKms } from "@app/hooks/api/kms"; +import { useOrganization } from "@app/context"; +import { useAddAwsExternalKms, useUpdateAwsExternalKms } from "@app/hooks/api"; +import { Kms } from "@app/hooks/api/kms/types"; const AWS_REGIONS = [ { name: "US East (Ohio)", slug: "us-east-2" }, @@ -78,47 +80,95 @@ type TForm = z.infer; type Props = { onCompleted: () => void; onCancel: () => void; + kms?: Kms; }; -export const AwsKmsForm = ({ onCompleted, onCancel }: Props) => { +export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { const { control, handleSubmit, watch, + setValue, formState: { isSubmitting } } = useForm({ - resolver: zodResolver(formSchema) + resolver: zodResolver(formSchema), + defaultValues: { + slug: kms?.slug, + description: kms?.description, + credential: { + type: kms?.external?.providerInput?.credential?.type, + data: { + accessKey: kms?.external?.providerInput?.credential?.data?.accessKey, + secretKey: kms?.external?.providerInput?.credential?.data?.secretKey, + assumeRoleArn: kms?.external?.providerInput?.credential?.data?.assumeRoleArn, + externalId: kms?.external?.providerInput?.credential?.data?.externalId + } + }, + awsRegion: kms?.external?.providerInput?.awsRegion, + kmsKeyId: kms?.external?.providerInput?.kmsKeyId + } }); + const { currentOrg } = useOrganization(); + const { mutateAsync: addAwsExternalKms } = useAddAwsExternalKms(currentOrg?.id!); + const { mutateAsync: updateAwsExternalKms } = useUpdateAwsExternalKms(currentOrg?.id!); + const selectedAwsAuthType = watch("credential.type"); - const { mutateAsync: addAwsExternalKms } = useAddAwsExternalKms(); - const handleAddAwsKms = async (data: TForm) => { const { slug, description, credential, awsRegion, kmsKeyId } = data; - await addAwsExternalKms({ - slug, - description, - credentialType: credential.type, - awsRegion, - kmsKeyId, - ...(credential.type === KmsAwsCredentialType.AccessKey - ? { - accessKey: credential.data.accessKey, - secretKey: credential.data.secretKey - } - : { - assumeRoleArn: credential.data.assumeRoleArn, - externalId: credential.data.externalId - }) - }); + try { + if (kms) { + await updateAwsExternalKms({ + kmsId: kms.id, + slug, + description, + credentialType: credential.type, + awsRegion, + kmsKeyId, + ...(credential.type === KmsAwsCredentialType.AccessKey + ? { + accessKey: credential.data.accessKey, + secretKey: credential.data.secretKey + } + : { + assumeRoleArn: credential.data.assumeRoleArn, + externalId: credential.data.externalId + }) + }); - createNotification({ - text: "Successfully added AWS External KMS", - type: "success" - }); + createNotification({ + text: "Successfully updated AWS External KMS", + type: "success" + }); + } else { + await addAwsExternalKms({ + slug, + description, + credentialType: credential.type, + awsRegion, + kmsKeyId, + ...(credential.type === KmsAwsCredentialType.AccessKey + ? { + accessKey: credential.data.accessKey, + secretKey: credential.data.secretKey + } + : { + assumeRoleArn: credential.data.assumeRoleArn, + externalId: credential.data.externalId + }) + }); - onCompleted(); + createNotification({ + text: "Successfully added AWS External KMS", + type: "success" + }); + } + + onCompleted(); + } catch (err) { + console.error(err); + } }; return ( @@ -154,7 +204,14 @@ export const AwsKmsForm = ({ onCompleted, onCancel }: Props) => {