Merge pull request #3079 from Infisical/feat/kmip-client-management

feat: kmip
This commit is contained in:
Sheen
2025-02-20 17:53:15 +08:00
committed by GitHub
84 changed files with 5002 additions and 71 deletions
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -1,5 +1,5 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
export type MenuProps = {
@@ -16,6 +16,7 @@ export type MenuItemProps<T extends ElementType> = {
as?: T;
children: ReactNode;
icon?: string;
iconMode?: Mode;
description?: ReactNode;
isDisabled?: boolean;
isSelected?: boolean;
@@ -26,6 +27,7 @@ export type MenuItemProps<T extends ElementType> = {
export const MenuItem = <T extends ElementType = "button">({
children,
icon,
iconMode,
className,
isDisabled,
isSelected,
@@ -62,6 +64,7 @@ export const MenuItem = <T extends ElementType = "button">({
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
mode={iconMode}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
@@ -24,7 +24,8 @@ export enum OrgPermissionSubjects {
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
AppConnections = "app-connections",
Kmip = "kmip"
}
export enum OrgPermissionAdminConsoleAction {
@@ -39,6 +40,11 @@ export enum OrgPermissionAppConnectionActions {
Connect = "connect"
}
export enum OrgPermissionKmipActions {
Proxy = "proxy",
Setup = "setup"
}
export type AppConnectionSubjectFields = {
connectionId: string;
};
@@ -61,7 +67,8 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections];
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
// TODO(scott): add back once org UI refactored
// | [
// OrgPermissionAppConnectionActions,
@@ -4,5 +4,6 @@ export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSub
} from "./types";
@@ -24,6 +24,14 @@ export enum ProjectPermissionCmekActions {
Decrypt = "decrypt"
}
export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
DeleteClients = "delete-clients",
ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
}
export enum ProjectPermissionSecretSyncActions {
Read = "read",
Create = "create",
@@ -102,7 +110,8 @@ export enum ProjectPermissionSub {
PkiCollections = "pki-collections",
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs"
SecretSyncs = "secret-syncs",
Kmip = "kmip"
}
export type SecretSubjectFields = {
@@ -190,5 +199,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
+1
View File
@@ -10,6 +10,7 @@ export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";
+6
View File
@@ -0,0 +1,6 @@
import FileSaver from "file-saver";
export const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
+18 -1
View File
@@ -121,7 +121,24 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
"OIDC group membership mapping assigned user to groups",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
"OIDC group membership mapping removed user from groups"
"OIDC group membership mapping removed user from groups",
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",
[EventType.GET_KMIP_CLIENT]: "Get KMIP client",
[EventType.GET_KMIP_CLIENTS]: "Get KMIP clients",
[EventType.CREATE_KMIP_CLIENT_CERTIFICATE]: "Create KMIP client certificate",
[EventType.SETUP_KMIP]: "Setup KMIP configuration",
[EventType.GET_KMIP]: "Get KMIP configuration",
[EventType.REGISTER_KMIP_SERVER]: "Register KMIP server",
[EventType.KMIP_OPERATION_CREATE]: "KMIP operation create",
[EventType.KMIP_OPERATION_GET]: "KMIP operation get",
[EventType.KMIP_OPERATION_DESTROY]: "KMIP operation destroy",
[EventType.KMIP_OPERATION_GET_ATTRIBUTES]: "KMIP operation get attributes",
[EventType.KMIP_OPERATION_ACTIVATE]: "KMIP operation activate",
[EventType.KMIP_OPERATION_REVOKE]: "KMIP operation revoke",
[EventType.KMIP_OPERATION_LOCATE]: "KMIP operation locate",
[EventType.KMIP_OPERATION_REGISTER]: "KMIP operation register"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+19 -1
View File
@@ -1,5 +1,6 @@
export enum ActorType {
PLATFORM = "platform",
KMIP_CLIENT = "kmipClient",
USER = "user",
SERVICE = "service",
IDENTITY = "identity",
@@ -132,5 +133,22 @@ export enum EventType {
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client",
UPDATE_KMIP_CLIENT = "update-kmip-client",
DELETE_KMIP_CLIENT = "delete-kmip-client",
GET_KMIP_CLIENT = "get-kmip-client",
GET_KMIP_CLIENTS = "get-kmip-clients",
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate",
SETUP_KMIP = "setup-kmip",
GET_KMIP = "get-kmip",
REGISTER_KMIP_SERVER = "register-kmip-server",
KMIP_OPERATION_CREATE = "kmip-operation-create",
KMIP_OPERATION_GET = "kmip-operation-get",
KMIP_OPERATION_DESTROY = "kmip-operation-destroy",
KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes",
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register"
}
+16 -1
View File
@@ -30,6 +30,10 @@ interface IdentityActorMetadata {
identityId: string;
name: string;
}
interface KmipClientActorMetadata {
clientId: string;
name: string;
}
interface UserActor {
type: ActorType.USER;
@@ -51,11 +55,22 @@ export interface PlatformActor {
metadata: object;
}
export interface KmipClientActor {
type: ActorType.KMIP_CLIENT;
metadata: KmipClientActorMetadata;
}
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor | UnknownUserActor;
export type Actor =
| UserActor
| ServiceActor
| IdentityActor
| PlatformActor
| UnknownUserActor
| KmipClientActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
+2
View File
@@ -0,0 +1,2 @@
export * from "./mutation";
export * from "./queries";
+90
View File
@@ -0,0 +1,90 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { kmipKeys } from "./queries";
import {
KmipClientCertificate,
TCreateKmipClient,
TDeleteKmipClient,
TGenerateKmipClientCertificate,
TSetupOrgKmipDTO,
TUpdateKmipClient
} from "./types";
export const useCreateKmipClient = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TCreateKmipClient) => {
const { data } = await apiRequest.post("/api/v1/kmip/clients", payload);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
});
}
});
};
export const useUpdateKmipClient = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, name, description, permissions }: TUpdateKmipClient) => {
const { data } = await apiRequest.patch(`/api/v1/kmip/clients/${id}`, {
name,
description,
permissions
});
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
});
}
});
};
export const useDeleteKmipClients = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id }: TDeleteKmipClient) => {
const { data } = await apiRequest.delete(`/api/v1/kmip/clients/${id}`);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
});
}
});
};
export const useGenerateKmipClientCertificate = () => {
return useMutation({
mutationFn: async (payload: TGenerateKmipClientCertificate) => {
const { data } = await apiRequest.post<KmipClientCertificate>(
`/api/v1/kmip/clients/${payload.clientId}/certificates`,
payload
);
return data;
}
});
};
export const useSetupOrgKmip = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TSetupOrgKmipDTO) => {
await apiRequest.post("/api/v1/kmip", payload);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: kmipKeys.getOrgKmip(orgId) });
}
});
};
+69
View File
@@ -0,0 +1,69 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "../generic/types";
import {
KmipClientOrderBy,
OrgKmipConfig,
TListProjectKmipClientsDTO,
TProjectKmipClientList
} from "./types";
export const kmipKeys = {
getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) =>
[projectId, filters] as const,
getOrgKmip: (orgId: string) => [{ orgId }, "org-kmip-config"] as const
};
export const useGetKmipClientsByProjectId = (
{
projectId,
offset = 0,
limit = 100,
orderBy = KmipClientOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListProjectKmipClientsDTO,
options?: Omit<
UseQueryOptions<
TProjectKmipClientList,
unknown,
TProjectKmipClientList,
ReturnType<typeof kmipKeys.getKmipClientsByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: kmipKeys.getKmipClientsByProjectId({
projectId,
offset,
limit,
orderBy,
orderDirection,
search
}),
queryFn: async () => {
const { data } = await apiRequest.get<TProjectKmipClientList>("/api/v1/kmip/clients", {
params: { projectId, offset, limit, search, orderBy, orderDirection }
});
return data;
},
enabled: Boolean(projectId) && (options?.enabled ?? true),
placeholderData: (previousData) => previousData,
...options
});
};
export const useGetOrgKmipConfig = (orgId: string) => {
return useQuery({
queryKey: kmipKeys.getOrgKmip(orgId),
queryFn: async () => {
const { data } = await apiRequest.get<OrgKmipConfig>("/api/v1/kmip");
return data;
}
});
};
+81
View File
@@ -0,0 +1,81 @@
import { CertKeyAlgorithm } from "../certificates/enums";
import { OrderByDirection } from "../generic/types";
export enum KmipPermission {
Create = "create",
Locate = "locate",
Check = "check",
Get = "get",
GetAttributes = "get-attributes",
Activate = "activate",
Revoke = "revoke",
Destroy = "destroy",
Register = "register"
}
export type TKmipClient = {
id: string;
name: string;
description?: string;
permissions: KmipPermission[];
projectId: string;
};
type ProjectRef = { projectId: string };
type KeyRef = { id: string };
export type TCreateKmipClient = Pick<TKmipClient, "name" | "description" | "permissions"> &
ProjectRef;
export type TUpdateKmipClient = KeyRef &
Partial<Pick<TKmipClient, "name" | "description" | "permissions">> &
ProjectRef;
export type TProjectKmipClientList = {
kmipClients: TKmipClient[];
totalCount: number;
};
export type TGenerateKmipClientCertificate = {
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
clientId: string;
};
export type KmipClientCertificate = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
export type TDeleteKmipClient = KeyRef & ProjectRef;
export type TListProjectKmipClientsDTO = {
projectId: string;
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export enum KmipClientOrderBy {
Name = "name"
}
export type OrgKmipConfig = {
serverCertificateChain: string;
clientCertificateChain: string;
};
export type TSetupOrgKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
};
export type OrgKmipServerCert = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
@@ -46,4 +46,5 @@ export type SubscriptionPlan = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: boolean;
kmip: boolean;
};
@@ -105,6 +105,20 @@ export const ProjectLayout = () => {
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/kmip` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="key-user" iconMode="reverse">
KMIP
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
@@ -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>
@@ -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()
});
@@ -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>
)}
</>
);
};
@@ -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>;
@@ -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);
@@ -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
}
]
}
};
+27
View File
@@ -83,6 +83,7 @@ import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/se
import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route'
import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/route'
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
import { Route as certManagerSettingsPageRouteImport } from './pages/cert-manager/SettingsPage/route'
import { Route as certManagerCertificatesPageRouteImport } from './pages/cert-manager/CertificatesPage/route'
import { Route as projectRoleDetailsBySlugPageRouteSshImport } from './pages/project/RoleDetailsBySlugPage/route-ssh'
@@ -802,6 +803,12 @@ const kmsOverviewPageRouteRoute = kmsOverviewPageRouteImport.update({
getParentRoute: () => kmsLayoutRoute,
} as any)
const kmsKmipPageRouteRoute = kmsKmipPageRouteImport.update({
id: '/kmip',
path: '/kmip',
getParentRoute: () => kmsLayoutRoute,
} as any)
const certManagerSettingsPageRouteRoute =
certManagerSettingsPageRouteImport.update({
id: '/settings',
@@ -1998,6 +2005,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof certManagerSettingsPageRouteImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': {
id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip'
path: '/kmip'
fullPath: '/kms/$projectId/kmip'
preLoaderRoute: typeof kmsKmipPageRouteImport
parentRoute: typeof kmsLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview'
path: '/overview'
@@ -2987,6 +3001,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
)
interface kmsLayoutRouteChildren {
kmsKmipPageRouteRoute: typeof kmsKmipPageRouteRoute
kmsOverviewPageRouteRoute: typeof kmsOverviewPageRouteRoute
kmsSettingsPageRouteRoute: typeof kmsSettingsPageRouteRoute
projectAccessControlPageRouteKmsRoute: typeof projectAccessControlPageRouteKmsRoute
@@ -2996,6 +3011,7 @@ interface kmsLayoutRouteChildren {
}
const kmsLayoutRouteChildren: kmsLayoutRouteChildren = {
kmsKmipPageRouteRoute: kmsKmipPageRouteRoute,
kmsOverviewPageRouteRoute: kmsOverviewPageRouteRoute,
kmsSettingsPageRouteRoute: kmsSettingsPageRouteRoute,
projectAccessControlPageRouteKmsRoute: projectAccessControlPageRouteKmsRoute,
@@ -3605,6 +3621,7 @@ export interface FileRoutesByFullPath {
'/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
'/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
'/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
@@ -3773,6 +3790,7 @@ export interface FileRoutesByTo {
'/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
'/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
'/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
@@ -3955,6 +3973,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
@@ -4130,6 +4149,7 @@ export interface FileRouteTypes {
| '/organization/ssh/overview'
| '/cert-manager/$projectId/overview'
| '/cert-manager/$projectId/settings'
| '/kms/$projectId/kmip'
| '/kms/$projectId/overview'
| '/kms/$projectId/settings'
| '/secret-manager/$projectId/allowlist'
@@ -4297,6 +4317,7 @@ export interface FileRouteTypes {
| '/organization/ssh/overview'
| '/cert-manager/$projectId/overview'
| '/cert-manager/$projectId/settings'
| '/kms/$projectId/kmip'
| '/kms/$projectId/overview'
| '/kms/$projectId/settings'
| '/secret-manager/$projectId/allowlist'
@@ -4477,6 +4498,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist'
@@ -4965,6 +4987,7 @@ export const routeTree = rootRoute
"filePath": "kms/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId",
"children": [
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip",
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview",
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/access-management",
@@ -5011,6 +5034,10 @@ export const routeTree = rootRoute
"filePath": "cert-manager/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip": {
"filePath": "kms/KmipPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
},
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview": {
"filePath": "kms/OverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
+1
View File
@@ -291,6 +291,7 @@ const certManagerRoutes = route("/cert-manager/$projectId", [
const kmsRoutes = route("/kms/$projectId", [
layout("kms-layout", "kms/layout.tsx", [
route("/overview", "kms/OverviewPage/route.tsx"),
route("/kmip", "kms/KmipPage/route.tsx"),
route("/settings", "kms/SettingsPage/route.tsx"),
route("/access-management", "project/AccessControlPage/route-kms.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-kms.tsx"),