mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 06:35:21 +00:00
Merge pull request #3079 from Infisical/feat/kmip-client-management
feat: kmip
This commit is contained in:
@@ -40,7 +40,8 @@ enum TabSections {
|
||||
Auth = "auth",
|
||||
RateLimit = "rate-limit",
|
||||
Integrations = "integrations",
|
||||
Users = "users"
|
||||
Users = "users",
|
||||
Kmip = "kmip"
|
||||
}
|
||||
|
||||
enum SignUpModes {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import { KmipClientTable } from "./components/KmipClientTable";
|
||||
|
||||
export const KmipPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "KMS" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="KMIP"
|
||||
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
passThrough={false}
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionKmipActions.ReadClients}
|
||||
a={ProjectPermissionSub.Kmip}
|
||||
>
|
||||
<KmipClientTable />
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||
import { useGenerateKmipClientCertificate } from "@app/hooks/api/kmip";
|
||||
import { KmipClientCertificate, TKmipClient } from "@app/hooks/api/kmip/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
|
||||
ttl: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
kmipClient?: TKmipClient | null;
|
||||
displayNewClientCertificate: (certificate: KmipClientCertificate) => void;
|
||||
};
|
||||
|
||||
type FormProps = Pick<Props, "kmipClient" | "displayNewClientCertificate"> & {
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const KmipClientCertificateForm = ({
|
||||
displayNewClientCertificate,
|
||||
kmipClient,
|
||||
onComplete
|
||||
}: FormProps) => {
|
||||
const { mutateAsync: createKmipClientCertificate } = useGenerateKmipClientCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const handleKmipClientSubmit = async (payload: FormData) => {
|
||||
if (!kmipClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const certificate = await createKmipClientCertificate({
|
||||
...payload,
|
||||
clientId: kmipClient?.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created KMIP client certificate",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
displayNewClientCertificate(certificate);
|
||||
onComplete();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="TTL" isError={Boolean(error)} errorText={error?.message} isRequired>
|
||||
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
defaultValue={CertKeyAlgorithm.RSA_2048}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Key Algorithm"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
helperText="This defines the key algorithm to use for signing the client certificate."
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{certKeyAlgorithms.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Generate Client Certificate
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateKmipClientCertificateModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
kmipClient,
|
||||
displayNewClientCertificate
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent title="Generate KMIP client certificate">
|
||||
<KmipClientCertificateForm
|
||||
onComplete={() => onOpenChange(false)}
|
||||
displayNewClientCertificate={displayNewClientCertificate}
|
||||
kmipClient={kmipClient}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { useDeleteKmipClients } from "@app/hooks/api/kmip";
|
||||
import { TKmipClient } from "@app/hooks/api/kmip/types";
|
||||
|
||||
type Props = {
|
||||
kmipClient: TKmipClient;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
|
||||
const deleteKmipClients = useDeleteKmipClients();
|
||||
|
||||
if (!kmipClient) return null;
|
||||
|
||||
const { id, projectId, name } = kmipClient;
|
||||
|
||||
const handleDeleteKmipClient = async () => {
|
||||
try {
|
||||
await deleteKmipClients.mutateAsync({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "KMIP client successfully deleted",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to delete KMIP client";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
onChange={onOpenChange}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteKmipClient}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { KmipClientCertificate } from "@app/hooks/api/kmip/types";
|
||||
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
certificate: KmipClientCertificate;
|
||||
};
|
||||
|
||||
export const KmipClientCertificateModal = ({ isOpen, onOpenChange, certificate }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent title="KMIP client certificate">
|
||||
<CertificateContent {...certificate} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip";
|
||||
import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types";
|
||||
|
||||
const KMIP_PERMISSIONS_OPTIONS = [
|
||||
{ value: KmipPermission.Check, label: "Check" },
|
||||
{ value: KmipPermission.Create, label: "Create" },
|
||||
{ value: KmipPermission.Get, label: "Get" },
|
||||
{ value: KmipPermission.Locate, label: "Locate" },
|
||||
{ value: KmipPermission.Destroy, label: "Destroy" },
|
||||
{ value: KmipPermission.Activate, label: "Activate" },
|
||||
{ value: KmipPermission.Revoke, label: "Revoke" },
|
||||
{ value: KmipPermission.GetAttributes, label: "Get Attributes" },
|
||||
{ value: KmipPermission.Register, label: "Register" }
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
description: z.string().max(500).optional(),
|
||||
permissions: z.object({
|
||||
[KmipPermission.Check]: z.boolean().optional(),
|
||||
[KmipPermission.Create]: z.boolean().optional(),
|
||||
[KmipPermission.Get]: z.boolean().optional(),
|
||||
[KmipPermission.Locate]: z.boolean().optional(),
|
||||
[KmipPermission.Destroy]: z.boolean().optional(),
|
||||
[KmipPermission.Activate]: z.boolean().optional(),
|
||||
[KmipPermission.GetAttributes]: z.boolean().optional(),
|
||||
[KmipPermission.Revoke]: z.boolean().optional(),
|
||||
[KmipPermission.Register]: z.boolean().optional()
|
||||
})
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
kmipClient?: TKmipClient | null;
|
||||
};
|
||||
|
||||
type FormProps = Pick<Props, "kmipClient"> & {
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
|
||||
const createKmipClient = useCreateKmipClient();
|
||||
const updateKmipClient = useUpdateKmipClient();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const isUpdate = !!kmipClient;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: kmipClient?.name,
|
||||
description: kmipClient?.description,
|
||||
permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true]))
|
||||
}
|
||||
});
|
||||
|
||||
const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => {
|
||||
const mutation = isUpdate
|
||||
? updateKmipClient.mutateAsync({
|
||||
id: kmipClient.id,
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
permissions: Object.entries(permissions)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as KmipPermission)
|
||||
})
|
||||
: createKmipClient.mutateAsync({
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
permissions: Object.entries(permissions)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as KmipPermission)
|
||||
});
|
||||
|
||||
try {
|
||||
await mutation;
|
||||
createNotification({
|
||||
text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
|
||||
<FormControl
|
||||
errorText={errors.name?.message}
|
||||
isError={Boolean(errors.name?.message)}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus placeholder="My KMIP Client" {...register("name")} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Description (optional)"
|
||||
errorText={errors.description?.message}
|
||||
isError={Boolean(errors.description?.message)}
|
||||
>
|
||||
<TextArea
|
||||
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||
{...register("description")}
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="permissions"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl label="Permissions" errorText={error?.message} isError={Boolean(error)}>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{KMIP_PERMISSIONS_OPTIONS.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Add"} KMIP client
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const KmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent title={`${kmipClient ? "Update" : "Add"} KMIP Client`}>
|
||||
<KmipClientForm onComplete={() => onOpenChange(false)} kmipClient={kmipClient} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,338 @@
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faCertificate,
|
||||
faEdit,
|
||||
faEllipsis,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faTrash,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip";
|
||||
import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types";
|
||||
|
||||
import { CreateKmipClientCertificateModal } from "./CreateKmipClientCertificateModal";
|
||||
import { DeleteKmipClientModal } from "./DeleteKmipClientModal";
|
||||
import { KmipClientCertificateModal } from "./KmipClientCertificateModal";
|
||||
import { KmipClientModal } from "./KmipClientModal";
|
||||
|
||||
export const KmipClientTable = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const projectId = currentWorkspace?.id ?? "";
|
||||
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
search,
|
||||
debouncedSearch,
|
||||
setPage,
|
||||
setSearch,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage
|
||||
} = usePagination(KmipClientOrderBy.Name);
|
||||
|
||||
const { data, isPending, isFetching } = useGetKmipClientsByProjectId({
|
||||
projectId,
|
||||
offset,
|
||||
limit,
|
||||
search: debouncedSearch,
|
||||
orderBy,
|
||||
orderDirection
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { kmipClients = [], totalCount = 0 } = data ?? {};
|
||||
useResetPageHelper({
|
||||
totalCount,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"upsertKmipClient",
|
||||
"deleteKmipClient",
|
||||
"generateKmipClientCert",
|
||||
"displayKmipClientCert",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const handleSort = () => {
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||
);
|
||||
};
|
||||
|
||||
const cannotEditKmipClient = permission.cannot(
|
||||
ProjectPermissionKmipActions.UpdateClients,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
const cannotDeleteKmipClient = permission.cannot(
|
||||
ProjectPermissionKmipActions.DeleteClients,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
const cannotGenerateKmipClientCertificate = permission.cannot(
|
||||
ProjectPermissionKmipActions.GenerateClientCertificates,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key="kmip-clients-tab"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="whitespace-nowrap text-xl font-semibold text-mineshaft-100">KMIP Clients</p>
|
||||
<div className="flex w-full justify-end pr-4">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/kms"
|
||||
>
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionKmipActions.CreateClients}
|
||||
a={ProjectPermissionSub.Kmip}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (subscription && !subscription.kmip) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upsertKmipClient", null);
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add KMIP Client
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<Input
|
||||
containerClassName="mb-4"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search clients by name..."
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="h-14">
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={handleSort}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Description</Th>
|
||||
<Th>Permissions</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="project-kmip-clients" />}
|
||||
{!isPending &&
|
||||
kmipClients.length > 0 &&
|
||||
kmipClients.map((kmipClient) => {
|
||||
const { name, id, description, permissions } = kmipClient;
|
||||
|
||||
return (
|
||||
<Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td className="max-w-80 break-all">{description}</Td>
|
||||
<Td className="max-w-40">{[permissions.join(", ")]}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="primary"
|
||||
className="ml-4 p-0 data-[state=open]:text-primary-400"
|
||||
ariaLabel="More options"
|
||||
>
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[160px]">
|
||||
<Tooltip
|
||||
content={
|
||||
cannotGenerateKmipClientCertificate ? "Access Restricted" : ""
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handlePopUpOpen("generateKmipClientCert", kmipClient)
|
||||
}
|
||||
icon={<FontAwesomeIcon icon={faCertificate} />}
|
||||
iconPos="left"
|
||||
isDisabled={cannotGenerateKmipClientCertificate}
|
||||
>
|
||||
Generate Certificate
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={cannotEditKmipClient ? "Access Restricted" : ""}
|
||||
position="left"
|
||||
>
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePopUpOpen("upsertKmipClient", kmipClient)}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
iconPos="left"
|
||||
isDisabled={cannotEditKmipClient}
|
||||
>
|
||||
Edit KMIP Client
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={cannotDeleteKmipClient ? "Access Restricted" : ""}
|
||||
position="left"
|
||||
>
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePopUpOpen("deleteKmipClient", kmipClient)}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
iconPos="left"
|
||||
isDisabled={cannotDeleteKmipClient}
|
||||
>
|
||||
Delete KMIP Client
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isPending && kmipClients.length === 0 && (
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0
|
||||
? "No KMIP clients match search filter"
|
||||
: "No KMIP clients have been added to this project"
|
||||
}
|
||||
icon={faUser}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<DeleteKmipClientModal
|
||||
isOpen={popUp.deleteKmipClient.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteKmipClient", isOpen)}
|
||||
kmipClient={popUp.deleteKmipClient.data as TKmipClient}
|
||||
/>
|
||||
<KmipClientModal
|
||||
isOpen={popUp.upsertKmipClient.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)}
|
||||
kmipClient={popUp.upsertKmipClient.data as TKmipClient | null}
|
||||
/>
|
||||
<CreateKmipClientCertificateModal
|
||||
isOpen={popUp.generateKmipClientCert.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("generateKmipClientCert", isOpen)}
|
||||
kmipClient={popUp.generateKmipClientCert.data as TKmipClient | null}
|
||||
displayNewClientCertificate={(certificate) =>
|
||||
handlePopUpOpen("displayKmipClientCert", certificate)
|
||||
}
|
||||
/>
|
||||
<KmipClientCertificateModal
|
||||
isOpen={popUp.displayKmipClientCert.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("displayKmipClientCert", isOpen)}
|
||||
certificate={popUp.displayKmipClientCert.data}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="KMIP requires an enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { KmipPage } from "./KmipPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip"
|
||||
)({
|
||||
component: KmipPage
|
||||
});
|
||||
@@ -94,6 +94,15 @@ export const LogsFilter = ({
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
case ActorType.KMIP_CLIENT:
|
||||
return (
|
||||
<SelectItem
|
||||
value={`${actor.type}-${actor.metadata.clientId}`}
|
||||
key={`kmip-client-filter-${actor.metadata.clientId}`}
|
||||
>
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<SelectItem value="actor-none" key="actor-none">
|
||||
|
||||
@@ -46,6 +46,13 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
<p>Platform</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.KMIP_CLIENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.name}</p>
|
||||
<p>KMIP Client</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.UNKNOWN_USER:
|
||||
return (
|
||||
<Td>
|
||||
|
||||
+13
-2
@@ -2,7 +2,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import {
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionKmipActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { TPermission } from "@app/hooks/api/roles/types";
|
||||
|
||||
const generalPermissionSchema = z
|
||||
@@ -24,6 +27,13 @@ const appConnectionsPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const kmipPermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionKmipActions.Proxy]: z.boolean().optional(),
|
||||
[OrgPermissionKmipActions.Setup]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const adminConsolePermissionSchmea = z
|
||||
.object({
|
||||
"access-all-projects": z.boolean().optional()
|
||||
@@ -61,7 +71,8 @@ export const formSchema = z.object({
|
||||
"organization-admin-console": adminConsolePermissionSchmea,
|
||||
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
|
||||
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
|
||||
"app-connections": appConnectionsPermissionSchema
|
||||
"app-connections": appConnectionsPermissionSchema,
|
||||
kmip: kmipPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{ action: "proxy", label: "Proxy KMIP requests" },
|
||||
{ action: "setup", label: "Setup KMIP" }
|
||||
] as const;
|
||||
|
||||
export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.kmip"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
if (rule?.proxy || rule?.setup) {
|
||||
return Permission.Custom;
|
||||
}
|
||||
return Permission.NoAccess;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
if (val === Permission.NoAccess) {
|
||||
setValue("permissions.kmip", { proxy: false, setup: false }, { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td>
|
||||
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td>KMIP</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td
|
||||
colSpan={3}
|
||||
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.kmip.${action}`}
|
||||
key={`permissions.kmip.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.kmip.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -74,7 +74,7 @@ type Props = {
|
||||
title: string;
|
||||
formName: keyof Omit<
|
||||
Exclude<TFormSchema["permissions"], undefined>,
|
||||
"workspace" | "organization-admin-console"
|
||||
"workspace" | "organization-admin-console" | "kmip"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
|
||||
+6
@@ -14,6 +14,7 @@ import {
|
||||
TFormSchema
|
||||
} from "../OrgRoleModifySection.utils";
|
||||
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
|
||||
@@ -180,6 +181,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionKmipRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Spinner,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { downloadTxtFile } from "@app/helpers/download";
|
||||
import { usePopUp, useTimedReset } from "@app/hooks";
|
||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||
import { useGetOrgKmipConfig, useSetupOrgKmip } from "@app/hooks/api/kmip";
|
||||
import { OrgKmipConfig } from "@app/hooks/api/kmip/types";
|
||||
|
||||
const orgConfigFormSchema = z.object({
|
||||
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||
});
|
||||
|
||||
type TKmipOrgConfigForm = z.infer<typeof orgConfigFormSchema>;
|
||||
|
||||
const OrgConfigSection = ({
|
||||
kmipConfig,
|
||||
isKmipConfigLoading
|
||||
}: {
|
||||
kmipConfig?: OrgKmipConfig;
|
||||
isKmipConfigLoading: boolean;
|
||||
}) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"configureKmip",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TKmipOrgConfigForm>({
|
||||
resolver: zodResolver(orgConfigFormSchema)
|
||||
});
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync: setupOrgKmip } = useSetupOrgKmip(currentOrg.id);
|
||||
|
||||
const onFormSubmit = async (formData: TKmipOrgConfigForm) => {
|
||||
await setupOrgKmip(formData);
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully configured KMIP"
|
||||
});
|
||||
|
||||
handlePopUpClose("configureKmip");
|
||||
};
|
||||
const [copyTextClientCertificate, isCopyingClientCertificate, setCopyTextClientCertificate] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
const [copyTextServerCertificate, isCopyingServerCertificate, setCopyTextServerCertificate] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="mb-2 text-xl font-semibold text-mineshaft-100">KMIP configuration</div>
|
||||
{isKmipConfigLoading && (
|
||||
<div className="mt-8 flex justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
{!isKmipConfigLoading && kmipConfig && (
|
||||
<>
|
||||
<div className="mt-2">
|
||||
<div className="text-lg">Certificate Chain for KMIP Clients</div>
|
||||
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
|
||||
This certificate chain is used by KMIP clients to verify the identity of the KMIP
|
||||
server. It should be presented by the server during TLS authentication to establish
|
||||
a secure and encrypted connection.
|
||||
</div>
|
||||
<div className="flex max-w-2xl">
|
||||
<div className="flex w-full justify-end">
|
||||
<Tooltip content={copyTextClientCertificate}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(kmipConfig.serverCertificateChain);
|
||||
setCopyTextClientCertificate("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingClientCertificate ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Download">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("ca-chain.pem", kmipConfig.serverCertificateChain);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<TextArea
|
||||
value={kmipConfig.serverCertificateChain}
|
||||
reSize="none"
|
||||
className="mt-2 h-48 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<div className="text-lg">Certificate Chain for KMIP Server</div>
|
||||
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
|
||||
This certificate chain is used by the KMIP server to verify the identity of KMIP
|
||||
clients. It should be configured on the server to establish trust in client
|
||||
certificates during mutual TLS authentication.
|
||||
</div>
|
||||
<div className="flex max-w-2xl">
|
||||
<div className="flex w-full justify-end">
|
||||
<Tooltip content={copyTextServerCertificate}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(kmipConfig.clientCertificateChain);
|
||||
setCopyTextServerCertificate("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingServerCertificate ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Download">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("ca-chain.pem", kmipConfig.clientCertificateChain);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<TextArea
|
||||
value={kmipConfig.clientCertificateChain}
|
||||
reSize="none"
|
||||
className="mt-2 h-48 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isKmipConfigLoading && !kmipConfig && (
|
||||
<div className="mt-2">
|
||||
<div>KMIP has not yet been configured for the organization.</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
onClick={() => {
|
||||
if (subscription && !subscription.kmip) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("configureKmip");
|
||||
}}
|
||||
>
|
||||
Setup KMIP
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp.configureKmip.isOpen}
|
||||
onOpenChange={(state) => handlePopUpToggle("configureKmip", state)}
|
||||
>
|
||||
<ModalContent title="Configure KMIP for the organization">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caKeyAlgorithm"
|
||||
defaultValue={CertKeyAlgorithm.RSA_2048}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Key Algorithm"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
helperText="This defines the key algorithm used for generating the KMIP Root CA and Intermediate CAs, which sign all KMIP server and client certificates."
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{certKeyAlgorithms.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-6 flex w-full gap-4">
|
||||
<Button
|
||||
className=""
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
className=""
|
||||
size="sm"
|
||||
variant="outline_bg"
|
||||
type="button"
|
||||
onClick={() => handlePopUpClose("configureKmip")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="KMIP requires an enterprise plan."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const KmipTab = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: kmipConfig, isPending } = useGetOrgKmipConfig(currentOrg.id);
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<OrgConfigSection kmipConfig={kmipConfig} isKmipConfigLoading={isPending} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
|
||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||
import { ImportTab } from "../ImportTab";
|
||||
import { KmipTab } from "../KmipTab/OrgKmipTab";
|
||||
import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
@@ -27,7 +28,8 @@ export const OrgTabGroup = () => {
|
||||
},
|
||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
||||
{ name: "Import", key: "tab-import", component: ImportTab },
|
||||
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
|
||||
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab },
|
||||
{ name: "KMIP", key: "kmip", component: KmipTab }
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||
|
||||
+58
-1
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
TPermissionCondition,
|
||||
TPermissionConditionOperators
|
||||
@@ -48,6 +49,14 @@ const SecretSyncPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const KmipPolicyActionSchema = z.object({
|
||||
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
|
||||
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
|
||||
[ProjectPermissionKmipActions.UpdateClients]: z.boolean().optional(),
|
||||
[ProjectPermissionKmipActions.DeleteClients]: z.boolean().optional(),
|
||||
[ProjectPermissionKmipActions.GenerateClientCertificates]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretRollbackPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
@@ -162,7 +171,8 @@ export const projectRoleFormSchema = z.object({
|
||||
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([])
|
||||
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([])
|
||||
})
|
||||
.partial()
|
||||
.optional()
|
||||
@@ -359,6 +369,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Kmip) {
|
||||
const canReadClients = action.includes(ProjectPermissionKmipActions.ReadClients);
|
||||
const canEditClients = action.includes(ProjectPermissionKmipActions.UpdateClients);
|
||||
const canDeleteClients = action.includes(ProjectPermissionKmipActions.DeleteClients);
|
||||
const canCreateClients = action.includes(ProjectPermissionKmipActions.CreateClients);
|
||||
const canGenerateClientCerts = action.includes(
|
||||
ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canReadClients) formVal[subject]![0][ProjectPermissionKmipActions.ReadClients] = true;
|
||||
if (canEditClients) formVal[subject]![0][ProjectPermissionKmipActions.UpdateClients] = true;
|
||||
if (canCreateClients) formVal[subject]![0][ProjectPermissionKmipActions.CreateClients] = true;
|
||||
if (canDeleteClients) formVal[subject]![0][ProjectPermissionKmipActions.DeleteClients] = true;
|
||||
if (canGenerateClientCerts)
|
||||
formVal[subject]![0][ProjectPermissionKmipActions.GenerateClientCertificates] = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretSyncs) {
|
||||
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
|
||||
@@ -738,5 +770,30 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
value: ProjectPermissionSecretSyncActions.RemoveSecrets
|
||||
}
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.Kmip]: {
|
||||
title: "KMIP",
|
||||
actions: [
|
||||
{
|
||||
label: "Read clients",
|
||||
value: ProjectPermissionKmipActions.ReadClients
|
||||
},
|
||||
{
|
||||
label: "Create clients",
|
||||
value: ProjectPermissionKmipActions.CreateClients
|
||||
},
|
||||
{
|
||||
label: "Modify clients",
|
||||
value: ProjectPermissionKmipActions.UpdateClients
|
||||
},
|
||||
{
|
||||
label: "Delete clients",
|
||||
value: ProjectPermissionKmipActions.DeleteClients
|
||||
},
|
||||
{
|
||||
label: "Generate client certificates",
|
||||
value: ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user