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 (
);
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.