import React, { useContext, useEffect, useState } from "react"; import { size } from "lodash"; import paths from "router/paths"; import { NotificationContext } from "context/notification"; import conditionalAccessAPI, { ConfirmMSConditionalAccessResponse, } from "services/entities/conditional_access"; import configAPI from "services/entities/config"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import CustomLink from "components/CustomLink"; import SectionHeader from "components/SectionHeader"; import { DEFAULT_USE_QUERY_OPTIONS, LEARN_MORE_ABOUT_BASE_LINK, } from "utilities/constants"; import Button from "components/buttons/Button"; import { IInputFieldParseTarget } from "interfaces/form_field"; import { AppContext } from "context/app"; import Spinner from "components/Spinner"; import PremiumFeatureMessage from "components/PremiumFeatureMessage"; import InfoBanner from "components/InfoBanner"; import Icon from "components/Icon"; import TooltipTruncatedText from "components/TooltipTruncatedText"; import { useQuery } from "react-query"; import DataError from "components/DataError"; import Modal from "components/Modal"; import { IConfig } from "interfaces/config"; const baseClass = "conditional-access"; const MSETID = "microsoft_entra_tenant_id"; interface IDeleteConditionalAccessModal { toggleDeleteConditionalAccessModal: () => void; onDelete: () => void; } const DeleteConditionalAccessModal = ({ toggleDeleteConditionalAccessModal, onDelete, }: IDeleteConditionalAccessModal) => { const { renderFlash } = useContext(NotificationContext); const [isDeleting, setIsDeleting] = useState(false); const handleDelete = async () => { setIsDeleting(true); try { await conditionalAccessAPI.deleteMicrosoftConditionalAccess(); renderFlash("success", "Successfully disconnected from Microsoft Entra."); toggleDeleteConditionalAccessModal(); onDelete(); } catch { renderFlash( "error", "Could not disconnect from Microsoft Entra, please try again." ); } setIsDeleting(false); }; return ( <>

Fleet will be disconnected from Microsoft Entra and will stop blocking end users from logging in with single sign-on.

); }; // conditions –> UI phases: // - no config.tenant id –> "form" // - config.tenant id: // - and config.confirmed –> "configured" // - not config.confirmed –> "confirming-configured", hit confirmation endpoint // - confirmation endpoint returns false –> "form", prefilled with current tid // - confirmation endpoint returns true –> "configured" // - conf ep returns error –> DataError, under header // - form submitted –> "form-submitted", new tab to MS stuff // interface IFormData { [MSETID]: string; } interface IFormErrors { [MSETID]?: string | null; } enum Phase { Form = "form", FormSubmitted = "form-submitted", ConfirmingConfigured = "confirming-configured", ConfirmationError = "confirmation-error", Configured = "configured", } const validate = (formData: IFormData) => { const errs: IFormErrors = {}; if (!formData[MSETID]) { errs[MSETID] = "Tenant ID must be present"; } return errs; }; const ConditionalAccess = () => { // HOOKS const { renderFlash } = useContext(NotificationContext); const { isPremiumTier, setConfig, config: contextConfig } = useContext( AppContext ); const [phase, setPhase] = useState(Phase.Form); const [isUpdating, setIsUpdating] = useState(false); // this page is unique in that it triggers a server process that will result in an update to // config, but via an endpoint (conditional access) other than the usual PATCH config, so we want // to both reference config context AND conditionally (when `isUpdating` from the Configured // phase) access `refetchConfig` and associated useQuery capability // see frontend/docs/patterns.md > ### Reading and updating configs for why this is atypical const { refetch: refetchConfig } = useQuery( ["config"], () => configAPI.loadAll(), { select: (data: IConfig) => data, enabled: isUpdating && phase === Phase.Configured, onSuccess: (_config) => { if ( !_config?.conditional_access?.microsoft_entra_connection_configured ) { setPhase(Phase.Form); } setConfig(_config); setIsUpdating(false); }, ...DEFAULT_USE_QUERY_OPTIONS, } ); const [formData, setFormData] = useState({ [MSETID]: contextConfig?.conditional_access?.microsoft_entra_tenant_id || "", }); const [formErrors, setFormErrors] = useState({}); const [ showDeleteConditionalAccessModal, setShowDeleteConditionalAccessModal, ] = useState(false); // "loading" state here is encompassed by phase === Phase.ConfirmingConfigured state, don't need // to use useQuery's // "error" state handled by onError callback // success state handled by onSuccess callback useQuery< ConfirmMSConditionalAccessResponse, Error, ConfirmMSConditionalAccessResponse >(["confirmAccess"], conditionalAccessAPI.confirmMicrosoftConditionalAccess, { ...DEFAULT_USE_QUERY_OPTIONS, // only make this call at the appropriate UI phase enabled: phase === Phase.ConfirmingConfigured && isPremiumTier, onSuccess: ({ configuration_completed, setup_error }) => { if (configuration_completed) { setPhase(Phase.Configured); renderFlash( "success", "Successfully verified conditional access integration" ); } else { setPhase(Phase.Form); if ( // IT admin did not complete the consent. !setup_error || // IT admin clicked "Cancel" in the consent dialog. setup_error.includes( "A Microsoft Entra admin did not consent to the permissions requested by the conditional access integration" ) ) { renderFlash( "error", "Couldn't update. Fleet didn't get permissions for Entra. Please try again and accept the permissions." ); } else if ( setup_error.includes( 'No "Fleet conditional access" Entra ID group was found' ) ) { renderFlash( "error", `Couldn't connect. The "Fleet conditional access" group doesn't exist in Entra. Please create the group and try again.` ); } else { // For other kind of errors we just show a generic error. // We won't render the error as is because the error comes from the MS proxy and they may be too big or unformatted // to display in the banner. // // For troubleshooting: // - The API response contains the setup_error. // - The Fleet server logs the error. // - The MS proxy stores the error in its database. renderFlash( "error", "Couldn't connect. Please contact your Fleet administrator." ); } } }, onError: () => { // distinct from successful confirmation response of `false`, this handles an API error setPhase(Phase.ConfirmationError); }, }); const { microsoft_entra_tenant_id: contextConfigMsetId, microsoft_entra_connection_configured: contextConfigMseConfigured, } = contextConfig?.conditional_access || {}; // only checks if tenant id already present in config, not if user added it to the form useEffect(() => { if (contextConfigMsetId) { if (!contextConfigMseConfigured) { setPhase(Phase.ConfirmingConfigured); } else { // tenant id is present and connection is configured setPhase(Phase.Configured); } } }, [contextConfigMsetId, contextConfigMseConfigured]); if (!isPremiumTier) { return ; } // HANDLERS const toggleDeleteConditionalAccessModal = () => { setShowDeleteConditionalAccessModal(!showDeleteConditionalAccessModal); }; const onSubmit = async (evt: React.FormEvent) => { evt.preventDefault(); const errs = validate(formData); if (Object.keys(errs).length > 0) { setFormErrors(errs); return; } setIsUpdating(true); try { const { microsoft_authentication_url: msAuthURL, } = await conditionalAccessAPI.triggerMicrosoftConditionalAccess( formData[MSETID] ); setIsUpdating(false); setPhase(Phase.FormSubmitted); window.open(msAuthURL); } catch (e) { renderFlash( "error", "Could not update conditional access integration settings." ); setIsUpdating(false); } }; const onDeleteConditionalAccess = async () => { setFormData({ [MSETID]: "" }); refetchConfig(); }; const onInputChange = ({ name, value }: IInputFieldParseTarget) => { const newFormData = { ...formData, [name]: value }; setFormData(newFormData); const newErrs = validate(newFormData); // only set errors that are updates of existing errors // new errors are only set onBlur or submit const errsToSet: Record = {}; Object.keys(formErrors).forEach((k) => { // @ts-ignore if (newErrs[k]) { // @ts-ignore errsToSet[k] = newErrs[k]; } }); setFormErrors(errsToSet); }; const onInputBlur = () => { setFormErrors(validate(formData)); }; const renderContent = () => { switch (phase) { case Phase.Form: return (
You can find this in your Microsoft Entra admin center.{" "} } onChange={onInputChange} name={MSETID} value={formData[MSETID]} parseTarget onBlur={onInputBlur} error={formErrors[MSETID]} /> ); case Phase.FormSubmitted: return ( To complete your integration, follow the instructions in the other tab, then refresh this page to verify. ); case Phase.ConfirmingConfigured: // checking integration return ; case Phase.ConfirmationError: return ; case Phase.Configured: return (
Microsoft Entra tenant ID:{" "}
); default: return ; } }; return (

Block hosts failing any policies from logging in with single sign-on. Enable or disable on the{" "} page.

{renderContent()} {showDeleteConditionalAccessModal && ( )}
); }; export default ConditionalAccess;