From 2f97fecf3931c410a0b898715ff30f47cec2caa4 Mon Sep 17 00:00:00 2001
From: Gabriel Hernandez
Date: Thu, 13 Mar 2025 16:46:10 +0000
Subject: [PATCH] add and delete digicert (#27083)
For #26606
This adds the ability to add and delete digicert integrations in the
fleet UI. this includes:
**The delete modal**

**the add digicert modal**

**integration with API to perform these actions**
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
---
frontend/interfaces/config.ts | 4 -
frontend/interfaces/integration.ts | 12 +-
.../IntegrationsPage/IntegrationNavItems.tsx | 12 +-
.../CertificateAuthorities.tsx | 83 ++++-----
.../AddCertAuthorityModal.tsx | 96 ++++++++++
.../AddCertAuthorityModal/_styles.scss | 6 +
.../AddCertAuthorityModal/helpers.tsx | 7 +
.../components/AddCertAuthorityModal/index.ts | 1 +
.../CertAuthorityListItem.tsx | 10 +-
.../CertificateAuthorityList.tsx | 10 +-
.../CertificateAuthorityList/_styles.scss | 8 +
.../DeleteCertificateAuthorityModal.tsx | 99 +++++++++++
.../_styles.scss | 3 +
.../helpers.ts | 125 +++++++++++++
.../DeleteCertificateAuthorityModal/index.ts | 1 +
.../components/DigicertForm/DigicertForm.tsx | 167 ++++++++++++++++++
.../components/DigicertForm/_styles.scss | 13 ++
.../components/DigicertForm/helpers.ts | 144 +++++++++++++++
.../components/DigicertForm/index.ts | 1 +
.../cards/CertificateAuthorities/helpers.tsx | 28 ++-
frontend/services/entities/certificates.ts | 11 ++
21 files changed, 765 insertions(+), 76 deletions(-)
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/_styles.scss
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/index.ts
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/DeleteCertificateAuthorityModal.tsx
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/_styles.scss
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/helpers.ts
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/index.ts
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/DigicertForm.tsx
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/_styles.scss
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/helpers.ts
create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/index.ts
create mode 100644 frontend/services/entities/certificates.ts
diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts
index ab5270e749..ff142318de 100644
--- a/frontend/interfaces/config.ts
+++ b/frontend/interfaces/config.ts
@@ -169,10 +169,6 @@ export interface IConfig {
disable_data_sync: boolean;
recent_vulnerability_max_age: number;
};
- // Note: `vulnerability_settings` is deprecated and should not be used
- // vulnerability_settings: {
- // databases_path: string;
- // };
webhook_settings: IWebhookSettings;
integrations: IGlobalIntegrations;
logging: {
diff --git a/frontend/interfaces/integration.ts b/frontend/interfaces/integration.ts
index 26e7f79eda..891aba0a54 100644
--- a/frontend/interfaces/integration.ts
+++ b/frontend/interfaces/integration.ts
@@ -25,8 +25,8 @@ export interface ICertificatesIntegrationNDES {
}
export interface ICertificatesIntegrationDigicert {
- id: number;
name: string;
+ url: string;
api_token: string;
profile_id: string;
certificate_common_name: string;
@@ -37,10 +37,18 @@ export interface ICertificatesIntegrationDigicert {
export interface ICertificatesIntegrationCustomSCEP {
id: number;
name: string;
- server_url: string;
+ url: string;
challenge: string;
}
+export type ICertificateAuthorityType = "ndes" | "digicert" | "custom";
+
+/** all the types of certificate integrations */
+export type ICertificateIntegration =
+ | ICertificatesIntegrationNDES
+ | ICertificatesIntegrationDigicert
+ | ICertificatesIntegrationCustomSCEP;
+
export interface IIntegration {
url: string;
username?: string;
diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx
index 40d175edff..7b82f974ed 100644
--- a/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx
+++ b/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx
@@ -34,12 +34,12 @@ const integrationSettingsNavItems: ISideNavItem[] = [
Card: ChangeManagement,
},
// TODO: digicert update: add this back when the feature is ready
- // {
- // title: "Certificates",
- // urlSection: "certificate-authorities",
- // path: PATHS.ADMIN_INTEGRATIONS_CERTIFICATE_AUTHORITIES,
- // Card: CertificateAuthorities,
- // },
+ {
+ title: "Certificates",
+ urlSection: "certificate-authorities",
+ path: PATHS.ADMIN_INTEGRATIONS_CERTIFICATE_AUTHORITIES,
+ Card: CertificateAuthorities,
+ },
];
export default integrationSettingsNavItems;
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx
index a639c6cc5d..4145346f9f 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx
@@ -3,7 +3,7 @@ import { Link } from "react-router";
import paths from "router/paths";
import { AppContext } from "context/app";
-import createMockConfig from "__mocks__/configMock";
+import { ICertificateIntegration } from "interfaces/integration";
import SectionHeader from "components/SectionHeader";
import CustomLink from "components/CustomLink";
@@ -12,52 +12,16 @@ import CertificateAuthorityList from "./components/CertificateAuthorityList";
import {
generateListData,
getCertificateAuthority,
- ICertAuthority,
+ ICertAuthorityListData,
} from "./helpers";
import AddCertAuthorityCard from "./components/AddCertAuthorityCard";
+import DeleteCertificateAuthorityModal from "./components/DeleteCertificateAuthorityModal";
+import AddCertAuthorityModal from "./components/AddCertAuthorityModal";
const baseClass = "certificate-authorities";
const CertificateAuthorities = () => {
- let { config } = useContext(AppContext);
- config = createMockConfig({
- integrations: {
- zendesk: [],
- jira: [],
- digicert: [
- {
- name: "DigiCert CA",
- id: 1,
- api_token: "123456",
- profile_id: "7ed77396-9186-4bfa-9fa7-63dddc46b8a3",
- certificate_common_name:
- "$FLEET_VAR_HOST_HARDWARE_SERIAL@example.com",
- certificate_user_principal_names: ["$FLEET_VAR_HOST_HARDWARE_SERIAL"],
- certificate_seat_id: "$FLEET_VAR_HOST_HARDWARE_SERIAL@example.com",
- },
- ],
- ndes_scep_proxy: {
- url: "https://ndes.scep.com",
- admin_url: "https://ndes.scep.com/admin",
- username: "ndes",
- password: "password",
- },
- custom_scep_proxy: [
- {
- id: 1,
- name: "Custom SCEP Proxy",
- server_url: "https://custom.scep.com",
- challenge: "challenge",
- },
- {
- id: 2,
- name: "Custom SCEP Proxy 2",
- server_url: "https://custom.scep2.com",
- challenge: "challenge-2",
- },
- ],
- },
- });
+ const { config } = useContext(AppContext);
const [showAddCertAuthorityModal, setShowAddCertAuthorityModal] = useState(
false
@@ -70,6 +34,14 @@ const CertificateAuthorities = () => {
setShowDeleteCertAuthorityModal,
] = useState(false);
+ const [selectedListItemId, setSelectedListItemId] = useState(
+ null
+ );
+ const [
+ selectedCertAuthority,
+ setSelectedCertAuthority,
+ ] = useState(null);
+
const certificateAuthorities = useMemo(() => {
if (!config) return [];
return generateListData(
@@ -83,26 +55,29 @@ const CertificateAuthorities = () => {
setShowAddCertAuthorityModal(true);
};
- const onEditCertAuthority = (cert: ICertAuthority) => {
+ const onEditCertAuthority = (cert: ICertAuthorityListData) => {
// TODO: use useCallback
- const ca = getCertificateAuthority(
+ const certAuthority = getCertificateAuthority(
cert.id,
config?.integrations.ndes_scep_proxy,
config?.integrations.digicert,
config?.integrations.custom_scep_proxy
);
- console.log(ca);
+ setSelectedListItemId(cert.id);
+ setSelectedCertAuthority(certAuthority);
setShowEditCertAuthorityModal(true);
};
- const onDeleteCertAuthority = (cert: ICertAuthority) => {
+ const onDeleteCertAuthority = (cert: ICertAuthorityListData) => {
// TODO: use useCallback
- getCertificateAuthority(
+ const certAuthority = getCertificateAuthority(
cert.id,
config?.integrations.ndes_scep_proxy,
config?.integrations.digicert,
config?.integrations.custom_scep_proxy
);
+ setSelectedListItemId(cert.id);
+ setSelectedCertAuthority(certAuthority);
setShowDeleteCertAuthorityModal(true);
};
@@ -138,9 +113,21 @@ const CertificateAuthorities = () => {
/>
{renderContent()}
- {showAddCertAuthorityModal && Modal showing
}
+ {showAddCertAuthorityModal && (
+ setShowAddCertAuthorityModal(false)}
+ />
+ )}
{showEditCertAuthorityModal && Modal showing
}
- {showDeleteCertAuthoirtyModal && Modal showing
}
+ {showDeleteCertAuthoirtyModal &&
+ selectedCertAuthority &&
+ selectedListItemId && (
+ setShowDeleteCertAuthorityModal(false)}
+ />
+ )}
);
};
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx
new file mode 100644
index 0000000000..38018dc204
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx
@@ -0,0 +1,96 @@
+import React, { useContext, useState } from "react";
+
+import { NotificationContext } from "context/notification";
+import certificatesAPI from "services/entities/certificates";
+import { ICertificateAuthorityType } from "interfaces/integration";
+import { AppContext } from "context/app";
+
+// @ts-ignore
+import Dropdown from "components/forms/fields/Dropdown";
+import Modal from "components/Modal";
+
+import { generateErrorMessage } from "./helpers";
+import DigicertForm from "../DigicertForm";
+import { IDigicertFormData } from "../DigicertForm/DigicertForm";
+import { useCertAuthorityDataGenerator } from "../DeleteCertificateAuthorityModal/helpers";
+
+const baseClass = "add-cert-authority-modal";
+
+interface IAddCertAuthorityModalProps {
+ onExit: () => void;
+}
+
+const AddCertAuthorityModal = ({ onExit }: IAddCertAuthorityModalProps) => {
+ const { setConfig } = useContext(AppContext);
+ const { renderFlash } = useContext(NotificationContext);
+ const [
+ certAuthorityType,
+ setCertAuthorityType,
+ ] = useState("digicert");
+ const [isAdding, setIsAdding] = useState(false);
+ const [formData, setFormData] = useState({
+ name: "",
+ url: "https://one.digicert.com",
+ apiToken: "",
+ profileId: "",
+ commonName: "",
+ userPrincipalName: "",
+ certificateSeatId: "",
+ });
+ const { generateAddPatchData } = useCertAuthorityDataGenerator(
+ certAuthorityType
+ );
+
+ const onChangeDropdown = (value: ICertificateAuthorityType) => {
+ setCertAuthorityType(value);
+ };
+
+ const onChangeForm = (update: { name: string; value: string }) => {
+ setFormData({ ...formData, [update.name]: update.value });
+ };
+
+ const onAddCertAuthority = async () => {
+ const addPatchData = generateAddPatchData(formData);
+ setIsAdding(true);
+ try {
+ const newConfig = await certificatesAPI.addCertificateAuthority(
+ addPatchData
+ );
+ renderFlash("success", "Successfully added your certificate authority.");
+ onExit();
+ setConfig(newConfig);
+ } catch (e) {
+ renderFlash("error", generateErrorMessage(e));
+ }
+ setIsAdding(false);
+ };
+
+ return (
+
+ <>
+
+
+ >
+
+ );
+};
+
+export default AddCertAuthorityModal;
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/_styles.scss
new file mode 100644
index 0000000000..8457d0880e
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/_styles.scss
@@ -0,0 +1,6 @@
+.add-cert-authority-modal {
+
+ &__cert-authority-dropdown {
+ margin-bottom: $pad-large;
+ }
+}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx
new file mode 100644
index 0000000000..d548cec89c
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx
@@ -0,0 +1,7 @@
+const DEFAULT_ERROR_MESSAGE =
+ "Couldn't add certificate authority. Please try again.";
+
+// eslint-disable-next-line import/prefer-default-export
+export const generateErrorMessage = (e: unknown) => {
+ return DEFAULT_ERROR_MESSAGE;
+};
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/index.ts
new file mode 100644
index 0000000000..9965f127fa
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./AddCertAuthorityModal";
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertAuthorityListItem/CertAuthorityListItem.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertAuthorityListItem/CertAuthorityListItem.tsx
index 3c70797613..f9ba0adbbc 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertAuthorityListItem/CertAuthorityListItem.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertAuthorityListItem/CertAuthorityListItem.tsx
@@ -5,7 +5,7 @@ import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
-import { ICertAuthority } from "../../helpers";
+import { ICertAuthorityListData } from "../../helpers";
const baseClass = "cert-authority-list-item";
@@ -17,7 +17,7 @@ interface IActionsProps {
const Actions = ({ onEdit, onDelete }: IActionsProps) => {
return (
<>
- (
)}
- />
+ /> */}
(
@@ -48,7 +48,7 @@ const Actions = ({ onEdit, onDelete }: IActionsProps) => {
};
interface ICertAuthorityListItemProps {
- cert: ICertAuthority;
+ cert: ICertAuthorityListData;
onClickEdit: () => void;
onClickDelete: () => void;
}
@@ -60,7 +60,7 @@ const CertAuthorityListItem = ({
}: ICertAuthorityListItemProps) => {
return (
void;
- onClickEdit: (cert: ICertAuthority) => void;
- onClickDelete: (cert: ICertAuthority) => void;
+ onClickEdit: (cert: ICertAuthorityListData) => void;
+ onClickDelete: (cert: ICertAuthorityListData) => void;
}
const CertificateAuthorityList = ({
@@ -22,7 +22,7 @@ const CertificateAuthorityList = ({
onClickDelete,
}: ICertificateAuthorityListProps) => {
return (
-
+
className={baseClass}
keyAttribute="name"
listItems={certAuthorities}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/_styles.scss
index 71bc9ca3e7..8111dd701d 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/_styles.scss
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/_styles.scss
@@ -1,3 +1,11 @@
.certificate-authority-list {
+ .list-item__actions {
+ display: none;
+ }
+ &:hover {
+ .list-item__actions {
+ display: flex;
+ }
+ }
}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/DeleteCertificateAuthorityModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/DeleteCertificateAuthorityModal.tsx
new file mode 100644
index 0000000000..e286a9e76e
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/DeleteCertificateAuthorityModal.tsx
@@ -0,0 +1,99 @@
+import React, { useContext, useState } from "react";
+
+import {
+ ICertificateAuthorityType,
+ ICertificateIntegration,
+} from "interfaces/integration";
+import certificatesAPI from "services/entities/certificates";
+import { NotificationContext } from "context/notification";
+import { AppContext } from "context/app";
+
+import Button from "components/buttons/Button";
+import Modal from "components/Modal";
+
+import {
+ generateCertAuthorityDisplayName,
+ useCertAuthorityDataGenerator,
+} from "./helpers";
+
+const baseClass = "delete-certificate-authority-modal";
+
+interface IDeleteCertificateAuthorityModalProps {
+ listItemId: string;
+ certAuthority: ICertificateIntegration;
+ onExit: () => void;
+}
+
+const DeleteCertificateAuthorityModal = ({
+ listItemId,
+ certAuthority,
+ onExit,
+}: IDeleteCertificateAuthorityModalProps) => {
+ const certAuthorityType = listItemId.split(
+ "-"
+ )[0] as ICertificateAuthorityType;
+
+ const { generateDeletePatchData } = useCertAuthorityDataGenerator(
+ certAuthorityType,
+ certAuthority
+ );
+ const { setConfig } = useContext(AppContext);
+ const { renderFlash } = useContext(NotificationContext);
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ const onDeleteCertAuthority = async () => {
+ setIsUpdating(true);
+ try {
+ const newConfig = await certificatesAPI.deleteCertificateAuthority(
+ generateDeletePatchData()
+ );
+ renderFlash(
+ "success",
+ "Successfully deleted your certificate authority."
+ );
+ setConfig(newConfig);
+ } catch (e) {
+ renderFlash(
+ "error",
+ "Couldn't delete certificate authority. Please try again."
+ );
+ }
+ setIsUpdating(false);
+ onExit();
+ };
+
+ const certAuthorityName = generateCertAuthorityDisplayName(
+ certAuthorityType,
+ certAuthority
+ );
+
+ return (
+
+ <>
+
+ Fleet won't remove certificates from the certificate authority (
+ {certAuthorityName}) on existing hosts.
+
+
+
+
+
+ >
+
+ );
+};
+
+export default DeleteCertificateAuthorityModal;
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/_styles.scss
new file mode 100644
index 0000000000..dbe4b65e62
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/_styles.scss
@@ -0,0 +1,3 @@
+.delete-certificate-authority-modal {
+
+}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/helpers.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/helpers.ts
new file mode 100644
index 0000000000..563e4d49aa
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/helpers.ts
@@ -0,0 +1,125 @@
+import { AppContext } from "context/app";
+
+import {
+ ICertificateAuthorityType,
+ ICertificateIntegration,
+ ICertificatesIntegrationCustomSCEP,
+ ICertificatesIntegrationDigicert,
+ IGlobalIntegrations,
+} from "interfaces/integration";
+import { useContext } from "react";
+import { IDigicertFormData } from "../DigicertForm/DigicertForm";
+
+export const useCertAuthorityDataGenerator = (
+ certAuthorityType: ICertificateAuthorityType,
+ certAuthority?: ICertificateIntegration
+) => {
+ const { config } = useContext(AppContext);
+
+ const generateAddPatchData = (formData: IDigicertFormData) => {
+ if (!config) return null;
+
+ const data: { integrations: Partial } = {
+ integrations: {},
+ };
+
+ switch (certAuthorityType) {
+ case "ndes":
+ break;
+ case "digicert":
+ data.integrations.digicert = [
+ ...(config.integrations.digicert || []),
+ {
+ name: formData.name,
+ url: formData.url,
+ api_token: formData.apiToken,
+ profile_id: formData.profileId,
+ certificate_common_name: formData.commonName,
+ certificate_user_principal_names: [formData.userPrincipalName],
+ certificate_seat_id: formData.certificateSeatId,
+ },
+ ];
+ break;
+ case "custom":
+ break;
+ default:
+ break;
+ }
+
+ return data;
+ };
+
+ /**
+ * generates the data to be sent to the API to delete the certificate authority.
+ * under the hood we are updating the app config object with the new data and
+ * have to generate the correct data for the PATCH request.
+ */
+ const generateDeletePatchData = () => {
+ if (!config) return null;
+
+ const data: { integrations: Partial } = {
+ integrations: {},
+ };
+
+ switch (certAuthorityType) {
+ case "ndes":
+ data.integrations.ndes_scep_proxy = null;
+ break;
+ case "digicert":
+ data.integrations.digicert = config.integrations.digicert?.filter(
+ (cert) => {
+ return (
+ (certAuthority as ICertificatesIntegrationDigicert).name !==
+ cert.name
+ );
+ }
+ );
+ break;
+ case "custom":
+ data.integrations.custom_scep_proxy = config.integrations.custom_scep_proxy?.filter(
+ (cert) => {
+ return (
+ (certAuthority as ICertificatesIntegrationCustomSCEP).id ===
+ cert.id
+ );
+ }
+ );
+ break;
+ default:
+ break;
+ }
+
+ return data;
+ };
+
+ /**
+ * generates the data to be sent to the API to edit the certificate authority.
+ * under the hood we are updating the app config object with the new data and
+ * have to generate the correct data for the PATCH request.
+ */
+ const generateEditPatchData = (formData: IDigicertFormData) => {
+ if (!config) return null;
+ };
+
+ return {
+ generateAddPatchData,
+ generateDeletePatchData,
+ generateEditPatchData,
+ };
+};
+
+export const generateCertAuthorityDisplayName = (
+ certAuthorityType: ICertificateAuthorityType,
+ certAuthority: ICertificateIntegration
+) => {
+ switch (certAuthorityType) {
+ case "ndes":
+ return "NDES";
+ case "digicert":
+ return (certAuthority as ICertificatesIntegrationDigicert).name;
+ case "custom":
+ return (certAuthority as ICertificatesIntegrationCustomSCEP).name;
+ default:
+ return "";
+ }
+};
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/index.ts
new file mode 100644
index 0000000000..d942a39da8
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DeleteCertificateAuthorityModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./DeleteCertificateAuthorityModal";
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/DigicertForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/DigicertForm.tsx
new file mode 100644
index 0000000000..53237eec62
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/DigicertForm.tsx
@@ -0,0 +1,167 @@
+import React, { useState } from "react";
+
+// @ts-ignore
+import InputField from "components/forms/fields/InputField";
+import Button from "components/buttons/Button";
+import CustomLink from "components/CustomLink";
+import TooltipWrapper from "components/TooltipWrapper";
+import { generateFormValidation, IDigicertFormValidation } from "./helpers";
+
+const baseClass = "digicert-form";
+
+export interface IDigicertFormData {
+ name: string;
+ url: string;
+ apiToken: string;
+ profileId: string;
+ commonName: string;
+ userPrincipalName: string;
+ certificateSeatId: string;
+}
+
+interface IDigicertFormProps {
+ formData: IDigicertFormData;
+ submitBtnText: string;
+ isSubmitting: boolean;
+ onChange: (update: { name: string; value: string }) => void;
+ onSubmit: () => void;
+ onCancel: () => void;
+}
+
+const DigicertForm = ({
+ formData,
+ submitBtnText,
+ isSubmitting,
+ onChange,
+ onSubmit,
+ onCancel,
+}: IDigicertFormProps) => {
+ const [formValidation, setFormValidation] = useState(
+ {
+ isValid: false,
+ }
+ );
+
+ const {
+ name,
+ url,
+ apiToken,
+ profileId,
+ commonName,
+ userPrincipalName,
+ certificateSeatId,
+ } = formData;
+
+ const onSubmitForm = (evt: React.FormEvent) => {
+ evt.preventDefault();
+ onSubmit();
+ };
+
+ const onInputChange = (update: { name: string; value: string }) => {
+ setFormValidation(
+ generateFormValidation({ ...formData, [update.name]: update.value })
+ );
+ onChange(update);
+ };
+
+ return (
+
+ );
+};
+
+export default DigicertForm;
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/_styles.scss
new file mode 100644
index 0000000000..2c5187f9aa
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/_styles.scss
@@ -0,0 +1,13 @@
+.digicert-form {
+ &__fields {
+ display: flex;
+ flex-direction: column;
+ gap: $pad-large;
+ }
+
+ &__cta {
+ display: flex;
+ flex-direction: row-reverse;
+ gap: $pad-medium;
+ }
+}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/helpers.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/helpers.ts
new file mode 100644
index 0000000000..2d89ca38ba
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/helpers.ts
@@ -0,0 +1,144 @@
+import valid_url from "components/forms/validators/valid_url";
+import { IDigicertFormData } from "./DigicertForm";
+
+// TODO: create a validator abstraction for this and the other form validation files
+
+export interface IDigicertFormValidation {
+ isValid: boolean;
+ name?: { isValid: boolean; message?: string };
+ url?: { isValid: boolean; message?: string };
+ apiToken?: { isValid: boolean };
+ profileId?: { isValid: boolean };
+ commonName?: { isValid: boolean };
+ certificateSeatId?: { isValid: boolean };
+}
+
+type IMessageFunc = (formData: IDigicertFormData) => string;
+type IValidationMessage = string | IMessageFunc;
+type IFormValidationKey = keyof Omit;
+
+interface IValidation {
+ name: string;
+ isValid: (formData: IDigicertFormData) => boolean;
+ message?: IValidationMessage;
+}
+
+const FORM_VALIDATIONS: Record<
+ IFormValidationKey,
+ { validations: IValidation[] }
+> = {
+ name: {
+ validations: [
+ {
+ name: "required",
+ isValid: (formData: IDigicertFormData) => {
+ return formData.name.length > 0;
+ },
+ },
+ {
+ name: "invalidCharacters",
+ isValid: (formData: IDigicertFormData) => {
+ return /^[a-zA-Z0-9_]+$/.test(formData.name);
+ },
+ message:
+ "Inalid characters. Only letters, numbers and underscores allowed.",
+ },
+ ],
+ },
+ url: {
+ validations: [
+ {
+ name: "required",
+ isValid: (formData: IDigicertFormData) => {
+ return formData.url.length > 0;
+ },
+ },
+ {
+ name: "validUrl",
+ isValid: (formData: IDigicertFormData) => {
+ return valid_url({ url: formData.url });
+ },
+ message: (formData: IDigicertFormData) =>
+ `${formData.url} is not a valid URL`,
+ },
+ ],
+ },
+ apiToken: {
+ validations: [
+ {
+ name: "required",
+ isValid: (formData: IDigicertFormData) => {
+ return formData.apiToken.length > 0;
+ },
+ },
+ ],
+ },
+ profileId: {
+ validations: [
+ {
+ name: "required",
+ isValid: (formData: IDigicertFormData) => {
+ return formData.profileId.length > 0;
+ },
+ },
+ ],
+ },
+ commonName: {
+ validations: [
+ {
+ name: "required",
+ isValid: (formData: IDigicertFormData) => {
+ return formData.commonName.length > 0;
+ },
+ },
+ ],
+ },
+ certificateSeatId: {
+ validations: [
+ {
+ name: "required",
+ isValid: (formData: IDigicertFormData) => {
+ return formData.certificateSeatId.length > 0;
+ },
+ },
+ ],
+ },
+};
+
+const getErrorMessage = (
+ formData: IDigicertFormData,
+ message?: IValidationMessage
+) => {
+ if (message === undefined || typeof message === "string") {
+ return message;
+ }
+ return message(formData);
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export const generateFormValidation = (formData: IDigicertFormData) => {
+ const formValidation: IDigicertFormValidation = {
+ isValid: true,
+ };
+
+ Object.keys(FORM_VALIDATIONS).forEach((key) => {
+ const objKey = key as keyof typeof FORM_VALIDATIONS;
+ const failedValidation = FORM_VALIDATIONS[objKey].validations.find(
+ (validation) => !validation.isValid(formData)
+ );
+
+ if (!failedValidation) {
+ formValidation[objKey] = {
+ isValid: true,
+ };
+ } else {
+ formValidation.isValid = false;
+ formValidation[objKey] = {
+ isValid: false,
+ message: getErrorMessage(formData, failedValidation.message),
+ };
+ }
+ });
+
+ return formValidation;
+};
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/index.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/index.ts
new file mode 100644
index 0000000000..5371d1b145
--- /dev/null
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/DigicertForm/index.ts
@@ -0,0 +1 @@
+export { default } from "./DigicertForm";
diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/helpers.tsx
index a69feb63f4..65228043a1 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/helpers.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/helpers.tsx
@@ -4,7 +4,7 @@ import {
ICertificatesIntegrationNDES,
} from "interfaces/integration";
-export interface ICertAuthority {
+export interface ICertAuthorityListData {
id: string;
name: string;
description: string;
@@ -15,7 +15,7 @@ export const generateListData = (
digicertCerts?: ICertificatesIntegrationDigicert[],
customProxies?: ICertificatesIntegrationCustomSCEP[]
) => {
- const listData: ICertAuthority[] = [];
+ const listData: ICertAuthorityListData[] = [];
// these values for the certificateAuthority is meant to be a hard coded .
if (ndesProxy) {
@@ -29,7 +29,7 @@ export const generateListData = (
if (digicertCerts?.length) {
digicertCerts.forEach((cert) => {
listData.push({
- id: `digicert-${cert.id}`,
+ id: `digicert-${cert.name}`,
name: cert.name,
description: "DigiCert",
});
@@ -51,6 +51,21 @@ export const generateListData = (
);
};
+export interface ICertIntegrationNDESWithListId
+ extends ICertificatesIntegrationNDES {
+ listId: string;
+}
+
+export interface ICertIntegrationDigicertWithListId
+ extends ICertificatesIntegrationDigicert {
+ listId: string;
+}
+
+export interface ICertIntegrationCustomSCEPWithListId
+ extends ICertificatesIntegrationCustomSCEP {
+ listId: string;
+}
+
/**
* Gets the original certificate aithority integration object from the id
* of the data representing a certificate authority list item.
@@ -67,14 +82,15 @@ export const getCertificateAuthority = (
if (id.includes("digicert")) {
return digicertCerts?.find(
- (cert) => id.split("digicert-")[1] === cert.id.toString()
- );
+ (cert) => id.split("digicert-")[1] === cert.name
+ ) as ICertificatesIntegrationDigicert;
}
if (id.includes("custom-scep-proxy")) {
return customProxies?.find(
+ // TODO: remove custom scep id
(cert) => id.split("custom-scep-proxy-")[1] === cert.id.toString()
- );
+ ) as ICertificatesIntegrationCustomSCEP;
}
return null;
diff --git a/frontend/services/entities/certificates.ts b/frontend/services/entities/certificates.ts
new file mode 100644
index 0000000000..265376e3f9
--- /dev/null
+++ b/frontend/services/entities/certificates.ts
@@ -0,0 +1,11 @@
+import configAPI from "./config";
+
+export default {
+ addCertificateAuthority: (updateData: any): Promise => {
+ return configAPI.update(updateData);
+ },
+
+ deleteCertificateAuthority: (updateData: any): Promise => {
+ return configAPI.update(updateData);
+ },
+};