add UI for adding, editing, deleting new NDES and custom scep cert authorities (#27270)

For #26607, #26608

This adds the ndes and custom scep forms to add those types of
certificate authorities. this includes:

**form for adding and editing ndes**


![image](https://github.com/user-attachments/assets/2effb143-d23b-4a87-948b-4732ddc5c29c)

**form for adding and editing custom scep**


![image](https://github.com/user-attachments/assets/212b496a-0f48-4b2b-aa72-aa482a4e0f6a)

This also contains the removal of the current ndes UI which was on the
mdm settings page

> NOTE: there will be another PR to handle the various error messages
and other polish to the UI.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
This commit is contained in:
Gabriel Hernandez 2025-03-20 16:14:53 +00:00 committed by GitHub
parent 1de78d40ab
commit 69165966c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 844 additions and 709 deletions

View file

@ -35,7 +35,6 @@ export interface ICertificatesIntegrationDigicert {
}
export interface ICertificatesIntegrationCustomSCEP {
id: number;
name: string;
url: string;
challenge: string;
@ -65,7 +64,9 @@ export const isDigicertCertIntegration = (
export const isCustomSCEPCertIntegration = (
integration: ICertificateIntegration
): integration is ICertificatesIntegrationCustomSCEP => {
return "id" in integration && "challenge" in integration;
return (
"name" in integration && "url" in integration && "challenge" in integration
);
};
export type ICertificateAuthorityType = "ndes" | "digicert" | "custom";

View file

@ -36,7 +36,7 @@ const integrationSettingsNavItems: ISideNavItem<any>[] = [
// TODO: digicert update: add this back when the feature is ready
{
title: "Certificates",
urlSection: "certificate-authorities",
urlSection: "certificates",
path: PATHS.ADMIN_INTEGRATIONS_CERTIFICATE_AUTHORITIES,
Card: CertificateAuthorities,
},

View file

@ -1,4 +1,4 @@
import React, { useContext, useState } from "react";
import React, { useContext, useMemo, useState } from "react";
import { NotificationContext } from "context/notification";
import certificatesAPI from "services/entities/certificates";
@ -9,14 +9,20 @@ import { AppContext } from "context/app";
import Dropdown from "components/forms/fields/Dropdown";
import Modal from "components/Modal";
import { generateErrorMessage } from "./helpers";
import { generateDropdownOptions, getErrorMessage } from "./helpers";
import DigicertForm from "../DigicertForm";
import { IDigicertFormData } from "../DigicertForm/DigicertForm";
import { useCertAuthorityDataGenerator } from "../DeleteCertificateAuthorityModal/helpers";
import NDESForm from "../NDESForm";
import { INDESFormData } from "../NDESForm/NDESForm";
import CustomSCEPForm from "../CustomSCEPForm";
import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm";
export type ICertFormData = IDigicertFormData;
// | IAnotherCertFormData
// | IYetAnotherCertFormData;
export type ICertFormData =
| IDigicertFormData
| INDESFormData
| ICustomSCEPFormData;
const baseClass = "add-cert-authority-modal";
@ -25,14 +31,14 @@ interface IAddCertAuthorityModalProps {
}
const AddCertAuthorityModal = ({ onExit }: IAddCertAuthorityModalProps) => {
const { setConfig } = useContext(AppContext);
const { config, setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const [
certAuthorityType,
setCertAuthorityType,
] = useState<ICertificateAuthorityType>("digicert");
const [isAdding, setIsAdding] = useState(false);
const [formData, setFormData] = useState<IDigicertFormData>({
const [digicertFormData, setDigicertFormData] = useState<IDigicertFormData>({
name: "",
url: "https://one.digicert.com",
apiToken: "",
@ -41,6 +47,21 @@ const AddCertAuthorityModal = ({ onExit }: IAddCertAuthorityModalProps) => {
userPrincipalName: "",
certificateSeatId: "",
});
const [ndesFormData, setNDESFormData] = useState<INDESFormData>({
scepURL: "",
adminURL: "",
username: "",
password: "",
});
const [
customSCEPFormData,
setCustomSCEPFormData,
] = useState<ICustomSCEPFormData>({
name: "",
scepURL: "",
challenge: "",
});
const { generateAddPatchData } = useCertAuthorityDataGenerator(
certAuthorityType
);
@ -50,10 +71,47 @@ const AddCertAuthorityModal = ({ onExit }: IAddCertAuthorityModalProps) => {
};
const onChangeForm = (update: { name: string; value: string }) => {
setFormData({ ...formData, [update.name]: update.value });
let setFormData;
let formData: ICertFormData;
switch (certAuthorityType) {
case "digicert":
setFormData = setDigicertFormData;
formData = digicertFormData;
break;
case "ndes":
setFormData = setNDESFormData;
formData = ndesFormData;
break;
case "custom":
setFormData = setCustomSCEPFormData;
formData = customSCEPFormData;
break;
default:
return;
}
(setFormData as React.Dispatch<React.SetStateAction<ICertFormData>>)({
...formData,
[update.name]: update.value,
});
};
const onAddCertAuthority = async () => {
let formData: ICertFormData;
switch (certAuthorityType) {
case "digicert":
formData = digicertFormData;
break;
case "ndes":
formData = ndesFormData;
break;
case "custom":
formData = customSCEPFormData;
break;
default:
return;
}
const addPatchData = generateAddPatchData(formData);
setIsAdding(true);
try {
@ -64,34 +122,74 @@ const AddCertAuthorityModal = ({ onExit }: IAddCertAuthorityModalProps) => {
onExit();
setConfig(newConfig);
} catch (e) {
renderFlash("error", generateErrorMessage(e));
renderFlash("error", getErrorMessage(e));
}
setIsAdding(false);
};
const dropdownOptions = useMemo(() => {
return generateDropdownOptions(!!config?.integrations.ndes_scep_proxy);
}, [config?.integrations.ndes_scep_proxy]);
const renderForm = () => {
const submitBtnText = "Add CA";
switch (certAuthorityType) {
case "digicert":
return (
<DigicertForm
formData={digicertFormData}
submitBtnText={submitBtnText}
isSubmitting={isAdding}
onChange={onChangeForm}
onSubmit={onAddCertAuthority}
onCancel={onExit}
/>
);
case "ndes":
return (
<NDESForm
formData={ndesFormData}
submitBtnText={submitBtnText}
isSubmitting={isAdding}
onChange={onChangeForm}
onSubmit={onAddCertAuthority}
onCancel={onExit}
/>
);
case "custom":
return (
<CustomSCEPForm
formData={customSCEPFormData}
submitBtnText={submitBtnText}
isSubmitting={isAdding}
onChange={onChangeForm}
onSubmit={onAddCertAuthority}
onCancel={onExit}
/>
);
default:
return null;
}
};
return (
<Modal
className={baseClass}
title="Add certificate authority (CA)"
width="large"
onExit={onExit}
isContentDisabled={isAdding}
>
<>
<Dropdown
options={[{ label: "Digicert", value: "digicert" }]}
options={dropdownOptions}
value={certAuthorityType}
className={`${baseClass}__cert-authority-dropdown`}
onChange={onChangeDropdown}
searchable={false}
/>
<DigicertForm
formData={formData}
submitBtnText="Add CA"
isSubmitting={isAdding}
onChange={onChangeForm}
onSubmit={onAddCertAuthority}
onCancel={onExit}
/>
{renderForm()}
</>
</Modal>
);

View file

@ -1,7 +1,33 @@
import { IDropdownOption } from "interfaces/dropdownOption";
const DEFAULT_CERT_AUTHORITY_OPTIONS: IDropdownOption[] = [
{ label: "Digicert", value: "digicert" },
{
label: "Microsoft NDES (Network Device Enrollment Service)",
value: "ndes",
},
{
label: "Custom (SCEP: Simple Certificate Enrollment Protocol)",
value: "custom",
},
];
export const generateDropdownOptions = (hasNDESCert: boolean) => {
if (!hasNDESCert) {
return DEFAULT_CERT_AUTHORITY_OPTIONS;
}
const ndesOption = DEFAULT_CERT_AUTHORITY_OPTIONS[1];
ndesOption.disabled = true;
ndesOption.tooltipContent = "Only one NDES can be added.";
return DEFAULT_CERT_AUTHORITY_OPTIONS;
};
const DEFAULT_ERROR_MESSAGE =
"Couldn't add certificate authority. Please try again.";
// eslint-disable-next-line import/prefer-default-export
export const generateErrorMessage = (e: unknown) => {
export const getErrorMessage = (e: unknown) => {
return DEFAULT_ERROR_MESSAGE;
};

View file

@ -1,11 +1,9 @@
.certificate-authority-list {
.list-item__actions {
li .list-item__actions {
display: none;
}
&:hover {
.list-item__actions {
display: flex;
}
li:hover .list-item__actions {
display: flex;
}
}

View file

@ -0,0 +1,113 @@
import React, { useState } from "react";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Button from "components/buttons/Button";
import TooltipWrapper from "components/TooltipWrapper";
import { ICustomSCEPFormValidation, validateFormData } from "./helpers";
const baseClass = "ndes-form";
export interface ICustomSCEPFormData {
name: string;
scepURL: string;
challenge: string;
}
interface ICustomSCEPFormProps {
formData: ICustomSCEPFormData;
submitBtnText: string;
isSubmitting: boolean;
onChange: (update: { name: string; value: string }) => void;
onSubmit: () => void;
onCancel: () => void;
}
const CustomSCEPForm = ({
formData,
submitBtnText,
isSubmitting,
onChange,
onSubmit,
onCancel,
}: ICustomSCEPFormProps) => {
const [
formValidation,
setFormValidation,
] = useState<ICustomSCEPFormValidation>({
isValid: false,
});
const { name, scepURL, challenge } = formData;
const onSubmitForm = (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onSubmit();
};
const onInputChange = (update: { name: string; value: string }) => {
setFormValidation(
validateFormData({ ...formData, [update.name]: update.value })
);
onChange(update);
};
return (
<form onSubmit={onSubmitForm}>
<div className={`${baseClass}__fields`}>
<InputField
label="Name"
name="name"
value={name}
error={formValidation.name?.message}
onChange={onInputChange}
parseTarget
placeholder="SCEP_WIFI"
helpText="Letters, numbers, and underscores only. Fleet will create configuration profile variables with the name as suffix (e.g. $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_SCEP_WIFI)."
/>
<InputField
label="SCEP URL"
name="scepURL"
value={scepURL}
error={formValidation.scepURL?.message}
onChange={onInputChange}
parseTarget
placeholder="https://example.com/scep"
/>
<InputField
type="password"
label="Challenge"
name="challenge"
value={challenge}
onChange={onInputChange}
parseTarget
placeholder="••••••••••••"
helpText="Password to authenticate with a SCEP server."
/>
</div>
<div className={`${baseClass}__cta`}>
<TooltipWrapper
tipContent="Complete all required fields to save."
underline={false}
position="top"
disableTooltip={formValidation.isValid}
showArrow
>
<Button
type="submit"
isLoading={isSubmitting}
disabled={!formValidation.isValid || isSubmitting}
>
{submitBtnText}
</Button>
</TooltipWrapper>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
);
};
export default CustomSCEPForm;

View file

@ -0,0 +1,13 @@
.custom-scep-form {
&__fields {
display: flex;
flex-direction: column;
gap: $pad-large;
}
&__cta {
display: flex;
flex-direction: row-reverse;
gap: $pad-medium;
}
}

View file

@ -0,0 +1,148 @@
import valid_url from "components/forms/validators/valid_url";
import { ICustomSCEPFormData } from "./CustomSCEPForm";
// TODO: create a validator abstraction for this and the other form validation files
export interface ICustomSCEPFormValidation {
isValid: boolean;
name?: { isValid: boolean; message?: string };
scepURL?: { isValid: boolean; message?: string };
challenge?: { isValid: boolean };
}
type IMessageFunc = (formData: ICustomSCEPFormData) => string;
type IValidationMessage = string | IMessageFunc;
type IFormValidationKey = keyof Omit<ICustomSCEPFormValidation, "isValid">;
interface IValidation {
name: string;
isValid: (formData: ICustomSCEPFormData) => boolean;
message?: IValidationMessage;
}
const FORM_VALIDATIONS: Record<
IFormValidationKey,
{ validations: IValidation[] }
> = {
name: {
validations: [
{
name: "required",
isValid: (formData: ICustomSCEPFormData) => {
return formData.name.length > 0;
},
},
{
name: "invalidCharacters",
isValid: (formData: ICustomSCEPFormData) => {
return /^[a-zA-Z0-9_]+$/.test(formData.name);
},
message:
"Inalid characters. Only letters, numbers and underscores allowed.",
},
],
},
scepURL: {
validations: [
{
name: "required",
isValid: (formData: ICustomSCEPFormData) => {
return formData.scepURL.length > 0;
},
},
{
name: "validUrl",
isValid: (formData: ICustomSCEPFormData) => {
return valid_url({ url: formData.scepURL });
},
message: (formData: ICustomSCEPFormData) =>
`${formData.scepURL} is not a valid URL`,
},
],
},
challenge: {
validations: [
{
name: "required",
isValid: (formData: ICustomSCEPFormData) => {
return formData.challenge.length > 0;
},
},
],
},
};
const getErrorMessage = (
formData: ICustomSCEPFormData,
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: ICustomSCEPFormData) => {
const formValidation: ICustomSCEPFormValidation = {
isValid: true,
};
Object.keys(FORM_VALIDATIONS).forEach((key) => {
const objKey = key as keyof typeof FORM_VALIDATIONS;
const failedValidation = FORM_VALIDATIONS[objKey].validations.find(
(validation) => !validation.isValid(formData)
);
if (!failedValidation) {
formValidation[objKey] = {
isValid: true,
};
} else {
formValidation.isValid = false;
formValidation[objKey] = {
isValid: false,
message: getErrorMessage(formData, failedValidation.message),
};
}
});
return formValidation;
};
const BAD_SCEP_URL_ERROR = "Invalid SCEP URL. Please correct and try again.";
const BAD_CREDENTIALS_ERROR =
"Couldn't add. Admin URL or credentials are invalid.";
const CACHE_ERROR =
"The NDES password cache is full. Please increase the number of cached passwords in NDES and try again. By default, NDES caches 5 passwords and they expire 60 minutes after they are created.";
const INSUFFICIENT_PERMISSIONS_ERROR =
"Couldn't add. This account doesn't have sufficient permissions. Please use the account with enroll permission.";
const SCEP_URL_TIMEOUT_ERROR =
"Couldn't add. Request to NDES (SCEP URL) timed out. Please try again.";
const DEFAULT_ERROR =
"Something went wrong updating your SCEP server. Please try again.";
// export const getErrorMessage = (
// err: unknown,
// formData: ICustomSCEPFormData
// ) => {
// const reason = getErrorReason(err);
// if (reason.includes("invalid admin URL or credentials")) {
// return BAD_CREDENTIALS_ERROR;
// } else if (reason.includes("the password cache is full")) {
// return CACHE_ERROR;
// } else if (reason.includes("does not have sufficient permissions")) {
// INSUFFICIENT_PERMISSIONS_ERROR;
// } else if (
// reason.includes(formData.scepURL) &&
// reason.includes("context deadline exceeded")
// ) {
// return SCEP_URL_TIMEOUT_ERROR;
// } else if (reason.includes("invalid SCEP URL")) {
// return BAD_SCEP_URL_ERROR;
// }
// return DEFAULT_ERROR;
// };

View file

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

View file

@ -9,6 +9,9 @@ import {
} from "interfaces/integration";
import { useCallback, useContext } from "react";
import { IDigicertFormData } from "../DigicertForm/DigicertForm";
import { ICertFormData } from "../AddCertAuthorityModal/AddCertAuthorityModal";
import { INDESFormData } from "../NDESForm/NDESForm";
import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm";
export const useCertAuthorityDataGenerator = (
certAuthorityType: ICertificateAuthorityType,
@ -17,7 +20,7 @@ export const useCertAuthorityDataGenerator = (
const { config } = useContext(AppContext);
const generateAddPatchData = useCallback(
(formData: IDigicertFormData) => {
(formData: ICertFormData) => {
if (!config) return null;
const data: { integrations: Partial<IGlobalIntegrations> } = {
@ -26,22 +29,59 @@ export const useCertAuthorityDataGenerator = (
switch (certAuthorityType) {
case "ndes":
// eslint-disable-next-line no-case-declarations
const {
scepURL: ndesSCEPUrl,
adminURL,
username,
password,
} = formData as INDESFormData;
data.integrations.ndes_scep_proxy = {
url: ndesSCEPUrl,
admin_url: adminURL,
username,
password,
};
break;
case "digicert":
// eslint-disable-next-line no-case-declarations
const {
name: digicertName,
url,
apiToken,
profileId,
commonName,
userPrincipalName,
certificateSeatId,
} = formData as IDigicertFormData;
data.integrations.digicert = [
...(config.integrations.digicert || []),
{
name: formData.name,
url: formData.url,
api_token: formData.apiToken,
profile_id: formData.profileId,
certificate_common_name: formData.commonName,
certificate_user_principal_names: [formData.userPrincipalName],
certificate_seat_id: formData.certificateSeatId,
name: digicertName,
url,
api_token: apiToken,
profile_id: profileId,
certificate_common_name: commonName,
certificate_user_principal_names: [userPrincipalName],
certificate_seat_id: certificateSeatId,
},
];
break;
case "custom":
// eslint-disable-next-line no-case-declarations
const {
name: customSCEPName,
scepURL: customSCEPUrl,
challenge,
} = formData as ICustomSCEPFormData;
data.integrations.custom_scep_proxy = [
...(config.integrations.custom_scep_proxy || []),
{
name: customSCEPName,
url: customSCEPUrl,
challenge,
},
];
break;
default:
break;
@ -82,8 +122,8 @@ export const useCertAuthorityDataGenerator = (
data.integrations.custom_scep_proxy = config.integrations.custom_scep_proxy?.filter(
(cert) => {
return (
(certAuthority as ICertificatesIntegrationCustomSCEP).id ===
cert.id
(certAuthority as ICertificatesIntegrationCustomSCEP).name !==
cert.name
);
}
);
@ -101,7 +141,7 @@ export const useCertAuthorityDataGenerator = (
* have to generate the correct data for the PATCH request.
*/
const generateEditPatchData = useCallback(
(formData: IDigicertFormData) => {
(formData: ICertFormData) => {
if (!config) return null;
const data: { integrations: Partial<IGlobalIntegrations> } = {
@ -110,8 +150,31 @@ export const useCertAuthorityDataGenerator = (
switch (certAuthorityType) {
case "ndes":
// eslint-disable-next-line no-case-declarations
const {
scepURL: ndesSCEPUrl,
adminURL,
username,
password,
} = formData as INDESFormData;
data.integrations.ndes_scep_proxy = {
url: ndesSCEPUrl,
admin_url: adminURL,
username,
password,
};
break;
case "digicert":
// eslint-disable-next-line no-case-declarations
const {
name: digicertName,
url,
apiToken,
profileId,
commonName,
userPrincipalName,
certificateSeatId,
} = formData as IDigicertFormData;
data.integrations.digicert = config.integrations.digicert?.map(
(cert) => {
// only update the certificate authority that we are editing
@ -120,15 +183,13 @@ export const useCertAuthorityDataGenerator = (
cert.name
) {
return {
name: formData.name,
url: formData.url,
api_token: formData.apiToken,
profile_id: formData.profileId,
certificate_common_name: formData.commonName,
certificate_user_principal_names: [
formData.userPrincipalName,
],
certificate_seat_id: formData.certificateSeatId,
name: digicertName,
url,
api_token: apiToken,
profile_id: profileId,
certificate_common_name: commonName,
certificate_user_principal_names: [userPrincipalName],
certificate_seat_id: certificateSeatId,
};
}
return cert;
@ -136,6 +197,28 @@ export const useCertAuthorityDataGenerator = (
);
break;
case "custom":
// eslint-disable-next-line no-case-declarations
const {
name: customSCEPName,
scepURL: customSCEPUrl,
challenge,
} = formData as ICustomSCEPFormData;
data.integrations.custom_scep_proxy = config.integrations.custom_scep_proxy?.map(
(cert) => {
// only update the certificate authority that we are editing
if (
(certAuthority as ICertificatesIntegrationCustomSCEP).name ===
cert.name
) {
return {
name: customSCEPName,
url: customSCEPUrl,
challenge,
};
}
return cert;
}
);
break;
default:
break;

View file

@ -5,19 +5,23 @@ import { AppContext } from "context/app";
import {
ICertificateIntegration,
isDigicertCertIntegration,
isNDESCertIntegration,
} from "interfaces/integration";
import certificatesAPI from "services/entities/certificates";
import Modal from "components/Modal";
import DigicertForm from "../DigicertForm";
import {
generateDefaultFormData,
generateErrorMessage,
getCertificateAuthorityType,
} from "./helpers";
import DigicertForm from "../DigicertForm";
import { ICertFormData } from "../AddCertAuthorityModal/AddCertAuthorityModal";
import { useCertAuthorityDataGenerator } from "../DeleteCertificateAuthorityModal/helpers";
import NDESForm from "../NDESForm";
import CustomSCEPForm from "../CustomSCEPForm";
const baseClass = "edit-cert-authority-modal";
@ -70,10 +74,13 @@ const EditCertAuthorityModal = ({
};
const getFormComponent = () => {
if (isNDESCertIntegration(certAuthority)) {
return NDESForm;
}
if (isDigicertCertIntegration(certAuthority)) {
return DigicertForm;
}
return null;
return CustomSCEPForm;
};
const renderForm = () => {
@ -82,6 +89,7 @@ const EditCertAuthorityModal = ({
return (
<FormComponent
// @ts-ignore TODO: figure out how to fix this type issue
formData={formData}
submitBtnText="Save"
isSubmitting={isUpdating}
@ -98,6 +106,7 @@ const EditCertAuthorityModal = ({
title="Edit certificate authority (CA)"
width="large"
onExit={onExit}
isContentDisabled={isUpdating}
>
{renderForm()}
</Modal>

View file

@ -1,8 +1,11 @@
import {
ICertificateAuthorityType,
ICertificateIntegration,
ICertificatesIntegrationCustomSCEP,
ICertificatesIntegrationDigicert,
ICertificatesIntegrationNDES,
isCustomSCEPCertIntegration,
isDigicertCertIntegration,
isNDESCertIntegration,
} from "interfaces/integration";
@ -16,21 +19,6 @@ export const generateErrorMessage = (e: unknown) => {
return DEFAULT_ERROR_MESSAGE;
};
export const generateDefaultFormData = (
certAuthority: ICertificateIntegration
): ICertFormData => {
const cert = certAuthority as ICertificatesIntegrationDigicert;
return {
name: cert.name,
url: cert.url,
apiToken: cert.api_token,
profileId: cert.profile_id,
commonName: cert.certificate_common_name,
userPrincipalName: cert.certificate_user_principal_names[0],
certificateSeatId: cert.certificate_seat_id,
};
};
export const getCertificateAuthorityType = (
certAuthority: ICertificateIntegration
): ICertificateAuthorityType => {
@ -38,3 +26,33 @@ export const getCertificateAuthorityType = (
if (isCustomSCEPCertIntegration(certAuthority)) return "custom";
return "digicert";
};
export const generateDefaultFormData = (
certAuthority: ICertificateIntegration
): ICertFormData => {
if (isNDESCertIntegration(certAuthority)) {
return {
scepURL: certAuthority.url,
adminURL: certAuthority.admin_url,
username: certAuthority.username,
password: certAuthority.password,
};
} else if (isDigicertCertIntegration(certAuthority)) {
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,
};
}
const customSCEPcert = certAuthority as ICertificatesIntegrationCustomSCEP;
return {
name: customSCEPcert.name,
scepURL: customSCEPcert.url,
challenge: customSCEPcert.challenge,
};
};

View file

@ -0,0 +1,120 @@
import React, { useState } from "react";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Button from "components/buttons/Button";
import TooltipWrapper from "components/TooltipWrapper";
import { INDESFormValidation, validateFormData } from "./helpers";
const baseClass = "ndes-form";
export interface INDESFormData {
scepURL: string;
adminURL: string;
username: string;
password: string;
}
interface INDESFormProps {
formData: INDESFormData;
submitBtnText: string;
isSubmitting: boolean;
onChange: (update: { name: string; value: string }) => void;
onSubmit: () => void;
onCancel: () => void;
}
const NDESForm = ({
formData,
submitBtnText,
isSubmitting,
onChange,
onSubmit,
onCancel,
}: INDESFormProps) => {
const [formValidation, setFormValidation] = useState<INDESFormValidation>({
isValid: false,
});
const { scepURL, adminURL, username, password } = formData;
const onSubmitForm = (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onSubmit();
};
const onInputChange = (update: { name: string; value: string }) => {
setFormValidation(
validateFormData({ ...formData, [update.name]: update.value })
);
onChange(update);
};
return (
<form onSubmit={onSubmitForm}>
<div className={`${baseClass}__fields`}>
<InputField
label="SCEP URL"
name="scepURL"
value={scepURL}
onChange={onInputChange}
parseTarget
placeholder="https://example.com/certsrv/mscep/mscep.dll"
helpText="The URL used by client devices to request and retrieve certificates."
/>
<InputField
label="Admin URL"
name="adminURL"
value={adminURL}
onChange={onInputChange}
parseTarget
placeholder="https://example.com/certsrv/mscep_admin/"
helpText="The admin interface for managing the SCEP service and viewing configuration details."
/>
<InputField
label="Username"
name="username"
value={username}
onChange={onInputChange}
parseTarget
placeholder="username@example.microsoft.com"
helpText="The username in the down-level logon name format required to log in to the SCEP admin page."
/>
<InputField
label="Password"
name="password"
value={password}
type="password"
onChange={onInputChange}
parseTarget
placeholder="••••••••"
blockAutoComplete
helpText="The password required to log in to the SCEP admin page."
/>
</div>
<div className={`${baseClass}__cta`}>
<TooltipWrapper
tipContent="Complete all required fields to save."
underline={false}
position="top"
disableTooltip={formValidation.isValid}
showArrow
>
<Button
type="submit"
isLoading={isSubmitting}
disabled={!formValidation.isValid || isSubmitting}
>
{submitBtnText}
</Button>
</TooltipWrapper>
<Button variant="inverse" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
);
};
export default NDESForm;

View file

@ -0,0 +1,13 @@
.ndes-form {
&__fields {
display: flex;
flex-direction: column;
gap: $pad-large;
}
&__cta {
display: flex;
flex-direction: row-reverse;
gap: $pad-medium;
}
}

View file

@ -0,0 +1,135 @@
import { getErrorReason } from "interfaces/errors";
import { INDESFormData } from "./NDESForm";
// TODO: create a validator abstraction for this and the other form validation files
export interface INDESFormValidation {
isValid: boolean;
scepURL?: { isValid: boolean };
adminURL?: { isValid: boolean };
username?: { isValid: boolean };
password?: { isValid: boolean };
}
type IMessageFunc = (formData: INDESFormData) => string;
type IValidationMessage = string | IMessageFunc;
type IFormValidationKey = keyof Omit<INDESFormValidation, "isValid">;
interface IValidation {
name: string;
isValid: (formData: INDESFormData) => boolean;
message?: IValidationMessage;
}
const FORM_VALIDATIONS: Record<
IFormValidationKey,
{ validations: IValidation[] }
> = {
scepURL: {
validations: [
{
name: "required",
isValid: (formData: INDESFormData) => {
return formData.scepURL.length > 0;
},
},
],
},
adminURL: {
validations: [
{
name: "required",
isValid: (formData: INDESFormData) => {
return formData.adminURL.length > 0;
},
},
],
},
username: {
validations: [
{
name: "required",
isValid: (formData: INDESFormData) => {
return formData.username.length > 0;
},
},
],
},
password: {
validations: [
{
name: "required",
isValid: (formData: INDESFormData) => {
return formData.password.length > 0;
},
},
],
},
};
// eslint-disable-next-line import/prefer-default-export
export const validateFormData = (formData: INDESFormData) => {
const formValidation: INDESFormValidation = {
isValid: true,
};
Object.keys(FORM_VALIDATIONS).forEach((key) => {
const objKey = key as keyof typeof FORM_VALIDATIONS;
const failedValidation = FORM_VALIDATIONS[objKey].validations.find(
(validation) => !validation.isValid(formData)
);
if (!failedValidation) {
formValidation[objKey] = {
isValid: true,
};
} else {
formValidation.isValid = false;
formValidation[objKey] = {
isValid: false,
};
}
});
return formValidation;
};
const BAD_SCEP_URL_ERROR = "Invalid SCEP URL. Please correct and try again.";
const BAD_CREDENTIALS_ERROR =
"Couldn't add. Admin URL or credentials are invalid.";
const CACHE_ERROR =
"The NDES password cache is full. Please increase the number of cached passwords in NDES and try again. By default, NDES caches 5 passwords and they expire 60 minutes after they are created.";
const INSUFFICIENT_PERMISSIONS_ERROR =
"Couldn't add. This account doesn't have sufficient permissions. Please use the account with enroll permission.";
const SCEP_URL_TIMEOUT_ERROR =
"Couldn't add. Request to NDES (SCEP URL) timed out. Please try again.";
const ADMIN_URL_TIMEOUT_ERROR =
"Couldn't add. Request to NDES (admin URL) timed out. Please try again.";
const DEFAULT_ERROR =
"Something went wrong updating your SCEP server. Please try again.";
export const getErrorMessage = (err: unknown, formData: INDESFormData) => {
const reason = getErrorReason(err);
if (reason.includes("invalid admin URL or credentials")) {
return BAD_CREDENTIALS_ERROR;
} else if (reason.includes("the password cache is full")) {
return CACHE_ERROR;
} else if (reason.includes("does not have sufficient permissions")) {
INSUFFICIENT_PERMISSIONS_ERROR;
} else if (
reason.includes(formData.scepURL) &&
reason.includes("context deadline exceeded")
) {
return SCEP_URL_TIMEOUT_ERROR;
} else if (
reason.includes(formData.adminURL) &&
reason.includes("context deadline exceeded")
) {
return ADMIN_URL_TIMEOUT_ERROR;
} else if (reason.includes("invalid SCEP URL")) {
return BAD_SCEP_URL_ERROR;
}
return DEFAULT_ERROR;
};

View file

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

View file

@ -2,7 +2,6 @@ import {
ICertificatesIntegrationCustomSCEP,
ICertificatesIntegrationDigicert,
ICertificatesIntegrationNDES,
isNDESCertIntegration,
} from "interfaces/integration";
export interface ICertAuthorityListData {
@ -40,7 +39,7 @@ export const generateListData = (
if (customProxies?.length) {
customProxies.forEach((cert) => {
listData.push({
id: `custom-scep-proxy-${cert.id}`,
id: `custom-scep-proxy-${cert.name}`,
name: cert.name,
description: "Custom Simple Certificate Enrollment Protocol (SCEP)",
});
@ -91,8 +90,7 @@ export const getCertificateAuthority = (
if (id.includes("custom-scep-proxy") && customProxies) {
return (
customProxies?.find(
// TODO: remove custom scep id
(cert) => id.split("custom-scep-proxy-")[1] === cert.id.toString()
(cert) => id.split("custom-scep-proxy-")[1] === cert.name
) ?? null
);
}

View file

@ -133,12 +133,6 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => {
isVppOn={!noVppTokenUploaded}
isPremiumTier={!!isPremiumTier}
/>
{/* TODO: digicert update: remove scep section when digicert feature is ready */}
<ScepSection
router={router}
isScepOn={!noScepCredentials}
isPremiumTier={!!isPremiumTier}
/>
{isPremiumTier && !!config?.mdm.apple_bm_enabled_and_configured && (
<>
<IdpSection />

View file

@ -1,135 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { createMockRouter } from "test/test-utils";
import createMockConfig, { createMockMdmConfig } from "__mocks__/configMock";
import { ScepCertificateContent } from "./ScepPage";
const FORM_DATA = { scepUrl: "", adminUrl: "", username: "", password: "" };
describe("Scep Page", () => {
it("renders PremiumFeatureMessage for non-premium tier", () => {
render(
<ScepCertificateContent
router={createMockRouter()}
onFormSubmit={jest.fn()}
formData={FORM_DATA}
formErrors={{}}
onInputChange={jest.fn()}
onBlur={jest.fn()}
config={createMockConfig()}
isLoading={false}
isSaving={false}
showDataError={false}
isPremiumTier={false} // test
/>
);
expect(
screen.getByText("This feature is included in Fleet Premium.")
).toBeInTheDocument();
});
it("renders TurnOnMdmMessage when MDM is not enabled", () => {
render(
<ScepCertificateContent
router={createMockRouter()}
onFormSubmit={jest.fn()}
formData={FORM_DATA}
formErrors={{}}
onInputChange={jest.fn()}
onBlur={jest.fn()}
config={createMockConfig({
mdm: createMockMdmConfig({ enabled_and_configured: false }),
})}
isLoading={false}
isSaving={false}
showDataError={false}
isPremiumTier
/>
);
expect(screen.getByText("Turn on Apple MDM")).toBeInTheDocument();
});
it("renders Spinner when loading", () => {
render(
<ScepCertificateContent
router={createMockRouter()}
onFormSubmit={jest.fn()}
formData={FORM_DATA}
formErrors={{}}
onInputChange={jest.fn()}
onBlur={jest.fn()}
config={createMockConfig()}
isLoading // test
isSaving={false}
showDataError={false}
isPremiumTier
/>
);
expect(screen.getByTestId("spinner")).toBeInTheDocument();
});
it("renders DataError when showDataError is true", () => {
render(
<ScepCertificateContent
router={createMockRouter()}
onFormSubmit={jest.fn()}
formData={{ scepUrl: "", adminUrl: "", username: "", password: "" }}
formErrors={{}}
onInputChange={jest.fn()}
onBlur={jest.fn()}
config={createMockConfig()}
isLoading={false}
isSaving={false}
showDataError // test
isPremiumTier
/>
);
expect(screen.getByText("Something's gone wrong.")).toBeInTheDocument();
});
it("renders form fields correctly", () => {
render(
<ScepCertificateContent
router={createMockRouter()}
onFormSubmit={jest.fn()}
formData={FORM_DATA}
formErrors={{}}
onInputChange={jest.fn()}
onBlur={jest.fn()}
config={createMockConfig()}
isLoading={false}
isSaving={false}
showDataError={false}
isPremiumTier
/>
);
expect(screen.getByLabelText("SCEP URL")).toBeInTheDocument();
expect(screen.getByLabelText("Admin URL")).toBeInTheDocument();
expect(screen.getByLabelText("Username")).toBeInTheDocument();
expect(screen.getByLabelText("Password")).toBeInTheDocument();
});
it("displays error messages for invalid inputs", () => {
const FORM_ERRORS = { scepUrl: "Invalid URL", adminUrl: "Invalid URL" };
const INVALID_FORM_DATA = {
scepUrl: "invalid",
adminUrl: "invalid",
username: "",
password: "",
};
render(
<ScepCertificateContent
router={createMockRouter()}
onFormSubmit={jest.fn()}
formData={INVALID_FORM_DATA}
formErrors={FORM_ERRORS}
onInputChange={jest.fn()}
onBlur={jest.fn()}
config={createMockConfig()}
isLoading={false}
isSaving={false}
showDataError={false}
isPremiumTier
/>
);
expect(screen.getAllByLabelText("Invalid URL").length).toBe(2);
});
});

View file

@ -1,425 +0,0 @@
import React, { useContext, useState } from "react";
import { useQuery } from "react-query";
import { InjectedRouter } from "react-router";
import PATHS from "router/paths";
import configAPI from "services/entities/config";
import { IConfig } from "interfaces/config";
import { getErrorReason } from "interfaces/errors";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import MainContent from "components/MainContent/MainContent";
import Button from "components/buttons/Button";
import BackLink from "components/BackLink/BackLink";
import CustomLink from "components/CustomLink";
import Card from "components/Card";
import validateUrl from "components/forms/validators/valid_url";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import TooltipWrapper from "components/TooltipWrapper";
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { SCEP_SERVER_TIP_CONTENT } from "../components/ScepSection/ScepSection";
const baseClass = "scep-page";
const BAD_SCEP_URL_ERROR = "Invalid SCEP URL. Please correct and try again.";
const BAD_CREDENTIALS_ERROR =
"Couldn't add. Admin URL or credentials are invalid.";
const CACHE_ERROR =
"The NDES password cache is full. Please increase the number of cached passwords in NDES and try again. By default, NDES caches 5 passwords and they expire 60 minutes after they are created.";
const INSUFFICIENT_PERMISSIONS_ERROR =
"Couldn't add. This account doesn't have sufficient permissions. Please use the account with enroll permission.";
const SCEP_URL_TIMEOUT_ERROR =
"Couldn't add. Request to NDES (SCEP URL) timed out. Please try again.";
const ADMIN_URL_TIMEOUT_ERROR =
"Couldn't add. Request to NDES (admin URL) timed out. Please try again.";
const DEFAULT_ERROR =
"Something went wrong updating your SCEP server. Please try again.";
interface IScepCertificateContentProps {
router: InjectedRouter;
onFormSubmit: (evt: React.MouseEvent<HTMLFormElement>) => Promise<void>;
formData: INdesFormData;
formErrors: INdesFormErrors;
onInputChange: ({ name, value }: IFormField) => void;
onBlur: (name: string, value: string) => void;
config: IConfig | null;
isPremiumTier: boolean;
isLoading: boolean;
isSaving: boolean;
showDataError: boolean;
}
export const ScepCertificateContent = ({
router,
onFormSubmit,
formData,
formErrors,
onInputChange,
onBlur,
config,
isPremiumTier,
isLoading,
isSaving,
showDataError,
}: IScepCertificateContentProps) => {
if (!isPremiumTier) {
return <PremiumFeatureMessage />;
}
if (isLoading) {
return <Spinner />;
}
if (!config?.mdm.enabled_and_configured) {
return (
<TurnOnMdmMessage
router={router}
header="Turn on Apple MDM"
info="To help your end users connect to Wi-Fi, first turn on Apple MDM."
/>
);
}
// TODO: error UI
if (showDataError) {
return (
<div>
<DataError />
</div>
);
}
const gitOpsModeEnabled = config?.gitops.gitops_mode_enabled;
const disableSave =
// all fields aren't empty
!Object.values(formData).every((val) => val === "") &&
// all fields aren't complete
!Object.values(formData).every((val) => val !== "");
return (
<>
<p>
To help your end users connect to Wi-Fi you can add your{" "}
<TooltipWrapper tipContent={SCEP_SERVER_TIP_CONTENT}>
SCEP server
</TooltipWrapper>
.
</p>
<div>
<ol className={`${baseClass}__steps`}>
<li>
<div>
Connect to your Network Device Enrollment Service (
<CustomLink
url="https://www.fleetdm.com/learn-more-about/ndes"
text="NDES"
/>
) admin account:
</div>
<Card>
<form onSubmit={onFormSubmit} autoComplete="off">
<InputField
inputWrapperClass={`${baseClass}__scep-url-input`}
label="SCEP URL"
name="scepUrl"
tooltip={
<>
The URL used by client devices
<br /> to request and retrieve certificates.
</>
}
value={formData.scepUrl}
onChange={onInputChange}
parseTarget
error={formErrors.scepUrl}
placeholder="https://example.com/certsrv/mscep/mscep.dll"
disabled={gitOpsModeEnabled}
/>
<InputField
inputWrapperClass={`${baseClass}__admin-url-input`}
label="Admin URL"
name="adminUrl"
tooltip={
<>
The admin interface for managing the SCEP
<br /> service and viewing configuration details.
</>
}
value={formData.adminUrl}
onChange={onInputChange}
onBlur={(e: any) => onBlur("adminUrl", e.target.value)}
parseTarget
error={formErrors.adminUrl}
placeholder="https://example.com/certsrv/mscep_admin/"
disabled={gitOpsModeEnabled}
/>
<InputField
inputWrapperClass={`${baseClass}__username-input`}
label="Username"
name="username"
tooltip={
<>
The username in the down-level logon name format
<br />
required to log in to the SCEP admin page.
</>
}
value={formData.username}
onChange={onInputChange}
onBlur={(e: any) => onBlur("username", e.target.value)}
parseTarget
placeholder="username@example.microsoft.com"
disabled={gitOpsModeEnabled}
/>
<InputField
inputWrapperClass={`${baseClass}__password-input`}
label="Password"
name="password"
tooltip={
<>
The password to use to log in
<br />
to the SCEP admin page.
</>
}
value={formData.password || ""}
type="password"
onChange={onInputChange}
parseTarget
placeholder="••••••••"
blockAutoComplete
error={formErrors.password}
disabled={gitOpsModeEnabled}
/>
<GitOpsModeTooltipWrapper
tipOffset={-8}
renderChildren={(disableChildren) => (
<Button
type="submit"
variant="brand"
className="button-wrap"
isLoading={isSaving}
disabled={disableSave || disableChildren}
>
Save
</Button>
)}
/>
</form>
</Card>
</li>
<li>
<span>
Now head over to{" "}
<CustomLink
url={PATHS.CONTROLS_CUSTOM_SETTINGS}
text="Controls > OS Settings > Custom settings"
/>{" "}
to configure how SCEP certificates are delivered to your hosts.{" "}
<CustomLink
url="https://fleetdm.com/learn-more-about/setup-ndes"
text="Learn more"
newTab
/>
</span>
</li>
</ol>
</div>
</>
);
};
interface IScepPageProps {
router: InjectedRouter;
}
interface INdesFormData {
scepUrl: string;
adminUrl: string;
username: string;
password: string;
}
interface INdesFormErrors {
scepUrl?: string | null;
adminUrl?: string | null;
password?: string | null;
}
export interface IFormField {
name: string;
value: string;
}
const ScepPage = ({ router }: IScepPageProps) => {
const { isPremiumTier, config, setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const [formData, setFormData] = useState<INdesFormData>({
scepUrl: config?.integrations.ndes_scep_proxy?.url || "",
adminUrl: config?.integrations.ndes_scep_proxy?.admin_url || "",
username: config?.integrations.ndes_scep_proxy?.username || "",
password: config?.integrations.ndes_scep_proxy?.password || "",
});
const [formErrors, setFormErrors] = useState<INdesFormErrors>({});
const [isUpdatingNdesScepProxy, setIsUpdatingNdesScepProxy] = useState(false);
const {
data: appConfig,
isLoading: isLoadingAppConfig,
refetch: refetchConfig,
isError: isErrorAppConfig,
} = useQuery<IConfig, Error, IConfig>(["config"], () => configAPI.loadAll(), {
select: (data: IConfig) => data,
onSuccess: (data) => {
setConfig(data);
},
});
const onInputChange = ({ name, value }: IFormField) => {
setFormErrors((prev) => ({ ...prev, [name]: null }));
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleBlur = (name: string, value: string) => {
// If the value of admin url or username has changed and
// it was not originally empty, prompt user to re-enter password
if (
(name === "adminUrl" &&
value !== config?.integrations.ndes_scep_proxy?.admin_url &&
config?.integrations.ndes_scep_proxy?.admin_url !== "") ||
(name === "username" &&
value !== config?.integrations.ndes_scep_proxy?.username &&
config?.integrations.ndes_scep_proxy?.username !== "")
) {
setFormErrors((prev: INdesFormErrors) => ({
...prev,
password:
"Please re-enter your password due to changes in admin URL or username",
}));
setFormData((prev: INdesFormData) => ({ ...prev, password: "" }));
}
};
const onFormSubmit = async (evt: React.MouseEvent<HTMLFormElement>) => {
evt.preventDefault();
const scepUrlValid = validateUrl({ url: formData.scepUrl });
const adminUrlValid = validateUrl({ url: formData.adminUrl });
const newFormErrors = {
scepUrl:
scepUrlValid || formData.scepUrl === ""
? undefined
: "Must be a valid URL.",
adminUrl:
adminUrlValid || formData.adminUrl === ""
? undefined
: "Must be a valid URL.",
};
setFormErrors(newFormErrors);
// Remove when all fields set to empty
const isRemovingNdesScepProxy = Object.values(formData).every(
(val) => val === ""
);
if (!isRemovingNdesScepProxy && (!scepUrlValid || !adminUrlValid)) {
return;
}
setIsUpdatingNdesScepProxy(true);
// Format for API
const formDataToSubmit = isRemovingNdesScepProxy
? null
: {
url: formData.scepUrl,
admin_url: formData.adminUrl,
username: formData.username,
password: formData.password,
};
// Update integrations.ndes_scep_proxy only
const destination = {
ndes_scep_proxy: formDataToSubmit,
};
try {
await configAPI.update({ integrations: destination });
renderFlash(
"success",
`Successfully ${
isRemovingNdesScepProxy ? "removed" : "added"
} your SCEP server.`
);
refetchConfig();
} catch (error) {
console.error(error);
const reason = getErrorReason(error);
if (reason.includes("invalid admin URL or credentials")) {
renderFlash("error", BAD_CREDENTIALS_ERROR);
} else if (reason.includes("the password cache is full")) {
renderFlash("error", CACHE_ERROR);
} else if (reason.includes("does not have sufficient permissions")) {
renderFlash("error", INSUFFICIENT_PERMISSIONS_ERROR);
} else if (
reason.includes(formData.scepUrl) &&
reason.includes("context deadline exceeded")
) {
renderFlash("error", SCEP_URL_TIMEOUT_ERROR);
} else if (
reason.includes(formData.adminUrl) &&
reason.includes("context deadline exceeded")
) {
renderFlash("error", ADMIN_URL_TIMEOUT_ERROR);
} else if (reason.includes("invalid SCEP URL")) {
renderFlash("error", BAD_SCEP_URL_ERROR);
} else renderFlash("error", DEFAULT_ERROR);
} finally {
setIsUpdatingNdesScepProxy(false);
}
};
return (
<MainContent className={baseClass}>
<>
<BackLink
text="Back to MDM"
path={PATHS.ADMIN_INTEGRATIONS_MDM}
className={`${baseClass}__back-to-mdm`}
/>
<div className={`${baseClass}__page-content`}>
<div className={`${baseClass}__page-header-section`}>
<h1>Simple Certificate Enrollment Protocol (SCEP)</h1>
</div>
<ScepCertificateContent
router={router}
onFormSubmit={onFormSubmit}
formData={formData}
formErrors={formErrors}
onInputChange={onInputChange}
onBlur={handleBlur}
config={appConfig || null}
isPremiumTier={isPremiumTier || false}
isLoading={isLoadingAppConfig}
isSaving={isUpdatingNdesScepProxy}
showDataError={isErrorAppConfig}
/>
</div>
</>
</MainContent>
);
};
export default ScepPage;

View file

@ -1,68 +0,0 @@
.scep-page {
&__back-to-mdm {
margin-bottom: $pad-xlarge;
}
h1 {
margin-bottom: $pad-xxlarge;
font-size: $x-large;
}
p {
font-size: $x-small;
margin: 0 0 $pad-large;
}
&__steps {
font-size: $x-small;
padding: 0;
margin: 0;
max-width: 660px;
counter-reset: step-counter;
list-style-type: none;
display: flex;
flex-direction: column;
gap: $pad-large;
form {
gap: $pad-small;
button {
align-self: flex-end;
}
}
li {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: $pad-small;
position: relative;
padding-left: $pad-medium;
&::before {
content: counter(step-counter) ".";
counter-increment: step-counter;
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
}
> span {
flex: 1 0 100%;
}
p {
margin: 0;
}
.url-inputs-wrapper {
flex: 1 0 100%;
gap: $pad-small;
}
}
}
}

View file

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

View file

@ -65,8 +65,6 @@ import SetupExperience from "pages/ManageControlsPage/SetupExperience/SetupExper
import WindowsMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage";
import AppleMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/AppleMdmPage";
import AndroidMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/AndroidMdmPage";
import ScepPage from "pages/admin/IntegrationsPage/cards/MdmSettings/ScepPage";
import CertificatesPage from "pages/admin/IntegrationsPage/cards/CertificateAuthorities";
import Scripts from "pages/ManageControlsPage/Scripts/Scripts";
import WindowsAutomaticEnrollmentPage from "pages/admin/IntegrationsPage/cards/MdmSettings/WindowsAutomaticEnrollmentPage";
import AppleBusinessManagerPage from "pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage";
@ -201,8 +199,6 @@ const routes = (
<Route path="integrations/mdm/windows" component={WindowsMdmPage} />
<Route path="integrations/mdm/apple" component={AppleMdmPage} />
<Route path="integrations/mdm/android" component={AndroidMdmPage} />
{/* TODO: digicert update: remove scep route when digicert feature is ready */}
<Route path="integrations/mdm/scep" component={ScepPage} />
{/* This redirect is used to handle old apple automatic enrollments page */}
<Redirect
from="integrations/automatic-enrollment/apple"

View file

@ -49,7 +49,7 @@ export default {
ADMIN_INTEGRATIONS_SCEP: `${URL_PREFIX}/settings/integrations/mdm/scep`,
ADMIN_INTEGRATIONS_CALENDARS: `${URL_PREFIX}/settings/integrations/calendars`,
ADMIN_INTEGRATIONS_CHANGE_MANAGEMENT: `${URL_PREFIX}/settings/integrations/change-management`,
ADMIN_INTEGRATIONS_CERTIFICATE_AUTHORITIES: `${URL_PREFIX}/settings/integrations/certificate-authorities`,
ADMIN_INTEGRATIONS_CERTIFICATE_AUTHORITIES: `${URL_PREFIX}/settings/integrations/certificates`,
ADMIN_INTEGRATIONS_VPP: `${URL_PREFIX}/settings/integrations/mdm/vpp`,
ADMIN_INTEGRATIONS_VPP_SETUP: `${URL_PREFIX}/settings/integrations/vpp/setup`,

View file

@ -1,4 +1,3 @@
import EditCertAuthorityModal from "pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal";
import configAPI from "./config";
export default {