diff --git a/frontend/pages/ManageControlsPage/SetupExperience/SetupExperienceNavItems.tsx b/frontend/pages/ManageControlsPage/SetupExperience/SetupExperienceNavItems.tsx index ee4406c932d..686008fb738 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/SetupExperienceNavItems.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/SetupExperienceNavItems.tsx @@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router"; import { ISideNavItem } from "pages/admin/components/SideNav/SideNav"; -import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthentication"; +import Users from "./cards/Users/Users"; import BootstrapPackage from "./cards/BootstrapPackage"; import SetupAssistant from "./cards/SetupAssistant"; import InstallSoftware from "./cards/InstallSoftware"; @@ -18,10 +18,10 @@ export interface ISetupExperienceCardProps { const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem[] = [ { - title: "1. End user authentication", + title: "1. Users", urlSection: "end-user-auth", path: PATHS.CONTROLS_END_USER_AUTHENTICATION, - Card: EndUserAuthentication, + Card: Users, }, { title: "2. Bootstrap package", diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/EndUserAuthForm.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/EndUserAuthForm.tsx deleted file mode 100644 index 4c5e756bd78..00000000000 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/EndUserAuthForm.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React, { useContext, useState } from "react"; -import classnames from "classnames"; - -import PATHS from "router/paths"; -import mdmAPI from "services/entities/mdm"; -import { NotificationContext } from "context/notification"; -import { AppContext } from "context/app"; - -import Button from "components/buttons/Button"; -import Checkbox from "components/forms/fields/Checkbox"; -import CustomLink from "components/CustomLink"; -import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; -import TooltipWrapper from "components/TooltipWrapper"; -import RevealButton from "components/buttons/RevealButton"; - -const baseClass = "end-user-auth-form"; - -const getTooltipCopy = (android = false) => { - return ( - <> - {android ? "Android" : "Apple"} MDM must be turned on in Settings{" "} - > Integrations > Mobile Device Management (MDM) to - turn on end user authentication. - - ); -}; -interface IEndUserAuthFormProps { - currentTeamId: number; - defaultIsEndUserAuthEnabled: boolean; - defaultLockEndUserInfo: boolean; -} - -const EndUserAuthForm = ({ - currentTeamId, - defaultIsEndUserAuthEnabled, - defaultLockEndUserInfo, -}: IEndUserAuthFormProps) => { - const { renderFlash } = useContext(NotificationContext); - const gitOpsModeEnabled = useContext(AppContext).config?.gitops - .gitops_mode_enabled; - - const [isEndUserAuthEnabled, setEndUserAuthEnabled] = useState( - defaultIsEndUserAuthEnabled - ); - const [lockEndUserInfo, setLockEndUserInfo] = useState( - defaultLockEndUserInfo - ); - const [isUpdating, setIsUpdating] = useState(false); - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - - const onToggleEndUserAuth = (newCheckVal: boolean) => { - setEndUserAuthEnabled(newCheckVal); - // Sync lock end user info with EUA: enabling EUA enables it, disabling EUA disables it. - setLockEndUserInfo(newCheckVal); - }; - - const onChangeLockEndUserInfo = (newCheckVal: boolean) => { - setLockEndUserInfo(newCheckVal); - }; - - const onClickSave = async () => { - setIsUpdating(true); - const canLockEndUserInfo = isEndUserAuthEnabled && lockEndUserInfo; - try { - await mdmAPI.updateEndUserAuthentication( - currentTeamId, - isEndUserAuthEnabled, - canLockEndUserInfo - ); - renderFlash("success", "Successfully updated."); - } catch { - renderFlash("error", "Couldn’t update. Please try again."); - } finally { - setIsUpdating(false); - setLockEndUserInfo(canLockEndUserInfo); - } - }; - - const classes = classnames({ [`${baseClass}--disabled`]: gitOpsModeEnabled }); - return ( -
-
-

- Require end users to authenticate with your{" "} - {" "} - when they set up their new hosts.{" "} - macOS{" "} - hosts will also be required to agree to an{" "} - {" "} - if configured. -

- - Turn on - - setShowAdvancedOptions(!showAdvancedOptions)} - /> - {showAdvancedOptions && ( - - - End user can't edit the local account's{" "} - Account Name and -
- Full Name in macOS Setup Assistant. These fields will - be -
- locked to values from your IdP. - - } - > - Lock end user info -
-
- )} - - ( - - )} - /> - -
- ); -}; - -export default EndUserAuthForm; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/_styles.scss b/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/_styles.scss deleted file mode 100644 index ea704c909fa..00000000000 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/_styles.scss +++ /dev/null @@ -1,6 +0,0 @@ -.end-user-auth-form { - form > p { - font-size: $x-small; - line-height: $line-height-large; - } -} diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/index.ts b/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/index.ts deleted file mode 100644 index 65d8a833f63..00000000000 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/components/EndUserAuthForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EndUserAuthForm"; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/index.ts b/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/index.ts deleted file mode 100644 index a071db675fb..00000000000 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EndUserAuthentication"; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/EndUserAuthentication.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/Users.tsx similarity index 81% rename from frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/EndUserAuthentication.tsx rename to frontend/pages/ManageControlsPage/SetupExperience/cards/Users/Users.tsx index 07bb7046888..4a98f6d6f4f 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/EndUserAuthentication/EndUserAuthentication.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/Users.tsx @@ -13,11 +13,31 @@ import GenericMsgWithNavButton from "components/GenericMsgWithNavButton"; import CustomLink from "components/CustomLink"; import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; -import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm"; +import UsersForm from "./components/UsersForm/UsersForm"; import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer"; import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems"; -const baseClass = "end-user-authentication"; +const baseClass = "setup-experience-users"; + +const getEnabledManagedLocalAccount = ( + currentTeamId: number, + globalConfig?: IConfig, + teamConfig?: ITeamConfig +) => { + if (globalConfig === undefined && teamConfig === undefined) { + return false; + } + + if (currentTeamId === 0) { + return ( + globalConfig?.mdm?.setup_experience?.enable_managed_local_account ?? false + ); + } + + return ( + teamConfig?.mdm?.setup_experience?.enable_managed_local_account ?? false + ); +}; const getEnabledEndUserAuth = ( currentTeamId: number, @@ -66,10 +86,7 @@ const isIdPConfigured = ({ ); }; -const EndUserAuthentication = ({ - currentTeamId, - router, -}: ISetupExperienceCardProps) => { +const Users = ({ currentTeamId, router }: ISetupExperienceCardProps) => { const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery< IConfig, Error @@ -101,6 +118,12 @@ const EndUserAuthentication = ({ teamConfig ); + const defaultEnableManagedLocalAccount = getEnabledManagedLocalAccount( + currentTeamId, + globalConfig, + teamConfig + ); + const renderContent = () => { if (!globalConfig || isLoadingGlobalConfig || isLoadingTeamConfig) { return ; @@ -117,10 +140,11 @@ const EndUserAuthentication = ({ path={PATHS.ADMIN_INTEGRATIONS_SSO_END_USERS} /> ) : ( - )} @@ -130,7 +154,7 @@ const EndUserAuthentication = ({ return (
{ + const defaultProps = { + currentTeamId: 0, + defaultIsEndUserAuthEnabled: false, + defaultLockEndUserInfo: false, + defaultEnableManagedLocalAccount: false, + }; + + const render = createCustomRenderer({ + withBackendMock: true, + }); + + it("renders the end user authentication and managed local account checkboxes", () => { + render(); + expect(screen.getByText("End user authentication")).toBeInTheDocument(); + expect(screen.getByText("Managed local account")).toBeInTheDocument(); + }); + + it("renders help text for end user authentication with IdP link", () => { + render(); + expect( + screen.getByText(/End users are required to authenticate/) + ).toBeInTheDocument(); + expect(screen.getByText("identity provider (IdP)")).toBeInTheDocument(); + }); + + it("renders help text for managed local account", () => { + render(); + expect( + screen.getByText(/Fleet generates a user \(_fleetadmin\)/) + ).toBeInTheDocument(); + }); + + it("hides lock end user info when end user auth is unchecked", () => { + render(); + expect(screen.queryByText("Lock end user info")).not.toBeInTheDocument(); + }); + + it("shows lock end user info inline when end user auth is checked", () => { + render(); + expect(screen.getByText("Lock end user info")).toBeInTheDocument(); + }); + + it("reveals lock end user info when end user auth is toggled on", async () => { + const { user } = render(); + + expect(screen.queryByText("Lock end user info")).not.toBeInTheDocument(); + + await user.click( + screen.getByRole("checkbox", { name: "End user authentication" }) + ); + + expect(screen.getByText("Lock end user info")).toBeInTheDocument(); + }); + + it("renders managed local account checkbox as unchecked by default", () => { + render(); + expect( + screen.getByRole("checkbox", { name: "Managed local account" }) + ).not.toBeChecked(); + }); + + it("renders managed local account checkbox as checked when default is true", () => { + render(); + expect( + screen.getByRole("checkbox", { name: "Managed local account" }) + ).toBeChecked(); + }); + + it("does not show an 'Advanced options' reveal button", () => { + render(); + expect(screen.queryByText("Advanced options")).not.toBeInTheDocument(); + }); + + it("renders exactly one Save button", () => { + render(); + const saveButtons = screen.getAllByRole("button", { name: "Save" }); + expect(saveButtons).toHaveLength(1); + }); +}); diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx new file mode 100644 index 00000000000..dd65657d3c1 --- /dev/null +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx @@ -0,0 +1,183 @@ +import React, { useContext, useState } from "react"; + +import PATHS from "router/paths"; +import mdmAPI from "services/entities/mdm"; +import { NotificationContext } from "context/notification"; +import { AppContext } from "context/app"; + +import Button from "components/buttons/Button"; +import Checkbox from "components/forms/fields/Checkbox"; +import CustomLink from "components/CustomLink"; +import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; +import TooltipWrapper from "components/TooltipWrapper"; + +const baseClass = "users-form"; + +interface IUsersFormProps { + currentTeamId: number; + defaultIsEndUserAuthEnabled: boolean; + defaultLockEndUserInfo: boolean; + defaultEnableManagedLocalAccount: boolean; +} + +const UsersForm = ({ + currentTeamId, + defaultIsEndUserAuthEnabled, + defaultLockEndUserInfo, + defaultEnableManagedLocalAccount, +}: IUsersFormProps) => { + const { renderFlash } = useContext(NotificationContext); + const gitOpsModeEnabled = useContext(AppContext).config?.gitops + .gitops_mode_enabled; + + const [isEndUserAuthEnabled, setEndUserAuthEnabled] = useState( + defaultIsEndUserAuthEnabled + ); + const [lockEndUserInfo, setLockEndUserInfo] = useState( + defaultLockEndUserInfo + ); + const [enableManagedLocalAccount, setEnableManagedLocalAccount] = useState( + defaultEnableManagedLocalAccount + ); + const [isUpdating, setIsUpdating] = useState(false); + + const onToggleEndUserAuth = (newCheckVal: boolean) => { + setEndUserAuthEnabled(newCheckVal); + // Sync lock end user info with EUA: enabling EUA enables it, disabling EUA disables it. + setLockEndUserInfo(newCheckVal); + }; + + const onChangeLockEndUserInfo = (newCheckVal: boolean) => { + setLockEndUserInfo(newCheckVal); + }; + + const onToggleManagedLocalAccount = (newCheckVal: boolean) => { + setEnableManagedLocalAccount(newCheckVal); + }; + + const onClickSave = async () => { + setIsUpdating(true); + const canLockEndUserInfo = isEndUserAuthEnabled && lockEndUserInfo; + + const endUserAuthDirty = + isEndUserAuthEnabled !== defaultIsEndUserAuthEnabled || + canLockEndUserInfo !== defaultLockEndUserInfo; + const managedAccountDirty = + enableManagedLocalAccount !== defaultEnableManagedLocalAccount; + + const errors: string[] = []; + + if (endUserAuthDirty) { + try { + await mdmAPI.updateEndUserAuthentication( + currentTeamId, + isEndUserAuthEnabled, + canLockEndUserInfo + ); + } catch { + errors.push("end user authentication"); + } + } + + if (managedAccountDirty) { + try { + await mdmAPI.updateSetupExperienceSettings({ + fleet_id: currentTeamId, + enable_managed_local_account: enableManagedLocalAccount, + }); + } catch { + errors.push("managed local account"); + } + } + + if (errors.length > 0) { + renderFlash( + "error", + `Couldn't update ${errors.join(" and ")}. Please try again.` + ); + } else { + renderFlash("success", "Successfully updated."); + } + + setIsUpdating(false); + setLockEndUserInfo(canLockEndUserInfo); + }; + + return ( +
+
+ + End users are required to authenticate with your{" "} + {" "} + when setting up new hosts. + + } + > + End user authentication + + {isEndUserAuthEnabled && ( +
+ + + End user can't edit the local account's{" "} + Account Name and +
+ Full Name in macOS Setup Assistant. These fields will + be +
+ locked to values from your IdP. + + } + > + Lock end user info +
+
+
+ )} + + Fleet generates a user (_fleetadmin) and unique password for each + host, accessible in Host details >{" "} + Show managed account. + + } + > + + Managed local account + + + ( + + )} + /> + +
+ ); +}; + +export default UsersForm; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/_styles.scss b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/_styles.scss new file mode 100644 index 00000000000..7e50aab004b --- /dev/null +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/_styles.scss @@ -0,0 +1,6 @@ +.users-form { + &__advanced-options { + margin-left: $pad-large; + margin-bottom: $pad-small; + } +} diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/index.ts b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/index.ts new file mode 100644 index 00000000000..cfcc7dea05e --- /dev/null +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/index.ts @@ -0,0 +1 @@ +export { default } from "./UsersForm"; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/index.ts b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/index.ts new file mode 100644 index 00000000000..d25de51bd03 --- /dev/null +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/index.ts @@ -0,0 +1 @@ +export { default } from "./Users"; diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index 0be02d8c32d..40095ff21c2 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -56,6 +56,7 @@ interface IUpdateSetupExperienceBody { enable_end_user_authentication?: boolean; apple_enable_release_device_manually?: boolean; macos_manual_agent_install?: boolean; + enable_managed_local_account?: boolean; } export interface IAppleSetupEnrollmentProfileResponse {