mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
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" />  - [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:
parent
5267395778
commit
50e7947b67
15 changed files with 700 additions and 158 deletions
1
changes/32110-custom-est-certificate-authorities
Normal file
1
changes/32110-custom-est-certificate-authorities
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Support Custom EST certificate authorities
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./CustomESTForm";
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue