Update Add, Edit, and Delete Certificate Authority modals to support Custom EST (#35085)

**Related issue:** Resolves #34276 

<img width="1241" height="924" alt="Screenshot 2025-10-31 at 5 21 57 PM"
src="https://github.com/user-attachments/assets/44d94842-c4d0-4770-9072-6a87da2ae6cb"
/>


![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
This commit is contained in:
jacobshandling 2025-11-04 15:06:07 -08:00 committed by GitHub
parent 5267395778
commit 50e7947b67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 700 additions and 158 deletions

View file

@ -0,0 +1 @@
* Support Custom EST certificate authorities

View file

@ -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",

View file

@ -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
);
};

View file

@ -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

View file

@ -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{" "}
<CustomLink
url={paths.CONTROLS_CUSTOM_SETTINGS}
text="Controls > OS Settings > Custom"
/>{" "}
settings to configure how certificates are delivered to your hosts.{" "}
certificate authority.{" "}
<CustomLink
text="Learn more"
url="https://fleetdm.com/learn-more-about/certificate-authorities"

View file

@ -28,13 +28,17 @@ import { IHydrantFormData } from "../HydrantForm/HydrantForm";
import SmallstepForm, {
ISmallstepFormData,
} from "../SmallstepForm/SmallstepForm";
import CustomESTForm, {
ICustomESTFormData,
} from "../CustomESTForm/CustomESTForm";
export type ICertFormData =
| IDigicertFormData
| IHydrantFormData
| INDESFormData
| ICustomSCEPFormData
| ISmallstepFormData;
| ISmallstepFormData
| ICustomESTFormData;
const baseClass = "add-cert-authority-modal";
@ -94,6 +98,16 @@ const AddCertAuthorityModal = ({
password: "",
});
const [
customESTFormData,
setCustomESTFormData,
] = useState<ICustomESTFormData>({
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 (
<CustomESTForm
formData={customESTFormData}
certAuthorities={certAuthorities}
submitBtnText={submitBtnText}
isSubmitting={isAdding}
onChange={onChangeForm}
onSubmit={onAddCertAuthority}
onCancel={onExit}
/>
);
default:
return null;
}

View file

@ -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}`
);
}
};

View file

@ -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";
}

View file

@ -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<ICustomESTFormData>) => ({
name: "TEST_NAME",
url: "https://test.com",
username: "testuser",
password: "testpassword",
...overrides,
});
describe("CustomESTForm", () => {
it("render the custom button text", () => {
render(
<CustomESTForm
formData={createTestFormData()}
isSubmitting={false}
submitBtnText="Submit"
onChange={noop}
onSubmit={noop}
onCancel={noop}
/>
);
expect(screen.getByRole("button", { name: "Submit" })).toBeVisible();
});
it("enables submission depending on the form validation", async () => {
const testData = createTestFormData();
render(
<CustomESTForm
formData={testData}
isSubmitting={false}
submitBtnText="Submit"
onChange={noop}
onSubmit={noop}
onCancel={noop}
/>
);
// 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(
<CustomESTForm
formData={testData}
isSubmitting={false}
submitBtnText="Submit"
onChange={noop}
onSubmit={noop}
onCancel={noop}
/>
);
// 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(
<CustomESTForm
formData={createTestFormData()}
isSubmitting
submitBtnText="Submit"
onChange={noop}
onSubmit={noop}
onCancel={noop}
/>
);
expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
});
it("submit button is disabled if isDirty is false", () => {
render(
<CustomESTForm
formData={createTestFormData()}
isSubmitting={false}
submitBtnText="Submit"
isDirty={false}
onChange={noop}
onSubmit={noop}
onCancel={noop}
/>
);
expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
});
it("submit button is enabled if isDirty", () => {
render(
<CustomESTForm
formData={createTestFormData()}
isSubmitting={false}
submitBtnText="Submit"
isDirty
onChange={noop}
onSubmit={noop}
onCancel={noop}
/>
);
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
});
});

View file

@ -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<HTMLFormElement>) => {
evt.preventDefault();
onSubmit();
};
return (
<form onSubmit={onSubmitForm}>
<InputField
label="Name"
name="name"
value={name}
error={validations.name?.message}
onChange={onChange}
parseTarget
placeholder="WIFI_CERTIFICATE"
helpText="Letters, numbers, and underscores only."
/>
<InputField
label="URL"
name="url"
value={url}
error={validations.url?.message}
onChange={onChange}
parseTarget
placeholder="https://example.com/well-known/est/abc123"
/>
<InputField
label="Username"
name="username"
value={username}
error={validations.username?.message}
onChange={onChange}
parseTarget
helpText="The username used to authenticate with the EST endpoint."
/>
<InputField
type="password"
label="Password"
name="password"
value={password}
error={validations.password?.message}
onChange={onChange}
parseTarget
helpText="The password used to authenticate with the EST endpoint."
/>
<div className="modal-cta-wrap">
<TooltipWrapper
tipContent="Complete all required fields to save."
underline={false}
position="top"
disableTooltip={validations.isValid}
showArrow
>
<Button
type="submit"
isLoading={isSubmitting}
disabled={!validations.isValid || isSubmitting || !isDirty}
>
{submitBtnText}
</Button>
</TooltipWrapper>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
);
};
export default CustomESTForm;

View file

@ -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<ICustomESTFormValidation, "isValid">;
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;
};

View file

@ -0,0 +1 @@
export { default } from "./CustomESTForm";

View file

@ -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 = () => {

View file

@ -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;
};

View file

@ -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<ICertificatesDigicert> }
| { ndes_scep_proxy: Partial<ICertificatesNDES> }
| { custom_scep_proxy: Partial<ICertificatesCustomSCEP> }
| { hydrant: Partial<ICertificatesHydrant> }
| { smallstep: Partial<ICertificatesSmallstep> };
| { smallstep: Partial<ICertificatesSmallstep> }
| { custom_est_proxy: Partial<ICertificatesCustomEST> };
export default {
getCertificateAuthoritiesList: (): Promise<IGetCertAuthoritiesListResponse> => {