Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/__mocks__/configMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const DEFAULT_CONFIG_MDM_MOCK: IMdmConfig = {
apple_setup_assistant: null,
apple_enable_release_device_manually: false,
require_all_software_macos: false,
require_all_software_windows: false,
lock_end_user_info: false,
},
macos_migration: {
Expand Down
1 change: 1 addition & 0 deletions frontend/interfaces/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface IMdmConfig {
apple_enable_release_device_manually: boolean | null;
macos_manual_agent_install: boolean | null;
require_all_software_macos: boolean | null;
require_all_software_windows: boolean | null;
lock_end_user_info: boolean | null;
enable_create_local_admin_account?: boolean;
};
Expand Down
1 change: 1 addition & 0 deletions frontend/interfaces/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface ITeam extends ITeamSummary {
apple_enable_release_device_manually: boolean | null;
macos_manual_agent_install: boolean | null;
require_all_software_macos: boolean | null;
require_all_software_windows: boolean | null;
lock_end_user_info: boolean | null;
enable_create_local_admin_account?: boolean;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const InstallSoftware = ({
);

const isAndroidMdmEnabled = globalConfig?.mdm.android_enabled_and_configured;
const isWindowsMdmEnabled = globalConfig?.mdm.windows_enabled_and_configured;

const isLoadingConfig = isLoadingGlobalConfig || isLoadingTeamConfig;

Expand All @@ -160,7 +161,10 @@ const InstallSoftware = ({

const turnOnAndroidMdm = platform === "android" && !isAndroidMdmEnabled;

// Only Apple and Android setup experience require MDM
// Only Apple and Android setup experience require MDM. Windows admins can
// pre-stage setup-experience software without MDM, but the
// require_all_software_windows option does require Windows MDM to be on
// (gated at the checkbox level inside InstallSoftwareForm).
const turnOnMdm = turnOnAppleMdm || turnOnAndroidMdm;

return (
Expand Down Expand Up @@ -199,6 +203,14 @@ const InstallSoftware = ({
: globalConfig?.mdm?.setup_experience
?.require_all_software_macos
}
savedRequireAllSoftwareWindows={
currentTeamId
? teamConfig?.mdm?.setup_experience
?.require_all_software_windows
: globalConfig?.mdm?.setup_experience
?.require_all_software_windows
}
isWindowsMdmEnabled={!!isWindowsMdmEnabled}
router={router}
refetchSoftwareTitles={refetchSoftwareTitles}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import {
createMockSoftwarePackage,
createMockSoftwareTitle,
} from "__mocks__/softwareMock";
import mdmAPI from "services/entities/mdm";

import InstallSoftwareForm from "./InstallSoftwareForm";

const render = createCustomRenderer({ withBackendMock: true });

describe("InstallSoftware", () => {
afterEach(() => {
jest.restoreAllMocks();
});

it("should render the expected message if there are no software titles to select from", () => {
render(
<InstallSoftwareForm
Expand Down Expand Up @@ -126,7 +131,7 @@ describe("InstallSoftware", () => {
});
});

it('should render the "Cancel setup if software install fails" form for macos platform', async () => {
it('should render the "Cancel setup if software fails" form for macos platform', async () => {
render(
<InstallSoftwareForm
savedRequireAllSoftwareMacOS
Expand Down Expand Up @@ -155,13 +160,93 @@ describe("InstallSoftware", () => {

await waitFor(() => {
const checkbox = screen.getByRole("checkbox", {
name: /Cancel setup if software install fails/,
name: /Cancel setup if software fails/,
});
expect(checkbox).toBeVisible();
expect(checkbox).toBeChecked();
});
});

it('should render the "Cancel setup if software fails" form for windows platform', async () => {
render(
<InstallSoftwareForm
savedRequireAllSoftwareWindows
isWindowsMdmEnabled
currentTeamId={1}
softwareTitles={[
createMockSoftwareTitle({
software_package: createMockSoftwarePackage({
install_during_setup: true,
}),
}),
createMockSoftwareTitle(),
]}
hasManualAgentInstall={false}
platform="windows"
router={createMockRouter()}
refetchSoftwareTitles={noop}
/>
);

await waitFor(() => {
const checkbox = screen.getByRole("checkbox", {
name: /Cancel setup if software fails/,
});
expect(checkbox).toBeVisible();
expect(checkbox).toBeChecked();
});
});

it("calls the Windows require-all API on Save when the Windows checkbox is toggled", async () => {
const updateRequireAllSoftwareWindowsSpy = jest
.spyOn(mdmAPI, "updateRequireAllSoftwareWindows")
.mockResolvedValue({});

const { user } = render(
<InstallSoftwareForm
savedRequireAllSoftwareWindows={false}
isWindowsMdmEnabled
currentTeamId={1}
softwareTitles={[createMockSoftwareTitle()]}
hasManualAgentInstall={false}
platform="windows"
router={createMockRouter()}
refetchSoftwareTitles={noop}
/>
);

await user.click(
screen.getByRole("checkbox", {
name: /Cancel setup if software fails/,
})
);
await user.click(screen.getByRole("button", { name: "Save" }));

await waitFor(() => {
expect(updateRequireAllSoftwareWindowsSpy).toHaveBeenCalledWith(1, true);
});
});
Comment thread
getvictor marked this conversation as resolved.

it("disables the Windows checkbox when Windows MDM is not configured", async () => {
render(
<InstallSoftwareForm
savedRequireAllSoftwareWindows={false}
isWindowsMdmEnabled={false}
currentTeamId={1}
softwareTitles={[createMockSoftwareTitle()]}
hasManualAgentInstall={false}
platform="windows"
router={createMockRouter()}
refetchSoftwareTitles={noop}
/>
);

const checkbox = screen.getByRole("checkbox", {
name: /Cancel setup if software fails/,
});
expect(checkbox).toHaveAttribute("aria-disabled", "true");
});

it("should disable adding software for macos with manual agent install", async () => {
render(
<InstallSoftwareForm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { ISoftwareTitle } from "interfaces/software";
import { INotification } from "interfaces/notification";

import { NotificationContext } from "context/notification";
import useGitOpsMode from "hooks/useGitOpsMode";
import mdmAPI from "services/entities/mdm";

import Button from "components/buttons/Button";
Expand Down Expand Up @@ -75,6 +74,8 @@ interface IInstallSoftwareFormProps {
softwareTitles: ISoftwareTitle[] | null;
platform: SetupExperiencePlatform;
savedRequireAllSoftwareMacOS?: boolean | null;
savedRequireAllSoftwareWindows?: boolean | null;
isWindowsMdmEnabled?: boolean;
router: InjectedRouter;
refetchSoftwareTitles: () => void;
}
Expand All @@ -85,31 +86,40 @@ const InstallSoftwareForm = ({
softwareTitles,
platform,
savedRequireAllSoftwareMacOS,
savedRequireAllSoftwareWindows,
isWindowsMdmEnabled = false,
router,
refetchSoftwareTitles,
}: IInstallSoftwareFormProps) => {
const noSoftwareUploaded = hasNoSoftwareUploaded(softwareTitles);
const { renderFlash, renderMultiFlash } = useContext(NotificationContext);
const { gitOpsModeEnabled } = useGitOpsMode("software");
const [requireAllSoftwareMacOS, setRequireAllSoftwareMacOS] = useState(
savedRequireAllSoftwareMacOS ?? false
);
const [requireAllSoftwareWindows, setRequireAllSoftwareWindows] = useState(
savedRequireAllSoftwareWindows ?? false
);
const [isSaving, setIsSaving] = useState(false);

const initialSelectedSoftware = useMemo(
() => (softwareTitles ? initializeSelectedSoftwareIds(softwareTitles) : []),
[softwareTitles]
);

// Track if the user changed the macOS checkbox since the last save.
// Track if the user changed the require-all checkbox since the last save.
// We don't compare against props here to avoid races with parent refetch timing.
const [touchedRequireAll, setTouchedRequireAll] = useState(false);

const handleChangeRequireAll = (value: boolean) => {
const handleChangeRequireAllMacOS = (value: boolean) => {
setRequireAllSoftwareMacOS(value);
setTouchedRequireAll(true);
};

const handleChangeRequireAllWindows = (value: boolean) => {
setRequireAllSoftwareWindows(value);
setTouchedRequireAll(true);
};

const [selectedSoftwareIds, setSelectedSoftwareIds] = useState<number[]>(
initialSelectedSoftware
);
Expand All @@ -136,7 +146,8 @@ const InstallSoftwareForm = ({
);

const shouldUpdateSoftware = isSoftwareSelectionDirty;
const shouldUpdateRequireAll = platform === "macos" && touchedRequireAll;
const shouldUpdateRequireAll =
(platform === "macos" || platform === "windows") && touchedRequireAll;

const onClickSave = async (evt: React.FormEvent) => {
evt.preventDefault();
Expand Down Expand Up @@ -170,13 +181,20 @@ const InstallSoftwareForm = ({
}
}

// 2. macOS “require all software update
// 2. "require all software" update (macOS or Windows)
if (shouldUpdateRequireAll) {
try {
await mdmAPI.updateRequireAllSoftwareMacOS(
currentTeamId,
requireAllSoftwareMacOS
);
if (platform === "windows") {
await mdmAPI.updateRequireAllSoftwareWindows(
currentTeamId,
requireAllSoftwareWindows
);
} else {
Comment thread
getvictor marked this conversation as resolved.
await mdmAPI.updateRequireAllSoftwareMacOS(
currentTeamId,
requireAllSoftwareMacOS
);
}
Comment thread
getvictor marked this conversation as resolved.
hadSuccess = true;
setTouchedRequireAll(false);
} catch (e) {
Comment on lines +184 to 200
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The new Windows checkbox in InstallSoftwareForm persists require_all_software_windows on the backend, but the device-user setup-experience UI never reads it: IDeviceGlobalConfig.mdm (frontend/interfaces/config.ts:103-107) and DeviceGlobalMDMConfig (server/fleet/app.go:1732-1735) only expose require_all_software_macos, and DeviceUserPage (frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx:636-639) gates the value on isAppleHost, so for Windows hosts requireAllSoftware is always false. The new admin checkbox is therefore functionally write-only — fix by extending DeviceGlobalMDMConfig/IDeviceGlobalConfig with require_all_software_windows and updating DeviceUserPage to use it for Windows hosts.

Extended reasoning...

What the bug is

The PR adds an admin-side write path for require_all_software_windows (the new Windows checkbox in InstallSoftwareForm.tsx, plus mdmAPI.updateRequireAllSoftwareWindows), but it does not add the corresponding read path for the device-user setup experience. As a result, no matter how the admin sets the Windows checkbox, the device-user UI on a Windows host can never observe it, and the failure-on-software-install flow that is the whole point of this feature can never trigger for Windows.

The exact code path

  1. frontend/interfaces/config.ts:103-107IDeviceGlobalConfig.mdm only declares enabled_and_configured and require_all_software_macos. No Windows field was added in this PR.
  2. server/fleet/app.go:1732-1735 — the backend DeviceGlobalMDMConfig (the device-facing payload) likewise only has RequireAllSoftware (json: require_all_software_macos). The Windows flag exists on AppConfig but is never copied into the device payload.
  3. frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx:636-639 passes:
    requireAllSoftware={
      (isAppleHost && globalConfig?.mdm?.require_all_software_macos) ?? false
    }
    For a Windows host, isAppleHost is false, so the short-circuit always yields false.
  4. frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx:36:
    const failedSoftware = requireAllSoftware
      ? getFailedSoftwareInstall(setupSteps)
      : null;
    With requireAllSoftware === false, failedSoftware is always null, so the failure UI never renders.

Why existing code does not prevent it

Windows IS routed through SettingUpYourDeviceDeviceUserPage.tsx:356-359 includes host?.platform === "windows" in isSetupExperienceSoftwareEnabledPlatform. So the component renders; it just receives requireAllSoftware=false regardless of the saved setting. The PR also did not touch DeviceUserPage.tsx, IDeviceGlobalConfig, or DeviceGlobalMDMConfig, so the read side is unchanged from before this PR.

Step-by-step proof

  1. Admin visits Setup Experience → Install Software → Windows tab and checks "Cancel setup if software fails", then clicks Save.
  2. InstallSoftwareForm calls mdmAPI.updateRequireAllSoftwareWindows(teamId, true), which PATCHes /setup_experience with { require_all_software_windows: true }. Backend persists this on AppConfig.MDM.MacOSSetup.RequireAllSoftwareWindows.
  3. A Windows host enrolls. The device-user UI loads /api/_version_/fleet/device/{token}/desktop (or equivalent) and gets back a DeviceGlobalConfig whose mdm payload is { enabled_and_configured, require_all_software_macos } only — no require_all_software_windows field is serialized (server/fleet/app.go:1732-1735).
  4. DeviceUserPage evaluates requireAllSoftware = (isAppleHost && globalConfig?.mdm?.require_all_software_macos) ?? false. isAppleHost is false for Windows → expression evaluates to false ?? falsefalse.
  5. SettingUpYourDevice receives requireAllSoftware={false}. Even if a software install fails, failedSoftware stays null, so the user is never shown "Device setup failed" and setup is never canceled.

Net effect: the Windows checkbox is functionally write-only on the frontend, which directly contradicts the PR title "Add Windows support for Require all software during setup experience".

Impact

The core feature this PR claims to deliver does not work end-to-end on Windows. Admins can toggle the setting and the value will persist, but no Windows device will ever experience the failure flow it controls. Shipping this as-is would be misleading and likely require a follow-up bug report once a customer notices.

How to fix

  • Backend: add RequireAllSoftwareWindows bool json:"require_all_software_windows"`` to DeviceGlobalMDMConfig in `server/fleet/app.go` and populate it from the effective team/global config when building the device response (see `server/service/devices.go` around lines 206-229).
  • Frontend types: add require_all_software_windows: boolean | null to IDeviceGlobalConfig.mdm in frontend/interfaces/config.ts.
  • Frontend UI: in DeviceUserPage.tsx:636-639, change the prop to something like:
    requireAllSoftware={
      ((isAppleHost && globalConfig?.mdm?.require_all_software_macos) ||
       (host?.platform === "windows" && globalConfig?.mdm?.require_all_software_windows)) ?? false
    }

This was independently flagged by Qodo in the same PR review as "Windows require-all not used".

🔬 also observed by qodo

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MDM BYOD setup experience will be handled in a separate PR/story.

Expand All @@ -185,7 +203,7 @@ const InstallSoftwareForm = ({
alertType: "error",
isVisible: true,
message:
"Couldn't update 'Cancel setup if software install fails'. Please try again.",
"Couldn't update 'Cancel setup if software fails'. Please try again.",
persistOnPageChange: false,
});
}
Expand Down Expand Up @@ -267,15 +285,54 @@ const InstallSoftwareForm = ({
/>
{platform === "macos" && (
<div className={`${baseClass}__macos_options`}>
<Checkbox
disabled={gitOpsModeEnabled || manualAgentInstallBlockingSoftware}
value={requireAllSoftwareMacOS}
onChange={handleChangeRequireAll}
>
<TooltipWrapper tipContent="If any software fails, the end user won't be let through, and will see a prompt to contact their IT admin. Remaining software installs will be canceled.">
Cancel setup if software install fails
</TooltipWrapper>
</Checkbox>
<GitOpsModeTooltipWrapper
tipOffset={6}
position="bottom-start"
entityType="software"
renderChildren={(disableChildren) => (
<Checkbox
disabled={
disableChildren || manualAgentInstallBlockingSoftware
}
value={requireAllSoftwareMacOS}
onChange={handleChangeRequireAllMacOS}
>
<TooltipWrapper
tipContent="If any software fails, the end user will be prompted to restart setup. Remaining software installs will be canceled."
disableTooltip={disableChildren}
>
Cancel setup if software fails
</TooltipWrapper>
</Checkbox>
)}
/>
</div>
)}
{platform === "windows" && (
<div className={`${baseClass}__windows_options`}>
<GitOpsModeTooltipWrapper
tipOffset={6}
position="bottom-start"
entityType="software"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren || !isWindowsMdmEnabled}
value={requireAllSoftwareWindows}
onChange={handleChangeRequireAllWindows}
>
<TooltipWrapper
tipContent={
isWindowsMdmEnabled
? "If any software fails, the end user will be prompted to restart setup. Remaining software installs will be canceled."
: "Turn on Windows MDM to use this option."
}
disableTooltip={disableChildren}
>
Cancel setup if software fails
</TooltipWrapper>
</Checkbox>
)}
/>
</div>
)}
<GitOpsModeTooltipWrapper
Expand Down
12 changes: 10 additions & 2 deletions frontend/services/entities/mdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,21 @@ const mdmService = {
},

updateRequireAllSoftwareMacOS: (teamId: number, isEnabled: boolean) => {
const { MDM_SETUP } = endpoints;
return sendRequest("PATCH", MDM_SETUP, {
const { MDM_SETUP_EXPERIENCE } = endpoints;
return sendRequest("PATCH", MDM_SETUP_EXPERIENCE, {
fleet_id: teamId,
require_all_software_macos: isEnabled,
});
},

updateRequireAllSoftwareWindows: (teamId: number, isEnabled: boolean) => {
const { MDM_SETUP_EXPERIENCE } = endpoints;
return sendRequest("PATCH", MDM_SETUP_EXPERIENCE, {
fleet_id: teamId,
require_all_software_windows: isEnabled,
});
},

updateSetupExperienceSettings: (updateData: IUpdateSetupExperienceBody) => {
const { MDM_SETUP_EXPERIENCE } = endpoints;
const body = {
Expand Down
Loading