feat: finalized kms settings in org-level

This commit is contained in:
Sheen Capadngan
2024-07-16 21:29:20 +08:00
committed by =
parent 26a5d74b14
commit dd46a21035
12 changed files with 435 additions and 52 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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";

View File

@@ -1 +1,2 @@
export { useAddAwsExternalKms } from "./mutations";
export { useAddAwsExternalKms, useRemoveExternalKms, useUpdateAwsExternalKms } from "./mutations";
export { useGetExternalKmsById, useGetExternalKmsList } from "./queries";

View File

@@ -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));
}
});
};

View File

@@ -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;
}
});
};

View File

@@ -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<string, any>;
};
};
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"
}

View File

@@ -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 }}
>
<AwsKmsForm onCancel={() => {}} onCompleted={() => {}} />
<AwsKmsForm onCancel={() => onToggle(false)} onCompleted={() => onToggle(false)} />
</motion.div>
)}
</AnimatePresence>

View File

@@ -1 +0,0 @@
export { AddExternalKmsForm } from "./AddExternalKmsForm";

View File

@@ -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<typeof formSchema>;
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<TForm>({
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) => {
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
onValueChange={(e) => {
setValue("credential.data.accessKey", "");
setValue("credential.data.secretKey", "");
setValue("credential.data.assumeRoleArn", "");
setValue("credential.data.externalId", "");
onChange(e);
}}
className="w-full"
>
<SelectItem value={KmsAwsCredentialType.AssumeRole}>AWS Assume Role</SelectItem>

View File

@@ -1,17 +1,31 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { faEllipsis, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, UpgradePlanModal } from "@app/components/v2";
import { createNotification } from "@app/components/notifications";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useGetExternalKmsList, useRemoveExternalKms } from "@app/hooks/api";
import { AddExternalKmsForm } from "./AddExternalKmsForm";
import { UpdateExternalKmsForm } from "./UpdateExternalKmsForm";
export const OrgEncryptionTab = withPermission(
() => {
@@ -19,17 +33,41 @@ export const OrgEncryptionTab = withPermission(
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan",
"addExternalKMS"
"addExternalKms",
"editExternalKms",
"removeExternalKms"
] as const);
const { subscription } = useSubscription();
const { data: externalKmsList, isLoading: isExternalKmsListLoading } =
useGetExternalKmsList(orgId);
const { mutateAsync: removeExternalKms } = useRemoveExternalKms(currentOrg?.id!);
const handleRemoveExternalKms = async () => {
const { kmsId } = popUp?.removeExternalKms?.data as {
kmsId: string;
};
try {
await removeExternalKms(kmsId);
createNotification({
text: "Successfully deleted external KMS",
type: "success"
});
handlePopUpToggle("removeExternalKms", false);
} catch (err) {
console.error(err);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Encryption</p>
<p className="text-xl font-semibold text-mineshaft-100">Key Management System (KMS)</p>
<Button
onClick={() => {
handlePopUpOpen("addExternalKMS");
handlePopUpOpen("addExternalKms");
// if (subscription && !subscription?.auditLogStreams) {
// handlePopUpOpen("upgradePlan");
// return;
@@ -42,16 +80,92 @@ export const OrgEncryptionTab = withPermission(
</Button>
</div>
<p className="mb-4 text-gray-400">
Configure the Key Management System used for encrypting/decrypting your data at rest
Integrate with external KMS systems for encrypting your organization&apos;s data
</p>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>Provider</Td>
<Td>Alias</Td>
</Tr>
</THead>
<TBody>
{isExternalKmsListLoading && <TableSkeleton columns={2} innerKey="kms-loading" />}
{!isExternalKmsListLoading && externalKmsList && externalKmsList?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No external KMS found" icon={faLock} />
</Td>
</Tr>
)}
{!isExternalKmsListLoading &&
externalKmsList?.map((kms) => (
<Tr key={kms.id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{kms.externalKms.provider}
</Td>
<Td>{kms.slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("editExternalKms", {
kmsId: kms.id
});
}}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeExternalKms", {
slug: kms.slug,
kmsId: kms.id,
provider: kms.externalKms.provider
});
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
text="You can configure external KMS if you switch to Infisical's Enterprise plan."
/>
<AddExternalKmsForm
isOpen={popUp.addExternalKMS.isOpen}
onToggle={(state) => handlePopUpToggle("addExternalKMS", state)}
isOpen={popUp.addExternalKms.isOpen}
onToggle={(state) => handlePopUpToggle("addExternalKms", state)}
/>
<UpdateExternalKmsForm
isOpen={popUp.editExternalKms.isOpen}
kmsId={(popUp.editExternalKms.data as { kmsId: string })?.kmsId}
onOpenChange={(state) => handlePopUpToggle("editExternalKms", state)}
/>
<DeleteActionModal
isOpen={popUp.removeExternalKms.isOpen}
title={`Are you sure want to remove ${
(popUp?.removeExternalKms?.data as { slug: string })?.slug || ""
} from ${(popUp?.removeExternalKms?.data as { provider: string })?.provider || ""}?`}
onChange={(isOpen) => handlePopUpToggle("removeExternalKms", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveExternalKms}
/>
</div>
);

View File

@@ -0,0 +1,29 @@
import { ContentLoader, Modal, ModalContent } from "@app/components/v2";
import { useGetExternalKmsById } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
type Props = {
isOpen: boolean;
kmsId: string;
onOpenChange: (state: boolean) => void;
};
export const UpdateExternalKmsForm = ({ isOpen, kmsId, onOpenChange }: Props) => {
const { data: externalKms, isLoading } = useGetExternalKmsById(kmsId);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Edit configuration">
{isLoading && <ContentLoader />}
{externalKms?.external?.provider === ExternalKmsProvider.AWS && (
<AwsKmsForm
kms={externalKms}
onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)}
/>
)}
</ModalContent>
</Modal>
);
};