From 50e7947b673b8db268d394469398b52776bf8c90 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:06:07 -0800 Subject: [PATCH] Update Add, Edit, and Delete Certificate Authority modals to support Custom EST (#35085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #34276 Screenshot 2025-10-31 at 5 21 57 PM ![ezgif-6f70f761e3ad5b](https://github.com/user-attachments/assets/606a4696-7fc2-409f-a047-6436f1916899) - [x] Changes file added for user-visible changes in `changes/` - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually --- .../32110-custom-est-certificate-authorities | 1 + frontend/interfaces/activity.ts | 3 + frontend/interfaces/certificates.ts | 28 +- .../GlobalActivityItem/GlobalActivityItem.tsx | 3 + .../CertificateAuthorities.tsx | 7 +- .../AddCertAuthorityModal.tsx | 35 +- .../AddCertAuthorityModal/helpers.tsx | 45 ++- .../CertificateAuthorityList.tsx | 3 + .../CustomESTForm/CustomESTForm.tests.tsx | 112 +++++++ .../CustomESTForm/CustomESTForm.tsx | 120 +++++++ .../components/CustomESTForm/helpers.ts | 148 +++++++++ .../components/CustomESTForm/index.ts | 1 + .../EditCertAuthorityModal.tsx | 39 ++- .../EditCertAuthorityModal/helpers.tsx | 306 +++++++++++------- frontend/services/entities/certificates.ts | 7 +- 15 files changed, 700 insertions(+), 158 deletions(-) create mode 100644 changes/32110-custom-est-certificate-authorities create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tests.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/helpers.ts create mode 100644 frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/index.ts diff --git a/changes/32110-custom-est-certificate-authorities b/changes/32110-custom-est-certificate-authorities new file mode 100644 index 0000000000..e594d2eb79 --- /dev/null +++ b/changes/32110-custom-est-certificate-authorities @@ -0,0 +1 @@ +* Support Custom EST certificate authorities diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index e643b58263..0b4059fe9f 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -65,6 +65,9 @@ export enum ActivityType { AddedSmallstep = "added_smallstep", DeletedSmallstep = "deleted_smallstep", EditedSmallstep = "edited_smallstep", + AddedCustomEST = "added_custom_est", + DeletedCustomEST = "deleted_custom_est", + EditedCustomEST = "edited_custom_est", CreatedWindowsProfile = "created_windows_profile", DeletedWindowsProfile = "deleted_windows_profile", EditedWindowsProfile = "edited_windows_profile", diff --git a/frontend/interfaces/certificates.ts b/frontend/interfaces/certificates.ts index 9b0d452bfb..655019a466 100644 --- a/frontend/interfaces/certificates.ts +++ b/frontend/interfaces/certificates.ts @@ -88,12 +88,22 @@ export interface ICertificatesSmallstep { password: string; } +export interface ICertificatesCustomEST { + id?: number; + type?: "custom_est_proxy"; + name: string; + url: string; + username: string; + password: string; +} + export type ICertificateAuthorityType = | "ndes_scep_proxy" | "digicert" | "custom_scep_proxy" | "hydrant" - | "smallstep"; + | "smallstep" + | "custom_est_proxy"; /** all the types of certificates */ export type ICertificateAuthority = @@ -101,7 +111,8 @@ export type ICertificateAuthority = | ICertificatesDigicert | ICertificatesHydrant | ICertificatesCustomSCEP - | ICertificatesSmallstep; + | ICertificatesSmallstep + | ICertificatesCustomEST; export const isNDESCertAuthority = ( integration: ICertificateAuthority @@ -154,3 +165,16 @@ export const isSmallstepCertAuthority = ( "password" in integration ); }; + +export const isCustomESTCertAuthority = ( + integration: ICertificateAuthority +): integration is ICertificatesCustomEST => { + return ( + "name" in integration && + "url" in integration && + // differentiates from smallstep + !("challenge_url" in integration) && + "username" in integration && + "password" in integration + ); +}; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 99528a5119..e83a1b44e8 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -1713,12 +1713,14 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.AddedCustomScepProxy: case ActivityType.AddedDigicert: case ActivityType.AddedHydrant: + case ActivityType.AddedCustomEST: case ActivityType.AddedSmallstep: { return TAGGED_TEMPLATES.addedCertificateAuthority(activity.details?.name); } case ActivityType.DeletedCustomScepProxy: case ActivityType.DeletedDigicert: case ActivityType.DeletedHydrant: + case ActivityType.DeletedCustomEST: case ActivityType.DeletedSmallstep: { return TAGGED_TEMPLATES.deletedCertificateAuthority( activity.details?.name @@ -1727,6 +1729,7 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.EditedCustomScepProxy: case ActivityType.EditedDigicert: case ActivityType.EditedHydrant: + case ActivityType.EditedCustomEST: case ActivityType.EditedSmallstep: { return TAGGED_TEMPLATES.editedCertificateAuthority( activity.details?.name diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx index 5eec1bcc3d..f3674f3b5a 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/CertificateAuthorities.tsx @@ -105,12 +105,7 @@ const CertificateAuthorities = () => { content={ <> To help your end users connect to Wi-Fi or VPNs, you can add your - certificate authority. Then, head over to{" "} - {" "} - settings to configure how certificates are delivered to your hosts.{" "} + certificate authority.{" "} ({ + name: "", + url: "", + username: "", + password: "", + }); + const onChangeDropdown = (value: ICertificateAuthorityType) => { setCertAuthorityType(value); }; @@ -122,6 +136,10 @@ const AddCertAuthorityModal = ({ setFormData = setSmallstepFormData; formData = smallstepFormData; break; + case "custom_est_proxy": + setFormData = setCustomESTFormData; + formData = customESTFormData; + break; default: return; } @@ -150,6 +168,9 @@ const AddCertAuthorityModal = ({ case "smallstep": formData = smallstepFormData; break; + case "custom_est_proxy": + formData = customESTFormData; + break; default: return; } @@ -241,6 +262,18 @@ const AddCertAuthorityModal = ({ onCancel={onExit} /> ); + case "custom_est_proxy": + return ( + + ); default: return null; } diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx index 9cd3aa0fbf..1830c85c9d 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx @@ -14,6 +14,7 @@ import { INDESFormData } from "../NDESForm/NDESForm"; import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm"; import { IHydrantFormData } from "../HydrantForm/HydrantForm"; import { ISmallstepFormData } from "../SmallstepForm/SmallstepForm"; +import { ICustomESTFormData } from "../CustomESTForm/CustomESTForm"; // FIXME: do we care about the order of these? Should we alphabetize them or something? const DEFAULT_CERT_AUTHORITY_OPTIONS: IDropdownOption[] = [ @@ -31,6 +32,10 @@ const DEFAULT_CERT_AUTHORITY_OPTIONS: IDropdownOption[] = [ value: "custom_scep_proxy", }, { label: "Smallstep", value: "smallstep" }, + { + label: "Custom EST (Enrollment Over Secure Transport)", + value: "custom_est_proxy", + }, ]; /** @@ -66,8 +71,7 @@ export const generateAddCertAuthorityData = ( formData: ICertFormData ): IAddCertAuthorityBody | undefined => { switch (certAuthorityType) { - case "ndes_scep_proxy": - // eslint-disable-next-line no-case-declarations + case "ndes_scep_proxy": { const { scepURL, adminURL, @@ -82,8 +86,8 @@ export const generateAddCertAuthorityData = ( password, }, }; - case "digicert": - // eslint-disable-next-line no-case-declarations + } + case "digicert": { const { name, url: digicertUrl, @@ -104,8 +108,8 @@ export const generateAddCertAuthorityData = ( certificate_seat_id: certificateSeatId, }, }; - case "custom_scep_proxy": - // eslint-disable-next-line no-case-declarations + } + case "custom_scep_proxy": { const { name: customSCEPName, scepURL: customSCEPUrl, @@ -118,8 +122,8 @@ export const generateAddCertAuthorityData = ( challenge, }, }; - case "hydrant": - // eslint-disable-next-line no-case-declarations + } + case "hydrant": { const { name: hydrantName, url, @@ -134,8 +138,8 @@ export const generateAddCertAuthorityData = ( client_secret: clientSecret, }, }; - case "smallstep": - // eslint-disable-next-line no-case-declarations + } + case "smallstep": { const { name: smallstepName, scepURL: smallstepScepURL, @@ -152,8 +156,27 @@ export const generateAddCertAuthorityData = ( password: smallstepPassword, }, }; + } + case "custom_est_proxy": { + const { + name: customESTName, + url: customESTUrl, + username: customESTUsername, + password: customESTPassword, + } = formData as ICustomESTFormData; + return { + custom_est_proxy: { + name: customESTName, + url: customESTUrl, + username: customESTUsername, + password: customESTPassword, + }, + }; + } default: - return undefined; + throw new Error( + `Unknown certificate authority type: ${certAuthorityType}` + ); } }; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx index 3d97d8adfa..3ba183a6bd 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx @@ -37,6 +37,9 @@ export const generateListData = ( case "smallstep": description = "Smallstep"; break; + case "custom_est_proxy": + description = "Custom Enrollment Over Secure Transport (EST)"; + break; default: description = "Unknown Certificate Authority Type"; } diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tests.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tests.tsx new file mode 100644 index 0000000000..10eda73ca5 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tests.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { noop } from "lodash"; +import { render, screen } from "@testing-library/react"; + +import CustomESTForm, { ICustomESTFormData } from "./CustomESTForm"; + +const createTestFormData = (overrides?: Partial) => ({ + name: "TEST_NAME", + url: "https://test.com", + username: "testuser", + password: "testpassword", + ...overrides, +}); + +describe("CustomESTForm", () => { + it("render the custom button text", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeVisible(); + }); + + it("enables submission depending on the form validation", async () => { + const testData = createTestFormData(); + render( + + ); + + // data is valid, so submit should be enabled + expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled(); + }); + + it("disables submission when form is invalid", async () => { + const testData = createTestFormData(); + // make name invalid by setting it to an empty string + testData.name = ""; + render( + + ); + // name is required, so submit should be disabled + expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); + }); + + it("disables submit when isSubmitting is set to true", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); + }); + + it("submit button is disabled if isDirty is false", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); + }); + + it("submit button is enabled if isDirty", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled(); + }); +}); diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tsx new file mode 100644 index 0000000000..67eb57ccbe --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/CustomESTForm.tsx @@ -0,0 +1,120 @@ +import React, { useMemo } from "react"; + +import { ICertificateAuthorityPartial } from "interfaces/certificates"; + +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +import Button from "components/buttons/Button"; +import TooltipWrapper from "components/TooltipWrapper"; + +import { generateFormValidations, validateFormData } from "./helpers"; + +export interface ICustomESTFormData { + name: string; + url: string; + username: string; + password: string; +} +interface ICustomESTFormProps { + formData: ICustomESTFormData; + certAuthorities?: ICertificateAuthorityPartial[]; + submitBtnText: string; + isSubmitting: boolean; + isEditing?: boolean; + isDirty?: boolean; + onChange: (update: { name: string; value: string }) => void; + onSubmit: () => void; + onCancel: () => void; +} + +const CustomESTForm = ({ + formData, + certAuthorities, + submitBtnText, + isSubmitting, + isEditing = false, + isDirty = true, + onChange, + onSubmit, + onCancel, +}: ICustomESTFormProps) => { + const validationsConfig = useMemo(() => { + return generateFormValidations(certAuthorities ?? [], isEditing); + }, [certAuthorities, isEditing]); + + const validations = useMemo(() => { + return validateFormData(formData, validationsConfig); + }, [formData, validationsConfig]); + + const { name, url, username, password } = formData; + + const onSubmitForm = (evt: React.FormEvent) => { + evt.preventDefault(); + onSubmit(); + }; + + return ( +
+ + + + +
+ + + + +
+ + ); +}; + +export default CustomESTForm; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/helpers.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/helpers.ts new file mode 100644 index 0000000000..05e328bab7 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/helpers.ts @@ -0,0 +1,148 @@ +import { ICertificateAuthorityPartial } from "interfaces/certificates"; + +import valid_url from "components/forms/validators/valid_url"; + +import { ICustomESTFormData } from "./CustomESTForm"; + +export interface ICustomESTFormValidation { + isValid: boolean; + name?: { isValid: boolean; message?: string }; + url?: { isValid: boolean; message?: string }; + username?: { isValid: boolean; message?: string }; + password?: { isValid: boolean; message?: string }; +} + +type IMessageFunc = (formData: ICustomESTFormData) => string; +type IValidationMessage = string | IMessageFunc; +type IFormValidationKey = keyof Omit; + +interface IValidation { + name: string; + isValid: (formData: ICustomESTFormData) => boolean; + message?: IValidationMessage; +} + +type IFormValidations = Record< + IFormValidationKey, + { validations: IValidation[] } +>; + +export const generateFormValidations = ( + customESTIntegrations: ICertificateAuthorityPartial[], + isEditing: boolean +) => { + const FORM_VALIDATIONS: IFormValidations = { + name: { + validations: [ + { + name: "required", + isValid: (formData: ICustomESTFormData) => { + return formData.name.length > 0; + }, + }, + { + name: "invalidCharacters", + isValid: (formData: ICustomESTFormData) => { + return /^[a-zA-Z0-9_]+$/.test(formData.name); + }, + message: + "Invalid characters. Only letters, numbers and underscores allowed.", + }, + { + name: "unique", + isValid: (formData: ICustomESTFormData) => { + return ( + isEditing || + customESTIntegrations.find( + (cert) => + cert.name === formData.name && + cert.type === "custom_est_proxy" + ) === undefined + ); + }, + message: "Name is already used by another custom EST CA.", + }, + ], + }, + url: { + validations: [ + { + name: "required", + isValid: (formData: ICustomESTFormData) => { + return formData.url.length > 0; + }, + }, + { + name: "validUrl", + isValid: (formData: ICustomESTFormData) => { + return valid_url({ url: formData.url }); + }, + message: "Must be a valid URL.", + }, + ], + }, + username: { + validations: [ + { + name: "required", + isValid: (formData: ICustomESTFormData) => { + return formData.username.length > 0; + }, + }, + ], + }, + password: { + validations: [ + { + name: "required", + isValid: (formData: ICustomESTFormData) => { + return formData.password.length > 0; + }, + }, + ], + }, + }; + + return FORM_VALIDATIONS; +}; + +const getErrorMessage = ( + formData: ICustomESTFormData, + message?: IValidationMessage +) => { + if (message === undefined || typeof message === "string") { + return message; + } + return message(formData); +}; + +// eslint-disable-next-line import/prefer-default-export +export const validateFormData = ( + formData: ICustomESTFormData, + validationConfig: IFormValidations +) => { + const formValidation: ICustomESTFormValidation = { + isValid: true, + }; + + Object.keys(validationConfig).forEach((key) => { + const objKey = key as keyof typeof validationConfig; + const failedValidation = validationConfig[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/CustomESTForm/index.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/index.ts new file mode 100644 index 0000000000..a0d2892d7c --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomESTForm/index.ts @@ -0,0 +1 @@ +export { default } from "./CustomESTForm"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx index f5a112619c..4e5ee3ebf8 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx @@ -23,6 +23,7 @@ import NDESForm from "../NDESForm"; import CustomSCEPForm from "../CustomSCEPForm"; import HydrantForm from "../HydrantForm"; import SmallstepForm from "../SmallstepForm"; +import CustomESTForm from "../CustomESTForm"; const baseClass = "edit-cert-authority-modal"; @@ -75,7 +76,7 @@ const EditCertAuthorityModal = ({ certAuthority.id, editPatchData ); - renderFlash("success", "Successfully edited your certificate authority."); + renderFlash("success", "Successfully edited certificate authority."); onExit(); } catch (e) { renderFlash("error", getErrorMessage(e)); @@ -84,26 +85,24 @@ const EditCertAuthorityModal = ({ }; const getFormComponent = () => { - if (certAuthority.type === "ndes_scep_proxy") { - return NDESForm; + switch (certAuthority.type) { + case "ndes_scep_proxy": + return NDESForm; + case "digicert": + return DigicertForm; + case "hydrant": + return HydrantForm; + case "smallstep": + return SmallstepForm; + case "custom_scep_proxy": + return CustomSCEPForm; + case "custom_est_proxy": + return CustomESTForm; + default: + throw new Error( + `Unknown certificate authority type: ${certAuthority.type}` + ); } - if (certAuthority.type === "digicert") { - return DigicertForm; - } - if (certAuthority.type === "hydrant") { - return HydrantForm; - } - if (certAuthority.type === "smallstep") { - return SmallstepForm; - } - - // FIXME: seems like we have some competing patterns in here where we sometimes do switch - // statements with a default and sometimes do if or if/else if with a final default return. We - // should probably standardize on one or the other. Also, do we really want this to be the - // default? Why not have an explicit check for custom_scep_proxy and have the final - // else throw an error? - - return CustomSCEPForm; }; const renderForm = () => { diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx index b76497eccc..fabf974e91 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx @@ -14,57 +14,67 @@ import { INDESFormData } from "../NDESForm/NDESForm"; import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm"; import { IHydrantFormData } from "../HydrantForm/HydrantForm"; import { ISmallstepFormData } from "../SmallstepForm/SmallstepForm"; +import { ICustomESTFormData } from "../CustomESTForm/CustomESTForm"; + +const UNCHANGED_PASSWORD_API_RESPONSE = "********"; export const generateDefaultFormData = ( certAuthority: ICertificateAuthority ): ICertFormData => { - if (certAuthority.type === "ndes_scep_proxy") { - return { - scepURL: certAuthority.url, - adminURL: certAuthority.admin_url, - username: certAuthority.username, - password: certAuthority.password, - }; - } else if (certAuthority.type === "digicert") { - return { - name: certAuthority.name, - url: certAuthority.url, - apiToken: certAuthority.api_token, - profileId: certAuthority.profile_id, - commonName: certAuthority.certificate_common_name, - userPrincipalName: - certAuthority.certificate_user_principal_names?.[0] ?? "", - certificateSeatId: certAuthority.certificate_seat_id, - }; - } else if (certAuthority.type === "hydrant") { - return { - name: certAuthority.name, - url: certAuthority.url, - clientId: certAuthority.client_id, - clientSecret: certAuthority.client_secret, - }; - } else if (certAuthority.type === "smallstep") { - return { - name: certAuthority.name, - scepURL: certAuthority.url, - challengeURL: certAuthority.challenge_url, - username: certAuthority.username, - password: certAuthority.password, - }; + switch (certAuthority.type) { + case "ndes_scep_proxy": + return { + scepURL: certAuthority.url, + adminURL: certAuthority.admin_url, + username: certAuthority.username, + password: certAuthority.password, + }; + case "digicert": + return { + name: certAuthority.name, + url: certAuthority.url, + apiToken: certAuthority.api_token, + profileId: certAuthority.profile_id, + commonName: certAuthority.certificate_common_name, + userPrincipalName: + certAuthority.certificate_user_principal_names?.[0] ?? "", + certificateSeatId: certAuthority.certificate_seat_id, + }; + case "hydrant": + return { + name: certAuthority.name, + url: certAuthority.url, + clientId: certAuthority.client_id, + clientSecret: certAuthority.client_secret, + }; + case "smallstep": + return { + name: certAuthority.name, + scepURL: certAuthority.url, + challengeURL: certAuthority.challenge_url, + username: certAuthority.username, + password: certAuthority.password, + }; + case "custom_scep_proxy": { + const customSCEPcert = certAuthority as ICertificatesCustomSCEP; + return { + name: customSCEPcert.name, + scepURL: customSCEPcert.url, + challenge: customSCEPcert.challenge, + }; + } + case "custom_est_proxy": + return { + name: certAuthority.name, + url: certAuthority.url, + username: certAuthority.username, + password: certAuthority.password, + }; + default: + throw new Error( + `Unknown certificate authority type: ${certAuthority.type}` + ); } - - // FIXME: seems like we have some competing patterns in here where we sometimes do switch - // statements with a default and sometimes do if or if/else if with a final default return. We - // should probably standardize on one or the other. Also, do we really want this to be the - // default? Why not have an explicit check for custom_scep_proxy and have the final - // else throw an error? - - const customSCEPcert = certAuthority as ICertificatesCustomSCEP; - return { - name: customSCEPcert.name, - scepURL: customSCEPcert.url, - challenge: customSCEPcert.challenge, - }; }; export const generateEditCertAuthorityData = ( @@ -76,8 +86,7 @@ export const generateEditCertAuthorityData = ( delete certAuthWithoutType.id; switch (certAuthority.type) { - case "ndes_scep_proxy": - // eslint-disable-next-line no-case-declarations + case "ndes_scep_proxy": { const { scepURL, adminURL, @@ -95,8 +104,8 @@ export const generateEditCertAuthorityData = ( certAuthWithoutType ), }; - case "digicert": - // eslint-disable-next-line no-case-declarations + } + case "digicert": { const { name, url: digicertUrl, @@ -120,8 +129,8 @@ export const generateEditCertAuthorityData = ( certAuthWithoutType ), }; - case "hydrant": - // eslint-disable-next-line no-case-declarations + } + case "hydrant": { const { name: hydrantName, url: hydrantUrl, @@ -139,8 +148,8 @@ export const generateEditCertAuthorityData = ( certAuthWithoutType ), }; - case "smallstep": - // eslint-disable-next-line no-case-declarations + } + case "smallstep": { const { name: smallstepName, scepURL: smallstepURL, @@ -160,12 +169,8 @@ export const generateEditCertAuthorityData = ( certAuthWithoutType ), }; - - // FIXME: do we really want this to be the default? why not have an explicit case for - // custom_scep_proxy and have the default throw an error? - default: - // custom_scep_proxy - // eslint-disable-next-line no-case-declarations + } + case "custom_scep_proxy": { const { name: customSCEPName, scepURL: customSCEPUrl, @@ -181,6 +186,30 @@ export const generateEditCertAuthorityData = ( certAuthWithoutType ), }; + } + case "custom_est_proxy": { + const { + name: customESTName, + scepURL: customESTUrl, + username: customESTUsername, + password: customESTPassword, + } = formData as ISmallstepFormData; + return { + custom_est_proxy: deepDifference( + { + name: customESTName, + url: customESTUrl, + username: customESTUsername, + password: customESTPassword, + }, + certAuthWithoutType + ), + }; + } + default: + throw new Error( + `Unknown certificate authority type: ${certAuthority.type}` + ); } }; @@ -191,69 +220,114 @@ export const updateFormData = ( ) => { const newData = { ...prevFormData, [update.name]: update.value }; - // for some inputs that change we want to reset one of the other inputs - // and force users to re-enter it. we only want to clear these values if it + // for some inputs that change we want to reset one or more of the other inputs + // and force users to re-enter them. we only want to clear these values if it // has not been updated. The characters "********" is the value the API sends - // back so we check for that value to determine if its been changed or not. - if (certAuthority.type === "digicert") { - const formData = prevFormData as IDigicertFormData; - if ( - update.name === "name" || - update.name === "url" || - update.name === "profileId" - ) { - return { - ...newData, - apiToken: formData.apiToken === "********" ? "" : formData.apiToken, - }; + // back so we check for that value to determine if it's been changed or not. + switch (certAuthority.type) { + case "digicert": { + const formData = prevFormData as IDigicertFormData; + if ( + update.name === "name" || + update.name === "url" || + update.name === "profileId" + ) { + return { + ...newData, + apiToken: + formData.apiToken === UNCHANGED_PASSWORD_API_RESPONSE + ? "" + : formData.apiToken, + }; + } + break; } - } else if (certAuthority.type === "ndes_scep_proxy") { - const formData = prevFormData as INDESFormData; - if (update.name === "adminURL" || update.name === "username") { - return { - ...newData, - password: formData.password === "********" ? "" : formData.password, - }; + case "ndes_scep_proxy": { + const formData = prevFormData as INDESFormData; + if (update.name === "adminURL" || update.name === "username") { + return { + ...newData, + password: + formData.password === UNCHANGED_PASSWORD_API_RESPONSE + ? "" + : formData.password, + }; + } + break; } - } else if (certAuthority.type === "custom_scep_proxy") { - const formData = prevFormData as ICustomSCEPFormData; - if (update.name === "name" || update.name === "scepURL") { - return { - ...newData, - challenge: formData.challenge === "********" ? "" : formData.challenge, - }; + case "custom_scep_proxy": { + const formData = prevFormData as ICustomSCEPFormData; + if (update.name === "name" || update.name === "scepURL") { + return { + ...newData, + challenge: + formData.challenge === UNCHANGED_PASSWORD_API_RESPONSE + ? "" + : formData.challenge, + }; + } + break; } - } else if (certAuthority.type === "hydrant") { - // for Hydrant, we reset clientId and clientSecret if name or url changes - // and the fields have not been updated. We do this to force users to send - // the correct clientId and clientSecret for the new name or url. - const formData = prevFormData as IHydrantFormData; - if (update.name === "name" || update.name === "url") { - return { - ...newData, - clientId: - formData.clientId === certAuthority.client_id - ? "" - : formData.clientId, - clientSecret: - formData.clientSecret === "********" ? "" : formData.clientSecret, - }; + case "hydrant": { + // for Hydrant, we reset clientId and clientSecret if name or url changes + // and the fields have not been updated. We do this to force users to send + // the correct clientId and clientSecret for the new name or url. + const formData = prevFormData as IHydrantFormData; + if (update.name === "name" || update.name === "url") { + return { + ...newData, + clientId: + formData.clientId === certAuthority.client_id + ? "" + : formData.clientId, + clientSecret: + formData.clientSecret === UNCHANGED_PASSWORD_API_RESPONSE + ? "" + : formData.clientSecret, + }; + } + break; } - } else if (certAuthority.type === "smallstep") { - const formData = prevFormData as ISmallstepFormData; - if ( - update.name === "name" || - update.name === "scepURL" || - update.name === "challengeURL" || - update.name === "username" - ) { - return { - ...newData, - password: formData.password === "********" ? "" : formData.password, - }; + case "smallstep": { + const formData = prevFormData as ISmallstepFormData; + if ( + update.name === "name" || + update.name === "scepURL" || + update.name === "challengeURL" || + update.name === "username" + ) { + return { + ...newData, + password: + formData.password === UNCHANGED_PASSWORD_API_RESPONSE + ? "" + : formData.password, + }; + } + break; } + case "custom_est_proxy": { + const formData = prevFormData as ICustomESTFormData; + if (update.name === "url") { + return { + ...newData, + username: + formData.username === certAuthority.username + ? "" + : formData.username, + password: + formData.password === UNCHANGED_PASSWORD_API_RESPONSE + ? "" + : formData.password, + }; + } + break; + } + default: + throw new Error( + `Unknown certificate authority type: ${certAuthority.type}` + ); } - return newData; }; diff --git a/frontend/services/entities/certificates.ts b/frontend/services/entities/certificates.ts index bdfcaced27..6ea5db064d 100644 --- a/frontend/services/entities/certificates.ts +++ b/frontend/services/entities/certificates.ts @@ -8,6 +8,7 @@ import { ICertificatesHydrant, ICertificatesNDES, ICertificatesSmallstep, + ICertificatesCustomEST, } from "interfaces/certificates"; type IGetCertAuthoritiesListResponse = { @@ -27,14 +28,16 @@ export type IAddCertAuthorityBody = | { ndes_scep_proxy: ICertificatesNDES } | { custom_scep_proxy: ICertificatesCustomSCEP } | { hydrant: ICertificatesHydrant } - | { smallstep: ICertificatesSmallstep }; + | { smallstep: ICertificatesSmallstep } + | { custom_est_proxy: ICertificatesCustomEST }; export type IEditCertAuthorityBody = | { digicert: Partial } | { ndes_scep_proxy: Partial } | { custom_scep_proxy: Partial } | { hydrant: Partial } - | { smallstep: Partial }; + | { smallstep: Partial } + | { custom_est_proxy: Partial }; export default { getCertificateAuthoritiesList: (): Promise => {