diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts
index 32dbc8fd0..b2ee04c4c 100644
--- a/backend/src/services/certificate-authority/certificate-authority-service.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-service.ts
@@ -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
diff --git a/frontend/src/components/v2/TextArea/TextArea.tsx b/frontend/src/components/v2/TextArea/TextArea.tsx
index 3f75f5a43..a1e71edd2 100644
--- a/frontend/src/components/v2/TextArea/TextArea.tsx
+++ b/frontend/src/components/v2/TextArea/TextArea.tsx
@@ -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: [
diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx
index 7edc99af9..e0bdaeb64 100644
--- a/frontend/src/views/Project/CaPage/CaPage.tsx
+++ b/frontend/src/views/Project/CaPage/CaPage.tsx
@@ -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(
diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx
index eb8ccf86f..94eae3e6b 100644
--- a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx
+++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx
@@ -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) => {
CA Details
+
+
CA Type
+
{caTypeToNameMap[ca.type]}
+
CA ID
@@ -56,26 +60,30 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
- {ca.parentCaId && (
+ {ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
Parent CA ID
-
{ca.parentCaId}
-
-
- {
- navigator.clipboard.writeText(ca.parentCaId as string);
- setCopyTextParentId("Copied");
- }}
- >
-
-
-
-
+
+ {ca.parentCaId ? ca.parentCaId : "N/A - External Parent CA"}
+
+ {ca.parentCaId && (
+
+
+ {
+ navigator.clipboard.writeText(ca.parentCaId as string);
+ setCopyTextParentId("Copied");
+ }}
+ >
+
+
+
+
+ )}
)}
@@ -83,10 +91,6 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
Friendly Name
{ca.friendlyName}
-
-
CA Type
-
{caTypeToNameMap[ca.type]}
-
Status
{caStatusToNameMap[ca.status]}
@@ -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
});
diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx
index 4d2bcb243..1095e0d3e 100644
--- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx
+++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx
@@ -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
;
+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.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({
- 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.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 (
-
- );
+ return ;
default:
- return External TODO
;
+ return ;
}
};
@@ -296,31 +42,32 @@ export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
isOpen={popUp?.installCaCert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("installCaCert", isOpen);
- reset();
}}
>
-
- {/*
+
+
- */}
+
{renderForm(parentCaType)}
diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx
new file mode 100644
index 000000000..d84b11f59
--- /dev/null
+++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx
@@ -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;
+
+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({
+ initialState: "Copy to clipboard"
+ });
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ 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 (
+
+ );
+};
diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/InternalCaInstallForm.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/InternalCaInstallForm.tsx
new file mode 100644
index 000000000..e54e97ae7
--- /dev/null
+++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaInstallCertModal/InternalCaInstallForm.tsx
@@ -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;
+
+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({
+ 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 (
+
+ );
+};