diff --git a/frontend/components/FileUploader/FileUploader.tsx b/frontend/components/FileUploader/FileUploader.tsx index fde81e73743c..a95177197c01 100644 --- a/frontend/components/FileUploader/FileUploader.tsx +++ b/frontend/components/FileUploader/FileUploader.tsx @@ -22,6 +22,7 @@ export type ISupportedGraphicNames = Extract< | "file-p7m" | "file-pem" | "file-vpp" + | "file-crt" >; interface IFileUploaderProps { diff --git a/frontend/components/graphics/FileCrt.tsx b/frontend/components/graphics/FileCrt.tsx new file mode 100644 index 000000000000..e5c33daf9202 --- /dev/null +++ b/frontend/components/graphics/FileCrt.tsx @@ -0,0 +1,71 @@ +import React from "react"; + +const FileCrt = () => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +export default FileCrt; diff --git a/frontend/components/graphics/index.ts b/frontend/components/graphics/index.ts index 38862d51da16..74dc39437bab 100644 --- a/frontend/components/graphics/index.ts +++ b/frontend/components/graphics/index.ts @@ -13,6 +13,7 @@ import FilePkg from "./FilePkg"; import FileP7m from "./FileP7m"; import FilePem from "./FilePem"; import FileVpp from "./FileVpp"; +import FileCrt from "./FileCrt"; import EmptyHosts from "./EmptyHosts"; import EmptyTeams from "./EmptyTeams"; import EmptyPacks from "./EmptyPacks"; @@ -48,6 +49,7 @@ export const GRAPHIC_MAP = { "file-p7m": FileP7m, "file-pem": FilePem, "file-vpp": FileVpp, + "file-crt": FileCrt, // Other graphics "collecting-results": CollectingResults, "data-error": DataError, diff --git a/frontend/interfaces/integration.ts b/frontend/interfaces/integration.ts index 802fd1fe5f34..586e1eaf3ae0 100644 --- a/frontend/interfaces/integration.ts +++ b/frontend/interfaces/integration.ts @@ -1,3 +1,5 @@ +import { IPkiConfig } from "./pki"; + export type IIntegrationType = "jira" | "zendesk"; export interface IJiraIntegration { url: string; @@ -92,6 +94,7 @@ export interface IZendeskJiraIntegrations { export interface IGlobalIntegrations extends IZendeskJiraIntegrations { google_calendar?: IGlobalCalendarIntegration[] | null; ndes_scep_proxy?: IScepIntegration | null; + digicert_pki?: IPkiConfig[] | null; } export interface ITeamIntegrations extends IZendeskJiraIntegrations { diff --git a/frontend/interfaces/pki.ts b/frontend/interfaces/pki.ts new file mode 100644 index 000000000000..a9953de4da80 --- /dev/null +++ b/frontend/interfaces/pki.ts @@ -0,0 +1,18 @@ +export interface IPkiCert { + name: string; + sha256: string; + not_valid_after: string; +} + +export interface IPkiTemplate { + profile_id: string; + name: string; + common_name: string; + san: { user_principal_names: string[] }; + seat_id: string; +} + +export interface IPkiConfig { + pki_name: string; + templates: IPkiTemplate[]; +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx index 11774043483b..ffe48330394c 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx @@ -18,6 +18,7 @@ import IdpSection from "./components/IdpSection"; import EulaSection from "./components/EulaSection"; import EndUserMigrationSection from "./components/EndUserMigrationSection"; import ScepSection from "./components/ScepSection/ScepSection"; +import PkiSection from "./components/PkiSection/PkiSection"; const baseClass = "mdm-settings"; @@ -133,6 +134,11 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { isVppOn={!noVppTokenUploaded} isPremiumTier={!!isPremiumTier} /> + void; +} + +const AddPkiMessage = ({ onAddPki }: IAddPkiMessageProps) => { + return ( +
+

Add your PKI

+

Help your end users connect to Wi-Fi

+ +
+ ); +}; + +const PkiPage = ({ router }: { router: InjectedRouter }) => { + const { config, isPremiumTier } = useContext(AppContext); + + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showAddPkiModal, setShowAddPkiModal] = useState(false); + const [showEditTemplateModal, setShowEditTemplateModal] = useState(false); + + const selectedPki = useRef(null); + + const { + data: pkiCerts, + error: errorCerts, + isLoading, + isRefetching, + refetch: refetchCerts, + } = useQuery( + ["pki_certs"], + () => pkiApi.listCerts(), + { + refetchOnWindowFocus: false, + select: (data) => data.certificates, + enabled: isPremiumTier, + } + ); + + const { + data: pkiConfigs, + error: errorConfigs, + isLoading: isLoadingConfigs, + isRefetching: isRefetchingConfigs, + refetch: refetchConfigs, + } = useQuery( + ["digicert_pki"], + () => configApi.loadAll(), + { + refetchOnWindowFocus: false, + select: (data) => data.integrations.digicert_pki || [], // TODO: handle no value + enabled: isPremiumTier, + } + ); + + const tableData = useMemo(() => { + const dict: Record = {}; + pkiConfigs?.forEach((pki) => { + dict[pki.pki_name] = pki; + }); + pkiCerts?.forEach((pki) => { + if (!dict[pki.name]) { + dict[pki.name] = { pki_name: pki.name, templates: [] }; + } + }); + return Object.values(dict); + }, [pkiConfigs, pkiCerts]); + + const onAdd = () => { + setShowAddPkiModal(true); + }; + + const onAdded = () => { + refetchCerts(); + setShowAddPkiModal(false); + }; + + const onEditTemplate = (pkiConfig: IPkiConfig) => { + selectedPki.current = pkiConfig; + setShowEditTemplateModal(true); + }; + + const onCancelEditTemplate = useCallback(() => { + selectedPki.current = null; + setShowEditTemplateModal(false); + }, []); + + const onEditedTemplate = useCallback(() => { + selectedPki.current = null; + refetchConfigs(); + setShowEditTemplateModal(false); + }, [refetchConfigs]); + + const onDelete = (pkiConfig: IPkiConfig) => { + selectedPki.current = pkiConfig; + setShowDeleteModal(true); + }; + + const onCancelDelete = useCallback(() => { + selectedPki.current = null; + setShowDeleteModal(false); + }, []); + + const onDeleted = useCallback(() => { + selectedPki.current = null; + refetchCerts(); + refetchConfigs(); + setShowDeleteModal(false); + }, [refetchCerts, refetchConfigs]); + + // if (isLoading || isRefetching || isLoadingConfigs || isRefetchingConfigs) { + // return ; + // } + + const showDataError = errorCerts || errorConfigs; + + const renderContent = () => { + if (!isPremiumTier) { + return ; + } + + if (!config?.mdm.enabled_and_configured) { + return ( + + ); + } + + if (isLoading || isRefetching || isLoadingConfigs || isRefetchingConfigs) { + return ; + } + + // TODO: error UI + if (showDataError) { + return ( +
+ +
+ ); + } + + if (!pkiCerts?.length) { + return ; + } + + return ( + <> +

To help your end users connect to Wi-Fi, you can add your PKI.

+ + + ); + }; + + return ( + + <> + +
+
+

Public key infrastructure (PKI)

+ {isPremiumTier && + !!pkiCerts?.length && + !!config?.mdm.enabled_and_configured && ( + + )} +
+ <>{renderContent()} +
+ + {showAddPkiModal && ( + setShowAddPkiModal(false)} + /> + )} + {showDeleteModal && selectedPki.current && ( + + )} + {showEditTemplateModal && selectedPki.current && ( + + )} +
+ ); +}; + +export default PkiPage; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/_styles.scss new file mode 100644 index 000000000000..88edd6b4ade2 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/_styles.scss @@ -0,0 +1,64 @@ +.pki-page { + &__back-to-mdm { + margin-bottom: $pad-xlarge; + } + + &__page-content { + display: flex; + flex-direction: column; + gap: $pad-xxlarge; + } + + &__page-header-section { + display: flex; + flex-direction: row; + gap: $pad-large; + align-items: center; + justify-content: space-between; + + h1 { + margin-bottom: 0; + font-size: $large; + } + } + + &__add-message { + margin: 0 auto; + text-align: center; + width: 450px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + > h2 { + margin-bottom: $pad-small; + font-size: $small; + font-weight: $bold; + } + + > p { + margin: 0 0 $pad-medium; + } + } + + &__url-inputs-wrapper { + display: flex; + flex-direction: column; + gap: $pad-icon; + margin-top: $pad-large; + } + + &__url-input { + margin-bottom: 0; + } + + // TODO: remove these styles when the updated error component is used + .data-error { + margin: $pad-xxlarge 0; + padding: $pad-xlarge 0; + } + .data-error__inner { + padding: $pad-xlarge 0; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/AddPkiModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/AddPkiModal.tsx new file mode 100644 index 000000000000..f4210750fa7c --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/AddPkiModal.tsx @@ -0,0 +1,164 @@ +import React, { useCallback, useContext, useState } from "react"; +import { noop } from "lodash"; + +import { NotificationContext } from "context/notification"; +import pkiAPI from "services/entities/pki"; + +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import FileUploader from "components/FileUploader"; +import CustomLink from "components/CustomLink"; +import TooltipWrapper from "components/TooltipWrapper"; + +import DownloadCSR from "pages/admin/components/DownloadFileButtons/DownloadCSR"; + +import { getErrorMessage } from "./helpers"; + +const baseClass = "add-pki-modal"; + +interface IAddPkiModalProps { + onCancel: () => void; + onAdded: () => void; +} + +const AddPkiModal = ({ onCancel, onAdded }: IAddPkiModalProps) => { + const { renderFlash } = useContext(NotificationContext); + + const [pkiName, setPkiName] = useState(""); + const [pkiCert, setPkiCert] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + const onSelectFile = useCallback((files: FileList | null) => { + const file = files?.[0]; + if (file) { + setPkiCert(file); + } + }, []); + + const uploadPkiCert = useCallback(async () => { + setIsUploading(true); + if (!pkiCert) { + setIsUploading(false); + renderFlash("error", "No file selected."); + return; + } + + try { + await pkiAPI.uploadCert(pkiName, pkiCert); + renderFlash("success", "Added successfully."); + onAdded(); + } catch (e) { + renderFlash("error", getErrorMessage(e)); + onCancel(); + } finally { + setIsUploading(false); + } + }, [pkiName, pkiCert, renderFlash, onAdded, onCancel]); + + const onInputChangeName = useCallback( + (value: string) => { + setPkiName(value); + }, + [setPkiName] + ); + + return ( + + <> +
+

To help your end users connect to Wi-Fi, you can add your PKI.

+

Fleet currently supports DigiCert PKI.

+ +
    +
  1. + 1. +

    + Download a certificate signing request (CSR) for DigiCert. + +

    +
  2. +
  3. + 2. + + + Go to{" "} + +
    +
    +
    +
  4. +
  5. + 3. + + In DigiCert, select Settings {">"} Get an RA certificate, + upload your CSR, and download your registration authority (RA) + certificate. + +
  6. +
  7. + 4. + Upload your RA certificate (.p7b file) below. +
  8. +
+
+ +
+ + + + +
+ +
+ ); +}; + +export default AddPkiModal; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/_styles.scss new file mode 100644 index 000000000000..ad5f957b9666 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/_styles.scss @@ -0,0 +1,67 @@ +.add-pki-modal { + &__request-button { + display: flex; + gap: $pad-small; + align-items: center; + margin-top: $pad-small; + + label { + display: flex; + gap: $pad-small; + cursor: pointer; + } + } + + &__setup { + display: flex; + flex-direction: column; + gap: $pad-large; + p { + margin: 0; + } + } + + &__setup-list { + font-size: $x-small; + display: flex; + flex-direction: column; + gap: $pad-large; + padding: 0; + margin: 0; + max-width: 660px; + list-style: none; + + li { + display: flex; + flex-direction: row; + gap: $pad-small; + + p { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 0; + } + } + } + + &__file-uploader { + margin-top: $pad-medium; + margin-left: $pad-medium; + + .file-uploader__message { + color: $ui-fleet-black-75; + margin: 0; + } + + button { + margin-top: 0; + } + + &--loading { + label { + opacity: 0.5; + } + } + } +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/helpers.tsx new file mode 100644 index 000000000000..dcd7440790f6 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/helpers.tsx @@ -0,0 +1,6 @@ +const DEFAULT_ERROR_MESSAGE = "Couldn't add. Please try again."; + +// eslint-disable-next-line import/prefer-default-export +export const getErrorMessage = (err: unknown) => { + return DEFAULT_ERROR_MESSAGE; +}; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/index.ts new file mode 100644 index 000000000000..598d8914fc2f --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/AddPkiModal/index.ts @@ -0,0 +1 @@ +export { default } from "./AddPkiModal"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/DeletePkiModal/DeletePkiModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/DeletePkiModal/DeletePkiModal.tsx new file mode 100644 index 000000000000..74a98950523c --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/DeletePkiModal/DeletePkiModal.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useContext, useState } from "react"; + +import { IPkiConfig } from "interfaces/pki"; +import pkiAPI from "services/entities/pki"; + +import { NotificationContext } from "context/notification"; + +import Button from "components/buttons/Button"; +import Modal from "components/Modal"; + +const baseClass = "delete-pki-modal"; + +interface IDeletePkiModalProps { + pkiConfig: IPkiConfig; + onCancel: () => void; + onDeleted: () => void; +} + +const DeletePkiModal = ({ + pkiConfig: { pki_name: name }, + onCancel, + onDeleted, +}: IDeletePkiModalProps) => { + const { renderFlash } = useContext(NotificationContext); + + const [isDeleting, setIsDeleting] = useState(false); + + const onDelete = useCallback(async () => { + setIsDeleting(true); + + try { + await pkiAPI.deleteCert(name); + renderFlash("success", "Deleted successfully."); + onDeleted(); + } catch (e) { + // TODO: Check API sends back correct error messages + renderFlash("error", "Couldn’t delete. Please try again."); + onCancel(); + } + }, [onCancel, onDeleted, renderFlash, name]); + + return ( + + <> +

+ If you want to re-enable PKI, you'll have to upload a new RA + certificate. +

+ +
+ + +
+ +
+ ); +}; + +export default DeletePkiModal; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/DeletePkiModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/DeletePkiModal/index.ts new file mode 100644 index 000000000000..0f03a38e4bf2 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/DeletePkiModal/index.ts @@ -0,0 +1 @@ +export { default } from "./DeletePkiModal"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/EditTemplateModal/EditTemplateModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/EditTemplateModal/EditTemplateModal.tsx new file mode 100644 index 000000000000..16ea464e9b84 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/EditTemplateModal/EditTemplateModal.tsx @@ -0,0 +1,211 @@ +import React, { useCallback, useState } from "react"; + +import { NotificationContext } from "context/notification"; + +import { IFormField } from "interfaces/form_field"; +import { IPkiConfig, IPkiTemplate } from "interfaces/pki"; + +import pkiApi from "services/entities/pki"; + +import Button from "components/buttons/Button"; +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +import Modal from "components/Modal"; +import TooltipWrapper from "components/TooltipWrapper"; + +const baseClass = "pki-edit-template-modal"; + +type IFormErrors = Partial>; + +const TEMPLATE_PLACEHOLDERS: Record = { + profile_id: "123", + name: "DIGICERT_TEMPLATE", + common_name: "$FLEET_VAR_HOST_HARDWARE_SERIAL@example.com", + san: "$FLEET_VAR_HOST_HARDWARE_SERIAL@example.com", + seat_id: "$FLEET_VAR_HOST_HARDWARE_SERIAL@example.com", +}; + +const TEMPLATE_HELP_TEXT: Record< + keyof IPkiTemplate, + string | React.ReactNode +> = { + profile_id: ( + + The Certificate profile ID field in DigiCert. + + ), + name: + "Letters, numbers, and underscores only. Fleet will create a configuration profile variable with the $FLEET_VAR_PKI_CERT_ prefix (e.g. $FLEET_VAR_PKI_CERT_DIGICERT_TEMPALTE).", + common_name: "Certificates delivered to your hosts using will have this CN.", + san: "Certificates delivered to your hosts using will have this SAN.", + seat_id: + "Certificates delivered to your hosts using will be assgined to this seat ID in DigiCert.", +}; + +// TODO: we should revisit this in design after the PoC +const flattenTemplate = (template: IPkiTemplate | undefined) => { + if (!template) { + return { + profile_id: "", + name: "", + common_name: "", + san: "", + seat_id: "", + }; + } + return { + profile_id: template.profile_id.toString(), + name: template.name, + common_name: template.common_name, + san: template.san.user_principal_names[0], + seat_id: template.seat_id, + }; +}; + +// TODO: we should revisit this in design after the PoC +const unflattenTemplate = (formData: Record) => { + return { + profile_id: formData.profile_id, + name: formData.name, + common_name: formData.common_name, + san: { user_principal_names: [formData.san] }, + seat_id: formData.seat_id, + }; +}; + +const EditTemplateModal = ({ + pkiConfig, + onCancel, + onSuccess, +}: { + pkiConfig: IPkiConfig; + onCancel: () => void; + onSuccess: () => void; +}) => { + const { renderFlash } = React.useContext(NotificationContext); + + const [formData, setFormData] = useState>( + flattenTemplate(pkiConfig.templates[0]) + ); + const [formErrors, setFormErrors] = useState({}); + + const onInputChange = ({ name, value }: IFormField) => { + setFormErrors((prev) => ({ ...prev, [name]: "" })); + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const onSubmit = useCallback(async () => { + // TODO: validations + + // TODO: how do we handle multiple array elements at top-level (i.e. certs by pki_name) and at cert-level + // (templates by template name)? + + try { + await pkiApi.addTemplate(pkiConfig.pki_name, unflattenTemplate(formData)); + onSuccess(); + } catch { + renderFlash("error", "Could not save template"); + } + }, [formData, onSuccess, pkiConfig.pki_name, renderFlash]); + + const disableInput = !!pkiConfig.templates.length; + const disableSave = Object.values(formData).some((v) => !v); + + const isSaving = false; + + return ( + + <> +
+ + + + + +
+ + + + +
+ + +
+ ); +}; + +export default EditTemplateModal; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/EditTemplateModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/EditTemplateModal/index.ts new file mode 100644 index 000000000000..f9b2f3042d7f --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/EditTemplateModal/index.ts @@ -0,0 +1 @@ +export { default } from "./EditTemplateModal"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/PkiTable.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/PkiTable.tsx new file mode 100644 index 000000000000..5ab07ce03c94 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/PkiTable.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +import { IPkiConfig } from "interfaces/pki"; + +import TableContainer from "components/TableContainer"; + +import { generateTableConfig } from "./PkiTableConfig"; + +const baseClass = "pki-table"; + +interface IPkiTableProps { + data: IPkiConfig[]; + onEdit: (pkiConfig: IPkiConfig) => void; + onDelete: (pkiConfig: IPkiConfig) => void; +} + +const PkiTable = ({ data, onEdit, onDelete }: IPkiTableProps) => { + const onSelectAction = (action: string, pkiConfig: IPkiConfig) => { + switch (action) { + case "view_template": + onEdit(pkiConfig); + break; + case "add_template": + onEdit(pkiConfig); + break; + case "delete": + onDelete(pkiConfig); + break; + default: + break; + } + }; + + const tableConfig = generateTableConfig(onSelectAction); + + return ( + + columnConfigs={tableConfig} + defaultSortHeader="org_name" + disableTableHeader + disablePagination + showMarkAllPages={false} + isAllPagesSelected={false} + emptyComponent={() => <>} + isLoading={false} + data={data} + className={baseClass} + /> + ); +}; + +export default PkiTable; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/PkiTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/PkiTableConfig.tsx new file mode 100644 index 000000000000..493af7df82ad --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/PkiTableConfig.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { CellProps, Column } from "react-table"; + +import { IMdmAbmToken } from "interfaces/mdm"; +import { IPkiConfig } from "interfaces/pki"; +import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; +import { IDropdownOption } from "interfaces/dropdownOption"; + +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; +import ActionsDropdown from "components/ActionsDropdown"; +import TextCell from "components/TableContainer/DataTable/TextCell"; + +type IPkiTableConfig = Column; +type ITableStringCellProps = IStringCellProps; +type IPkiTemplatesCellProps = CellProps; + +type ITableHeaderProps = IHeaderProps; + +const generateActions = (pkiConfig: IPkiConfig): IDropdownOption[] => { + return [ + { + value: pkiConfig.templates?.length ? "view_template" : "add_template", + label: pkiConfig.templates?.length ? "View template" : "Add template", + disabled: false, + }, + { + value: "delete", + label: "Delete", + disabled: false, + }, + ]; +}; + +export const generateTableConfig = ( + actionSelectHandler: (value: string, pkiConfig: IPkiConfig) => void +): IPkiTableConfig[] => { + return [ + { + accessor: "pki_name", + sortType: "caseInsensitive", + Header: (cellProps: ITableHeaderProps) => ( + + ), + Cell: (cellProps: ITableStringCellProps) => { + const { pki_name: name } = cellProps.cell.row.original; + return ; + }, + }, + { + accessor: "templates", + Header: "Certificate template", + disableSortBy: true, + Cell: ({ value: templates }: IPkiTemplatesCellProps) => { + return ; // TODO: use our own icon + }, + }, + { + Header: "", + id: "actions", + disableSortBy: true, + // the accessor here is insignificant, we just need it as its required + // but we don't use it. + accessor: () => "name", + Cell: (cellProps: CellProps) => ( + + actionSelectHandler(action, cellProps.row.original) + } + placeholder="Actions" + /> + ), + }, + ]; +}; + +export const generateTableData = (data: IMdmAbmToken[]) => { + return data; +}; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/_styles.scss new file mode 100644 index 000000000000..844f82633e0e --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/_styles.scss @@ -0,0 +1,2 @@ +.pki-table { +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/index.ts new file mode 100644 index 000000000000..9ad8cd8cfa0b --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/components/PkiTable/index.ts @@ -0,0 +1 @@ +export { default } from "./PkiTable"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/index.ts new file mode 100644 index 000000000000..50ce02284801 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/PkiPage/index.ts @@ -0,0 +1 @@ +export { default } from "./PkiPage"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/PkiSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/PkiSection.tsx new file mode 100644 index 000000000000..43791a2cd59e --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/PkiSection.tsx @@ -0,0 +1,113 @@ +import React, { useContext } from "react"; +import { InjectedRouter } from "react-router"; + +import PATHS from "router/paths"; +import { AppContext } from "context/app"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; + +import SettingsSection from "pages/admin/components/SettingsSection"; +import PremiumFeatureMessage from "components/PremiumFeatureMessage"; +import TooltipWrapper from "components/TooltipWrapper"; + +import SectionCard from "../SectionCard"; + +const baseClass = "pki-section"; + +interface IPkiCardProps { + isAppleMdmOn: boolean; + isPkiOn: boolean; + router: InjectedRouter; +} + +export const PKI_TIP_CONTENT = <>Fleet currently supports DigiCert as a PKI.; + +const DIGICERT_PKI_ADDED_MESSAGE = "DigiCert added as your PKI."; // TODO: confirm this message + +const PkiCard = ({ isAppleMdmOn, isPkiOn, router }: IPkiCardProps) => { + const navigateToPkiSetup = () => { + router.push(PATHS.ADMIN_INTEGRATIONS_PKI); + }; + + const appleMdmDisabledCard = ( + +

+ To help your end users connect to Wi-Fi by adding your{" "} + PKI, first + turn on Apple (macOS, iOS, iPadOS) MDM. +

+
+ ); + + const isPkiOnCard = ( + + + Edit + + } + > + {DIGICERT_PKI_ADDED_MESSAGE} + + ); + + const isPkiOffCard = ( + + Add PKI + + } + > +
+ To help your end users connect to Wi-Fi, you can add your{" "} + PKI. +
+
+ ); + + if (!isAppleMdmOn) { + return appleMdmDisabledCard; + } + + return isPkiOn ? isPkiOnCard : isPkiOffCard; +}; + +interface IPkiSectionProps { + router: InjectedRouter; + isPkiOn: boolean; + isPremiumTier: boolean; +} + +const PkiSection = ({ router, isPkiOn, isPremiumTier }: IPkiSectionProps) => { + const { config } = useContext(AppContext); + + return ( + + {!isPremiumTier ? ( + + ) : ( + + )} + + ); +}; + +export default PkiSection; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/__styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/__styles.scss new file mode 100644 index 000000000000..da5a66396361 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/__styles.scss @@ -0,0 +1,5 @@ +.pki-section { + .section-card__content-wrapper span { + display: inline-flex; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/index.ts new file mode 100644 index 000000000000..95b76aac0088 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/PkiSection/index.ts @@ -0,0 +1 @@ +export { default } from "./PkiSection"; diff --git a/frontend/pages/admin/components/DownloadFileButtons/DownloadCSR.tsx b/frontend/pages/admin/components/DownloadFileButtons/DownloadCSR.tsx index a1e82da21bac..25f60e13ae6e 100644 --- a/frontend/pages/admin/components/DownloadFileButtons/DownloadCSR.tsx +++ b/frontend/pages/admin/components/DownloadFileButtons/DownloadCSR.tsx @@ -1,6 +1,7 @@ import React, { FormEvent, useCallback, useMemo, useState } from "react"; import mdmAppleApi from "services/entities/mdm_apple"; +import pkiApi from "services/entities/pki"; import Icon from "components/Icon"; import Button from "components/buttons/Button"; @@ -10,10 +11,11 @@ interface IDownloadCSRProps { baseClass: string; onSuccess?: () => void; onError?: (e: unknown) => void; + pkiName?: string; } -const downloadCSRFile = (data: { csr: string }) => { - downloadBase64ToFile(data.csr, "fleet-mdm-apple.csr"); +const downloadCSRFile = (data: { csr: string }, filename?: string) => { + downloadBase64ToFile(data.csr, filename || "fleet-mdm-apple.csr"); }; // TODO: why can't we use Content-Dispostion for these? We're only getting one file back now. @@ -21,6 +23,7 @@ const downloadCSRFile = (data: { csr: string }) => { const useDownloadCSR = ({ onSuccess, onError, + pkiName, }: Omit) => { const [downloadState, setDownloadState] = useState(undefined); @@ -29,8 +32,13 @@ const useDownloadCSR = ({ evt.preventDefault(); setDownloadState("loading"); try { - const data = await mdmAppleApi.requestCSR(); - downloadCSRFile(data); + let data; + if (pkiName) { + data = await pkiApi.requestCSR(pkiName); + } else { + data = await mdmAppleApi.requestCSR(); + } + downloadCSRFile(data, pkiName); setDownloadState("success"); onSuccess && onSuccess(); } catch (e) { @@ -38,7 +46,7 @@ const useDownloadCSR = ({ onError && onError(e); } }, - [onError, onSuccess] + [onError, onSuccess, pkiName] ); const memoized = useMemo( @@ -56,8 +64,9 @@ export const DownloadCSR = ({ baseClass, onSuccess, onError, + pkiName, }: IDownloadCSRProps) => { - const { handleDownload } = useDownloadCSR({ onSuccess, onError }); + const { handleDownload } = useDownloadCSR({ onSuccess, onError, pkiName }); return (