mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add issue SSH certificate modal
This commit is contained in:
@@ -2,3 +2,8 @@ export enum SshCaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled"
|
||||
}
|
||||
|
||||
export enum SshCertType {
|
||||
USER = "user",
|
||||
HOST = "host"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { SshCaStatus } from "./enums";
|
||||
export { useCreateSshCa, useDeleteSshCa,useUpdateSshCa } from "./mutations";
|
||||
export { useCreateSshCa, useDeleteSshCa, useIssueSshCreds,useUpdateSshCa } from "./mutations";
|
||||
export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
@@ -7,6 +6,8 @@ import { organizationKeys } from "../organization/queries";
|
||||
import {
|
||||
TCreateSshCaDTO,
|
||||
TDeleteSshCaDTO,
|
||||
TIssueSshCredsDTO,
|
||||
TIssueSshCredsResponse,
|
||||
TSshCertificateAuthority,
|
||||
TUpdateSshCaDTO} from "./types";
|
||||
|
||||
@@ -59,3 +60,12 @@ export const useDeleteSshCa = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useIssueSshCreds = () => {
|
||||
return useMutation<TIssueSshCredsResponse, {}, TIssueSshCredsDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TIssueSshCredsResponse>("/api/v1/ssh/issue", body);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||
import { SshCaStatus } from "./enums";
|
||||
import { SshCaStatus, SshCertType } from "./enums";
|
||||
|
||||
export type TSshCertificateAuthority = {
|
||||
id: string;
|
||||
@@ -26,3 +26,20 @@ export type TUpdateSshCaDTO = {
|
||||
export type TDeleteSshCaDTO = {
|
||||
caId: string;
|
||||
};
|
||||
|
||||
export type TIssueSshCredsDTO = {
|
||||
templateName: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
certType: SshCertType;
|
||||
principals: string[];
|
||||
ttl?: string;
|
||||
keyId?: string;
|
||||
};
|
||||
|
||||
export type TIssueSshCredsResponse = {
|
||||
serialNumber: string;
|
||||
signedKey: string;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -15,13 +15,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization
|
||||
} from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
signedKey: string;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
|
||||
export const SshCertificateContent = ({
|
||||
serialNumber,
|
||||
signedKey,
|
||||
privateKey,
|
||||
publicKey
|
||||
}: Props) => {
|
||||
const [copyTextSerialNumber, isCopyingSerialNumber, setCopyTextSerialNumber] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>(
|
||||
{
|
||||
initialState: "Copy to clipboard"
|
||||
}
|
||||
);
|
||||
|
||||
const [copyTextCertificateSk, isCopyingCertificateSk, setCopyTextCertificateSk] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
const [copyTextCertificatePk, isCopyingCertificatePk, setCopyTextCertificatePk] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
const downloadTxtFile = (filename: string, content: string) => {
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
FileSaver.saveAs(blob, filename);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{serialNumber}</p>
|
||||
<Tooltip content={copyTextSerialNumber}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(serialNumber);
|
||||
setCopyTextSerialNumber("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingSerialNumber ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>SSH Certificate / Signed Key</h2>
|
||||
<div className="flex">
|
||||
<Tooltip content={copyTextCertificate}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(signedKey);
|
||||
setCopyTextCertificate("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCertificate ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Download">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("user_key-cert.pub", signedKey);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{signedKey}</p>
|
||||
</div>
|
||||
{privateKey && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Private Key</h2>
|
||||
<div className="flex">
|
||||
<Tooltip content={copyTextCertificateSk}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setCopyTextCertificateSk("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCertificateSk ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={copyTextCertificateSk}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("user_key", privateKey);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{privateKey}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{publicKey && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Public Key</h2>
|
||||
<div className="flex">
|
||||
<Tooltip content={copyTextCertificatePk}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(publicKey);
|
||||
setCopyTextCertificatePk("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCertificatePk ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={copyTextCertificatePk}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("user_key.pub", publicKey);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{publicKey}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,274 @@
|
||||
import { useEffect, useState } from "react";
|
||||
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,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useGetSshCaCertTemplates,useIssueSshCreds } from "@app/hooks/api";
|
||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||
import { SshCertType } from "@app/hooks/api/ssh-ca/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SshCertificateContent } from "./SshCertificateContent";
|
||||
|
||||
/**
|
||||
* // NOTE (dangtony98): current UI only supports SSH certificate
|
||||
* issuance via /issue endpoint but should extend to also support
|
||||
* /sign endpoint as this is already supported in the backend
|
||||
*/
|
||||
|
||||
const schema = z.object({
|
||||
templateName: z.string(),
|
||||
keyAlgorithm: z.enum([
|
||||
CertKeyAlgorithm.RSA_2048,
|
||||
CertKeyAlgorithm.RSA_4096,
|
||||
CertKeyAlgorithm.ECDSA_P256,
|
||||
CertKeyAlgorithm.ECDSA_P384
|
||||
]),
|
||||
certType: z.nativeEnum(SshCertType),
|
||||
principals: z.string(),
|
||||
ttl: z.string().optional(),
|
||||
keyId: z.string().optional()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["sshCertificate"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCertificate"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
type TSshCertificateDetails = {
|
||||
serialNumber: string;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
signedKey: string;
|
||||
};
|
||||
|
||||
export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TSshCertificateDetails | null>(null);
|
||||
const { mutateAsync: issueSshCreds } = useIssueSshCreds();
|
||||
|
||||
const popUpData = popUp?.sshCertificate?.data as { sshCaId: string; templateName: string };
|
||||
|
||||
const { data: templatesData } = useGetSshCaCertTemplates(popUpData?.sshCaId || "");
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
certType: SshCertType.USER
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (popUpData) {
|
||||
setValue("templateName", popUpData.templateName);
|
||||
}
|
||||
}, [popUpData]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
templateName,
|
||||
keyAlgorithm,
|
||||
certType,
|
||||
principals,
|
||||
ttl,
|
||||
keyId
|
||||
}: FormData) => {
|
||||
try {
|
||||
const { serialNumber, publicKey, privateKey, signedKey } = await issueSshCreds({
|
||||
templateName,
|
||||
keyAlgorithm,
|
||||
certType,
|
||||
principals: principals.split(",").map((user) => user.trim()),
|
||||
ttl,
|
||||
keyId
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
privateKey,
|
||||
publicKey,
|
||||
signedKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created SSH certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create SSH certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.sshCertificate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("sshCertificate", isOpen);
|
||||
reset();
|
||||
setCertificateDetails(null);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Issue SSH Certificate">
|
||||
{!certificateDetails ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="templateName"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Template"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled
|
||||
>
|
||||
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||
<SelectItem value={name} key={`ssh-cert-template-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="principals"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Principal(s)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ec2-user" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="certType"
|
||||
defaultValue={SshCertType.USER}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Type"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={SshCertType.USER}>User</SelectItem>
|
||||
<SelectItem value={SshCertType.HOST}>Host</SelectItem>
|
||||
</Select>
|
||||
</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)}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="TTL" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Key ID" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="12345678" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("sshCertificate", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<SshCertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
signedKey={certificateDetails.signedKey}
|
||||
publicKey={certificateDetails.publicKey}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -4,10 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||
import { OrgPermissionSshCertificateTemplateActions,OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteSshCertTemplate } from "@app/hooks/api";
|
||||
|
||||
import { SshCertificateModal } from "./SshCertificateModal";
|
||||
import { SshCertificateTemplateModal } from "./SshCertificateTemplateModal";
|
||||
import { SshCertificateTemplatesTable } from "./SshCertificateTemplatesTable";
|
||||
|
||||
@@ -18,6 +19,7 @@ type Props = {
|
||||
export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"sshCertificateTemplate",
|
||||
"sshCertificate",
|
||||
"deleteSshCertificateTemplate",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
@@ -69,6 +71,7 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
||||
<div className="py-4">
|
||||
<SshCertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} sshCaId={caId} />
|
||||
</div>
|
||||
<SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<SshCertificateTemplateModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCertificate,faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionSshCertificateTemplateActions,OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useGetSshCaCertTemplates } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -27,11 +27,13 @@ type Props = {
|
||||
sshCaId: string;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["sshCertificateTemplate", "deleteSshCertificateTemplate", "upgradePlan"]
|
||||
["sshCertificateTemplate", "sshCertificate", "deleteSshCertificateTemplate", "upgradePlan"]
|
||||
>,
|
||||
data?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
sshCaId?: string;
|
||||
templateName?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
@@ -66,6 +68,24 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionSshCertificateTemplateActions.Edit}
|
||||
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handlePopUpOpen("sshCertificate", {
|
||||
sshCaId,
|
||||
templateName: certificateTemplate.name
|
||||
});
|
||||
}}
|
||||
icon={
|
||||
<FontAwesomeIcon icon={faCertificate} size="sm" className="mr-1" />
|
||||
}
|
||||
>
|
||||
Issue SSH Certificate
|
||||
</DropdownMenuItem>
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionSshCertificateTemplateActions.Edit}
|
||||
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { SshCaDetailsSection } from "./SshCaDetailsSection";
|
||||
export { SshCertificateModal } from "./SshCertificateModal";
|
||||
export { SshCertificateTemplatesSection } from "./SshCertificateTemplatesSection";
|
||||
|
||||
Reference in New Issue
Block a user