Compare commits

...

2 Commits

21 changed files with 533 additions and 318 deletions

View File

@ -368,7 +368,6 @@ export const certificateAuthorityServiceFactory = ({
);
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId,
@ -407,7 +406,8 @@ export const certificateAuthorityServiceFactory = ({
/**
* Renew certificate for CA with id [caId]
* Note: Currently implements CA renewal with same key-pair only
* Note 1: This CA renewal method is only applicable to CAs with internal parent CAs
* Note 2: Currently implements CA renewal with same key-pair only
*/
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
@ -888,9 +888,9 @@ export const certificateAuthorityServiceFactory = ({
};
/**
* Import certificate for (un-installed) CA with id [caId].
* Import certificate for CA with id [caId].
* Note: Can be used to import an external certificate and certificate chain
* to be installed into the CA.
* to be into an installed or uninstalled CA.
*/
const importCertToCa = async ({
caId,
@ -917,7 +917,18 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" });
if (ca.parentCaId) {
/**
* re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate
* CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different
* internal CA.
*/
throw new BadRequestError({
message: "Cannot import certificate to intermediate CA chained to internal parent CA"
});
}
const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined;
const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@ -988,7 +999,7 @@ export const certificateAuthorityServiceFactory = ({
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: 1,
version: caCert ? caCert.version + 1 : 1,
caSecretId: caSecret.id
},
tx

View File

@ -24,8 +24,8 @@ graph TD
A typical workflow for setting up a Private CA hierarchy consists of the following steps:
1. Configuring a root CA with details like name, validity period, and path length.
2. Configuring and chaining intermediate CA(s) with details like name, validity period, path length, and imported certificate.
1. Configuring an Infisical root CA with details like name, validity period, and path length — This step is optional if you wish to use an external root CA.
2. Configuring and chaining intermediate CA(s) with details like name, validity period, path length, and imported certificate to your Root CA.
3. Managing the CA lifecycle events such as CA succession.
<Note>
@ -39,19 +39,21 @@ A typical workflow for setting up a Private CA hierarchy consists of the followi
## Guide to Creating a CA Hierarchy
In the following steps, we explore how to create a simple Private CA hierarchy
consisting of a root CA and an intermediate CA.
consisting of an (optional) root CA and an intermediate CA.
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Creating a root CA">
If you wish to use an external root CA, you can skip this step and head to step 2 to create an intermediate CA.
To create a root CA, head to your Project > Internal PKI > Certificate Authorities and press **Create CA**.
![pki create ca](/images/platform/pki/ca-create.png)
![pki create ca](/images/platform/pki/ca/ca-create.png)
Here, set the **CA Type** to **Root** and fill out details for the root CA.
![pki create root ca](/images/platform/pki/ca-create-root.png)
![pki create root ca](/images/platform/pki/ca/ca-create-root.png)
Here's some guidance on each field:
@ -71,17 +73,19 @@ consisting of a root CA and an intermediate CA.
</Note>
</Step>
<Step title="Creating an intermediate CA">
1.1. To create an intermediate CA, press **Create CA** again but this time specifying the **CA Type** to be **Intermediate**. Fill out the details for the intermediate CA.
2.1. To create an intermediate CA, press **Create CA** again but this time specifying the **CA Type** to be **Intermediate**. Fill out the details for the intermediate CA.
![pki create intermediate ca](/images/platform/pki/ca-create-intermediate.png)
![pki create intermediate ca](/images/platform/pki/ca/ca-create-intermediate.png)
1.2. Next, press the **Install Certificate** option on the intermediate CA from step 1.1.
2.2. Next, press the **Install Certificate** option on the intermediate CA from step 1.1.
![pki install cert opt](/images/platform/pki/ca-install-intermediate-opt.png)
![pki install cert opt](/images/platform/pki/ca/ca-install-intermediate-opt.png)
Here, set the **Parent CA** to the root CA created in step 1 and configure the intended **Valid Until** and **Path Length** fields on the intermediate CA; feel free to use the prefilled values.
2.3a. If you created a root CA in step 1, select **Infisical CA** for the **Parent CA Type** field.
![pki install cert](/images/platform/pki/ca-install-intermediate.png)
Next, set the **Parent CA** to the root CA created in step 1 and configure the intended **Valid Until** and **Path Length** fields on the intermediate CA; feel free to use the prefilled values.
![pki install cert](/images/platform/pki/ca/ca-install-intermediate.png)
Here's some guidance on each field:
@ -91,17 +95,30 @@ consisting of a root CA and an intermediate CA.
Finally, press **Install** to chain the intermediate CA to the root CA; this creates a Certificate Signing Request (CSR) for the intermediate CA, creates an intermediate certificate using the root CA private key and CSR, and imports the signed certificate back to the intermediate CA.
![pki cas](/images/platform/pki/cas.png)
![pki cas](/images/platform/pki/ca/cas.png)
Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA.
Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA.
2.3b. If you have an external root CA, select **External CA** for the **Parent CA Type** field.
Next, use the provided intermediate CSR to generate a certificate from your external root CA and paste the PEM-encoded certificate back into the **Certificate Body** field; the PEM-encoded external root CA certificate should be pasted under the **Certificate Chain** field.
![pki ca csr](/images/platform/pki/ca/ca-install-intermediate-csr.png)
Finally, press **Install** to import the certificate and certificate chain as part of the installation step for the intermediate CA
Great! You've successfully created a Private CA hierarchy with an intermediate CA chained to an external root CA.
Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA.
</Step>
</Steps>
</Tab>
<Tab title="API">
<Steps>
<Step title="Creating a root CA">
If you wish to use an external root CA, you can skip this step and head to step 2 to create an intermediate CA.
To create a root CA, make an API request to the [Create CA](/api-reference/endpoints/certificate-authorities/create) API endpoint, specifying the `type` as `root`.
### Sample request
@ -181,6 +198,8 @@ consisting of a root CA and an intermediate CA.
}
```
If using an external root CA, then use the CSR to generate a certificate for the intermediate CA using your external root CA and skip to step 2.4.
2.3. Next, create an intermediate certificate by making an API request to the [Sign Intermediate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint
containing the CSR from step 2.2, referencing the root CA created in step 1.
@ -212,6 +231,8 @@ consisting of a root CA and an intermediate CA.
2.4. Finally, import the intermediate certificate and certificate chain from step 2.3 back to the intermediate CA by making an API request to the [Import Certificate](/api-reference/endpoints/certificate-authorities/import-cert) API endpoint.
If using an external root CA, then import the generated certificate and root CA certificate under certificate chain back into the intermediate CA.
### Sample request
```bash Request
@ -242,7 +263,17 @@ consisting of a root CA and an intermediate CA.
## Guide to CA Renewal
In the following steps, we explore how to renew a CA certificate via same key pair.
In the following steps, we explore how to renew a CA certificate.
<Note>
If renewing an intermediate CA chained to an Infisical CA, then Infisical will
automate the process of generating a new certificate for the intermediate CA for you.
If renewing an intermediate CA chained to an external parent CA, you'll be
required to generate a new certificate from the external parent CA and manually import
the certificate back to the intermediate CA.
</Note>
<Tabs>
<Tab title="Infisical UI">
@ -296,4 +327,10 @@ In the following steps, we explore how to renew a CA certificate via same key pa
At the moment, Infisical only supports CA renewal via same key pair. We
anticipate supporting CA renewal via new key pair in the coming month.
</Accordion>
<Accordion title="Does Infisical support chaining an Intermediate CA to an external Root CA?">
Yes. You may obtain a CSR from the Intermediate CA and use it to generate a
certificate from your external Root CA. The certificate, along with the Root
CA certificate, can be imported back to the Intermediate CA as part of the
CA installation step.
</Accordion>
</AccordionGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

View File

@ -11,7 +11,7 @@ type Props = {
};
const textAreaVariants = cva(
"textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50",
"textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50",
{
variants: {
size: {
@ -25,13 +25,13 @@ const textAreaVariants = cva(
false: ""
},
variant: {
filled: ["bg-bunker-800", "text-gray-400"],
filled: ["bg-mineshaft-900", "text-gray-400"],
outline: ["bg-transparent"],
plain: "bg-transparent outline-none"
},
isError: {
true: "focus:ring-red/50 placeholder-red-300 border-red",
false: "focus:ring-primary/50 border-mineshaft-400"
false: "focus:ring-primary-400/50 focus:ring-1 border-mineshaft-500"
}
},
compoundVariants: [

View File

@ -22,7 +22,6 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { TabSections } from "../Types";
import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components";
export const CaPage = withProjectPermission(

View File

@ -6,7 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { CaStatus, useGetCaById } from "@app/hooks/api";
import { CaStatus, CaType, useGetCaById } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
@ -35,6 +35,10 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA ID</p>
<div className="group flex align-top">
@ -56,26 +60,30 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
</div>
</div>
</div>
{ca.parentCaId && (
{ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.parentCaId}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextParentId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.parentCaId as string);
setCopyTextParentId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
<p className="text-sm text-mineshaft-300">
{ca.parentCaId ? ca.parentCaId : "N/A - External Parent CA"}
</p>
{ca.parentCaId && (
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextParentId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.parentCaId as string);
setCopyTextParentId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
)}
</div>
</div>
)}
@ -83,10 +91,6 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
@ -124,6 +128,15 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
colorSchema="primary"
type="submit"
onClick={() => {
if (ca.type === CaType.INTERMEDIATE && !ca.parentCaId) {
// intermediate CA with external parent CA
handlePopUpOpen("installCaCert", {
caId,
isParentCaExternal: true
});
return;
}
handlePopUpOpen("renewCa", {
caId
});

View File

@ -1,53 +1,10 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
// DatePicker,
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
import { ExternalCaInstallForm } from "./ExternalCaInstallForm";
import { InternalCaInstallForm } from "./InternalCaInstallForm";
type Props = {
popUp: UsePopUpState<["installCaCert"]>;
@ -60,234 +17,23 @@ enum ParentCaType {
}
export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const [parentCaType] = useState<ParentCaType>(ParentCaType.Internal);
const { currentWorkspace } = useWorkspace();
const caId = (popUp?.installCaCert?.data as { caId: string })?.caId || "";
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
const popupData = popUp?.installCaCert?.data;
const caId = popupData?.caId || "";
const isParentCaExternal = popupData?.isParentCaExternal || false;
const [parentCaType, setParentCaType] = useState<ParentCaType>(ParentCaType.Internal);
useEffect(() => {
if (cas?.length) {
setValue("parentCaId", cas[0].id);
if (popupData?.isParentCaExternal) {
setParentCaType(ParentCaType.External);
}
}, [cas, setValue]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
}, [popupData]);
const renderForm = (parentCaTypeInput: ParentCaType) => {
switch (parentCaTypeInput) {
case ParentCaType.Internal:
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas =
c.maxPathLength && c.maxPathLength !== 0;
return (
isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas
);
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* <Controller
name="notAfter"
control={control}
defaultValue={getDefaultNotAfterDate()}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Validity"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/> */}
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Path Length"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
return <InternalCaInstallForm caId={caId} handlePopUpToggle={handlePopUpToggle} />;
default:
return <div>External TODO</div>;
return <ExternalCaInstallForm caId={caId} handlePopUpToggle={handlePopUpToggle} />;
}
};
@ -296,31 +42,32 @@ export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
isOpen={popUp?.installCaCert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("installCaCert", isOpen);
reset();
}}
>
<ModalContent title="Install Intermediate CA certificate">
{/* <FormControl label="Parent CA Type" className="mt-4">
<ModalContent
title={`${isParentCaExternal ? "Renew" : "Install"} Intermediate CA certificate`}
>
<FormControl label="Parent CA Type">
<Select
defaultValue={ParentCaType.Internal}
value={parentCaType}
onValueChange={(e) => setParentCaType(e as ParentCaType)}
className="w-full"
isDisabled={isParentCaExternal}
>
<SelectItem
value={ParentCaType.Internal}
key={`parent-ca-type-${ParentCaType.Internal}`}
>
Infisical Private CA
Infisical CA
</SelectItem>
<SelectItem
value={ParentCaType.External}
key={`parent-ca-type-${ParentCaType.External}`}
>
External Private CA
External CA
</SelectItem>
</Select>
</FormControl> */}
</FormControl>
{renderForm(parentCaType)}
</ModalContent>
</Modal>

View File

@ -0,0 +1,172 @@
import { useEffect } from "react";
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 FileSaver from "file-saver";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton,TextArea, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetCaCsr, useImportCaCertificate } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
certificate: z.string().min(1),
certificateChain: z.string().min(1)
});
export type FormData = z.infer<typeof schema>;
type Props = {
caId: string;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
};
export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const [copyTextCaCsr, isCopyingCaCsr, setCopyTextCaCsr] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
useEffect(() => {
reset();
}, []);
const onFormSubmit = async ({ certificate, certificateChain }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
{csr && (
<>
<div className="my-4 flex items-center justify-between">
<h2>CSR for this CA</h2>
<div className="flex">
<Tooltip content={copyTextCaCsr}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(csr);
setCopyTextCaCsr("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCaCsr ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("csr.pem", csr);
}}
>
<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">{csr}</p>
</div>
</>
)}
<Controller
control={control}
name="certificate"
render={({ field, fieldState: { error } }) => (
<FormControl label="Certificate Body" errorText={error?.message} isError={Boolean(error)}>
<TextArea
{...field}
placeholder="PEM-encoded certificate..."
reSize="none"
className="h-48"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="certificateChain"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate Chain"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="PEM-encoded certificate chain..."
reSize="none"
className="h-48"
/>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
};

View File

@ -0,0 +1,236 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input,Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
caId: string;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
};
export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
useEffect(() => {
reset();
}, []);
useEffect(() => {
if (cas?.length) {
setValue("parentCaId", cas[0].id);
}
}, [cas, setValue]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
// defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
// defaultValue={field.value}
{...field}
onValueChange={onChange}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas = c.maxPathLength && c.maxPathLength !== 0;
return isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas;
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Path Length" errorText={error?.message} isError={Boolean(error)}>
<Select
// defaultValue={field.value}
{...field}
onValueChange={onChange}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
};