feature: windows profiles (#15349)

This commit is contained in:
Roberto Dip
2023-11-29 09:32:42 -05:00
committed by GitHub
parent f6cb052e7a
commit f439432fae
58 changed files with 1575 additions and 1307 deletions
+4
View File
@@ -0,0 +1,4 @@
- Introduced `POST /mdm/profiles` for uploading Windows or macOS custom profiles.
- New endpoints for managing MDM profiles: `DELETE /mdm/profiles/{id}`, `GET /mdm/profiles/{id}`, `GET /mdm/profiles` (paginated list), `GET /mdm/profiles/summary`.
- Updated `GET /api/v1/hosts/:id` to include Windows MDM profiles.
- Fleetctl now supports configuration of Windows MDM profiles.
+1
View File
@@ -0,0 +1 @@
- add UI to upload, delete, download, and view windows custom MDM profiles.
+3 -3
View File
@@ -976,9 +976,9 @@ func newMDMProfileManager(
schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
return service.ReconcileAppleProfiles(ctx, ds, commander, logger)
}),
//schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
// return service.ReconcileWindowsProfiles(ctx, ds, logger)
//}),
schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
return service.ReconcileWindowsProfiles(ctx, ds, logger)
}),
)
return s, nil
+3 -3
View File
@@ -160,7 +160,7 @@ func TestApplyTeamSpecs(t *testing.T) {
return nil
}
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, tmID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
return nil
}
@@ -906,7 +906,7 @@ func TestApplyAsGitOps(t *testing.T) {
teamEnrollSecrets = secrets
return nil
}
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, tmID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs, profileIDs []uint, profileUUIDs, hostUUIDs []string) error {
@@ -1086,7 +1086,7 @@ spec:
}, savedTeam.Config.MDM)
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, teamEnrollSecrets)
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
assert.True(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
assert.True(t, ds.BatchSetMDMProfilesFuncInvoked)
// add macos setup assistant to team
name = writeTmpYml(t, fmt.Sprintf(`
+1 -1
View File
@@ -2015,7 +2015,7 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
}
return nil, fmt.Errorf("team not found: %s", name)
}
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, tmID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs, profileIDs []uint, profileUUIDs, uuids []string) error {
@@ -104,6 +104,9 @@
"enable_end_user_authentication": false,
"macos_setup_assistant": null
},
"windows_settings": {
"custom_settings": null
},
"end_user_authentication": {
"entity_id": "",
"issuer_uri": "",
@@ -33,6 +33,8 @@ spec:
bootstrap_package:
enable_end_user_authentication: false
macos_setup_assistant:
windows_settings:
custom_settings: null
end_user_authentication:
idp_name: ""
issuer_uri: ""
@@ -62,6 +62,9 @@
"enable_end_user_authentication": false,
"macos_setup_assistant": null
},
"windows_settings": {
"custom_settings": null
},
"end_user_authentication": {
"entity_id": "",
"issuer_uri": "",
@@ -33,6 +33,8 @@ spec:
bootstrap_package:
enable_end_user_authentication: false
macos_setup_assistant:
windows_settings:
custom_settings:
end_user_authentication:
idp_name: ""
issuer_uri: ""
+6
View File
@@ -36,6 +36,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null
},
"windows_settings": {
"custom_settings": null
}
},
"scripts": null,
@@ -97,6 +100,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null
},
"windows_settings": {
"custom_settings": null
}
},
"scripts": null,
+4
View File
@@ -13,6 +13,8 @@ spec:
deadline: null
macos_settings:
custom_settings:
windows_settings:
custom_settings:
macos_setup:
bootstrap_package:
enable_end_user_authentication: false
@@ -43,6 +45,8 @@ spec:
deadline: "2021-12-14"
macos_settings:
custom_settings:
windows_settings:
custom_settings:
macos_setup:
bootstrap_package:
enable_end_user_authentication: false
@@ -26,6 +26,8 @@ spec:
webhook_url: ""
macos_settings:
custom_settings: null
windows_settings:
custom_settings: null
macos_setup:
bootstrap_package: null
enable_end_user_authentication: false
@@ -26,6 +26,8 @@ spec:
webhook_url: ""
macos_settings:
custom_settings: null
windows_settings:
custom_settings: null
macos_setup:
bootstrap_package: %s
enable_end_user_authentication: false
@@ -10,6 +10,8 @@ spec:
enable_disk_encryption: false
macos_settings:
custom_settings: null
windows_settings:
custom_settings: null
macos_setup:
bootstrap_package: null
enable_end_user_authentication: false
@@ -31,6 +33,8 @@ spec:
enable_disk_encryption: false
macos_settings:
custom_settings: null
windows_settings:
custom_settings: null
macos_setup:
bootstrap_package: null
macos_setup_assistant: null
@@ -10,6 +10,8 @@ spec:
enable_disk_encryption: false
macos_settings:
custom_settings: null
windows_settings:
custom_settings: null
macos_setup:
bootstrap_package: %s
enable_end_user_authentication: false
@@ -31,6 +33,8 @@ spec:
enable_disk_encryption: false
macos_settings:
custom_settings: null
windows_settings:
custom_settings: null
macos_setup:
bootstrap_package: %s
macos_setup_assistant: %s
@@ -17,6 +17,8 @@ spec:
macos_updates:
deadline: null
minimum_version: null
windows_settings:
custom_settings: null
scripts: null
name: tm1
+9 -9
View File
@@ -935,16 +935,16 @@ func (svc *Service) editTeamFromSpec(
}
team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = spec.MDM.MacOSSetup.EnableEndUserAuthentication
// if spec.MDM.WindowsSettings.CustomSettings.Set {
// if !appCfg.MDM.WindowsEnabledAndConfigured &&
// len(spec.MDM.WindowsSettings.CustomSettings.Value) > 0 &&
// !server.SliceStringsMatch(team.Config.MDM.WindowsSettings.CustomSettings.Value, spec.MDM.WindowsSettings.CustomSettings.Value) {
// return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("windows_settings.custom_settings",
// `Couldnt edit windows_settings.custom_settings. Windows MDM isnt turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`))
// }
if spec.MDM.WindowsSettings.CustomSettings.Set {
if !appCfg.MDM.WindowsEnabledAndConfigured &&
len(spec.MDM.WindowsSettings.CustomSettings.Value) > 0 &&
!server.SliceStringsMatch(team.Config.MDM.WindowsSettings.CustomSettings.Value, spec.MDM.WindowsSettings.CustomSettings.Value) {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("windows_settings.custom_settings",
`Couldnt edit windows_settings.custom_settings. Windows MDM isnt turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`))
}
// team.Config.MDM.WindowsSettings.CustomSettings = spec.MDM.WindowsSettings.CustomSettings
// }
team.Config.MDM.WindowsSettings.CustomSettings = spec.MDM.WindowsSettings.CustomSettings
}
if spec.Scripts.Set {
team.Config.Scripts = spec.Scripts
+2
View File
@@ -18,9 +18,11 @@ const DEFAULT_MDM_PROFILE_DATA: IMdmProfile = {
profile_id: 1,
team_id: 0,
name: "Test Profile",
platform: "darwin",
identifier: "com.test.profile",
created_at: "2021-01-01T00:00:00Z",
updated_at: "2021-01-01T00:00:00Z",
checksum: "123abc",
};
export const createMockMdmProfile = (
@@ -6,9 +6,11 @@ import FleetIcon from "components/icons/FleetIcon";
const baseClass = "pagination";
// TODO: Refactor to typescript
// Already seeing issues with prop types (currentPage passed through as string instead of number)
/**
* WARNING: DEPRICATED:
* This pagination component is DEPRICATED. It is being kept around until we replace its
* use. For now use the Pagination component in the pages/ManageControlsPage/components.
*/
class Pagination extends PureComponent {
static propTypes = {
currentPage: PropTypes.number,
+7 -9
View File
@@ -55,29 +55,27 @@ export interface IMdmSummaryResponse {
mobile_device_management_solution: IMdmSolution[] | null;
}
type SupportedMdmPlatform = "darwin" | "windows";
type ProfilePlatform = "darwin" | "windows";
export interface IMdmProfile {
profile_id: number;
profile_id: number | string; // string for windows profiles
team_id: number;
name: string;
identifier: string;
platform: ProfilePlatform;
identifier: string | null; // null for windows profiles
created_at: string;
updated_at: string;
}
export interface IMdmProfilesResponse {
profiles: IMdmProfile[] | null;
checksum: string | null; // null for windows profiles
}
export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
export type MacMdmProfileOperationType = "remove" | "install";
export type ProfileOperationType = "remove" | "install";
export interface IHostMdmProfile {
profile_id: number;
name: string;
operation_type: MacMdmProfileOperationType | null;
operation_type: ProfileOperationType | null;
status: MdmProfileStatus;
detail: string;
}
@@ -16,6 +16,7 @@ const baseClass = "os-settings";
interface IOSSettingsProps {
params: Params;
router: InjectedRouter;
currentPage: number;
location: {
search: string;
};
@@ -23,6 +24,7 @@ interface IOSSettingsProps {
const OSSettings = ({
router,
currentPage,
location: { search: queryString },
params,
}: IOSSettingsProps) => {
@@ -41,8 +43,7 @@ const OSSettings = ({
isLoading: isLoadingAggregateProfileStatus,
} = useQuery(
["aggregateProfileStatuses", teamId],
() =>
mdmAPI.getAggregateProfileStatuses(teamId, config?.mdm_enabled ?? false),
() => mdmAPI.getProfilesStatusSummary(teamId),
{
refetchOnWindowFocus: false,
retry: false,
@@ -87,6 +88,8 @@ const OSSettings = ({
key={teamId}
currentTeamId={teamId}
onMutation={refetchAggregateProfileStatus}
router={router}
currentPage={currentPage}
/>
}
/>
@@ -1,14 +1,18 @@
import React, { useContext, useRef, useState } from "react";
import React, { useCallback, useContext, useRef, useState } from "react";
import { InjectedRouter } from "react-router";
import { useQuery } from "react-query";
import { IMdmProfile, IMdmProfilesResponse } from "interfaces/mdm";
import mdmAPI from "services/entities/mdm";
import { IMdmProfile } from "interfaces/mdm";
import mdmAPI, { IMdmProfilesResponse } from "services/entities/mdm";
import { NotificationContext } from "context/notification";
import PATHS from "router/paths";
import CustomLink from "components/CustomLink";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
import Pagination from "pages/ManageControlsPage/components/Pagination";
import UploadList from "../../../components/UploadList";
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
@@ -16,10 +20,14 @@ import ProfileListItem from "./components/ProfileListItem";
import ProfileListHeading from "./components/ProfileListHeading";
import ProfileUploader from "./components/ProfileUploader";
const PROFILES_PER_PAGE = 10;
const baseClass = "custom-settings";
interface ICustomSettingsProps {
currentTeamId: number;
router: InjectedRouter; // v3
currentPage: number;
/** handler that fires when a change occures on the section (e.g. disk encryption
* enabled, profile uploaded) */
onMutation: () => void;
@@ -27,6 +35,8 @@ interface ICustomSettingsProps {
const CustomSettings = ({
currentTeamId,
router,
currentPage,
onMutation,
}: ICustomSettingsProps) => {
const { renderFlash } = useContext(NotificationContext);
@@ -35,21 +45,27 @@ const CustomSettings = ({
const selectedProfile = useRef<IMdmProfile | null>(null);
const onClickDelete = (profile: IMdmProfile) => {
selectedProfile.current = profile;
setShowDeleteProfileModal(true);
};
const {
data: profiles,
data: profilesData,
isLoading: isLoadingProfiles,
isError: isErrorProfiles,
refetch: refetchProfiles,
} = useQuery<IMdmProfilesResponse, unknown, IMdmProfile[] | null>(
["profiles", currentTeamId],
() => mdmAPI.getProfiles(currentTeamId),
} = useQuery<IMdmProfilesResponse, unknown>(
[
{
scope: "profiles",
team_id: currentTeamId,
page: currentPage,
per_page: PROFILES_PER_PAGE,
},
],
() =>
mdmAPI.getProfiles({
team_id: currentTeamId,
page: currentPage,
per_page: PROFILES_PER_PAGE,
}),
{
select: (data) => data.profiles,
refetchOnWindowFocus: false,
}
);
@@ -64,7 +80,7 @@ const CustomSettings = ({
setShowDeleteProfileModal(false);
};
const onDeleteProfile = async (profileId: number) => {
const onDeleteProfile = async (profileId: number | string) => {
try {
await mdmAPI.deleteProfile(profileId);
refetchProfiles();
@@ -78,6 +94,24 @@ const CustomSettings = ({
}
};
// pagination controls
const path = PATHS.CONTROLS_CUSTOM_SETTINGS.concat(
`?team_id=${currentTeamId}`
);
const onPrevPage = useCallback(() => {
router.push(path.concat(`&page=${currentPage - 1}`));
}, [router, path, currentPage]);
const onNextPage = useCallback(() => {
router.push(path.concat(`&page=${currentPage + 1}`));
}, [router, path, currentPage]);
const onClickDelete = (profile: IMdmProfile) => {
selectedProfile.current = profile;
setShowDeleteProfileModal(true);
};
const renderProfileList = () => {
if (isLoadingProfiles) {
return <Spinner />;
@@ -87,18 +121,32 @@ const CustomSettings = ({
return <DataError />;
}
if (!profiles || profiles.length === 0) {
if (
!profilesData ||
!profilesData.profiles ||
profilesData.profiles.length === 0
) {
return null;
}
const { profiles, meta } = profilesData;
return (
<UploadList
listItems={profiles}
HeadingComponent={ProfileListHeading}
ListItemComponent={({ listItem }) => (
<ProfileListItem profile={listItem} onDelete={onClickDelete} />
)}
/>
<>
<UploadList
listItems={profiles}
HeadingComponent={ProfileListHeading}
ListItemComponent={({ listItem }) => (
<ProfileListItem profile={listItem} onDelete={onClickDelete} />
)}
/>
<Pagination
className={`${baseClass}__pagination-controls`}
disableNext={!meta.has_next_results}
disablePrev={!meta.has_previous_results}
onNextPage={onNextPage}
onPrevPage={onPrevPage}
/>
</>
);
};
@@ -29,6 +29,12 @@
margin: 0;
}
&__pagination-controls {
display: flex;
justify-content: flex-end;
margin: $pad-large 0;
}
&__file-uploader {
margin-top: $pad-xxlarge;
}
@@ -7,9 +7,9 @@ import Button from "components/buttons/Button";
interface DeleteProfileModalProps {
profileName: string;
profileId: number;
profileId: number | string;
onCancel: () => void;
onDelete: (profileId: number) => void;
onDelete: (profileId: number | string) => void;
}
const baseClass = "delete-profile-modal";
@@ -43,7 +43,7 @@ const DeleteProfileModal = ({
<p>
This action will delete configuration profile{" "}
<span className={`${baseClass}__profile-name`}>{profileName}</span>{" "}
from all macOS hosts{messageSuffix}.
from all hosts{messageSuffix}.
</p>
<div className="modal-cta-wrap">
<Button
@@ -12,12 +12,19 @@ import ListItem from "components/ListItem";
const baseClass = "profile-list-item";
interface IProfileDetailsProps {
platform: string;
createdAt: string;
}
const ProfileDetails = ({ createdAt }: IProfileDetailsProps) => {
const ProfileDetails = ({ platform, createdAt }: IProfileDetailsProps) => {
const getPlatformName = () => {
return platform === "darwin" ? "macOS" : "Windows";
};
return (
<div className={`${baseClass}__profile-details`}>
<span className={`${baseClass}__platform`}>{getPlatformName()}</span>
<span>&bull;</span>
<span className={`${baseClass}__list-item-uploaded`}>
{`Uploaded ${formatDistanceToNow(new Date(createdAt))} ago`}
</span>
@@ -34,7 +41,8 @@ const ProfileListItem = ({ profile, onDelete }: IProfileListItemProps) => {
const onClickDownload = async () => {
const fileContent = await mdmAPI.downloadProfile(profile.profile_id);
const formatDate = format(new Date(), "yyyy-MM-dd");
const filename = `${formatDate}_${profile.name}.mobileconfig`;
const extension = profile.platform === "darwin" ? "mobileconfig" : "xml";
const filename = `${formatDate}_${profile.name}.${extension}`;
const file = new File([fileContent], filename);
FileSaver.saveAs(file);
};
@@ -44,7 +52,12 @@ const ProfileListItem = ({ profile, onDelete }: IProfileListItemProps) => {
className={baseClass}
graphic="file-configuration-profile"
title={profile.name}
details={<ProfileDetails createdAt={profile.created_at} />}
details={
<ProfileDetails
platform={profile.platform}
createdAt={profile.created_at}
/>
}
actions={
<>
<Button
@@ -7,7 +7,7 @@ import mdmAPI from "services/entities/mdm";
import FileUploader from "components/FileUploader";
import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
import { getErrorMessage } from "./helpers";
const baseClass = "profile-uploader";
@@ -34,17 +34,6 @@ const ProfileUploader = ({
const file = files[0];
if (
// file.type might be empty on some systems as uncommon file extensions
// would return an empty string.
(file.type !== "" && file.type !== "application/x-apple-aspen-config") ||
!file.name.includes(".mobileconfig")
) {
renderFlash("error", UPLOAD_ERROR_MESSAGES.wrongType.message);
setShowLoading(false);
return;
}
try {
await mdmAPI.uploadProfile(file, currentTeamId);
renderFlash("success", "Successfully uploaded!");
@@ -61,8 +50,8 @@ const ProfileUploader = ({
return (
<FileUploader
graphicName="file-configuration-profile"
message="Configuration profile (.mobileconfig)"
accept=".mobileconfig,application/x-apple-aspen-config"
message="Configuration profile (.mobileconfig for macOS or .xml for Windows)"
accept=".mobileconfig,application/x-apple-aspen-config,.xml"
isLoading={showLoading}
onFileUpload={onFileUpload}
className={`${baseClass}__file-uploader`}
@@ -1,52 +0,0 @@
import { AxiosResponse } from "axios";
import { IApiError } from "interfaces/errors";
export const UPLOAD_ERROR_MESSAGES = {
wrongType: {
condition: () => false,
message: "Couldnt upload. The file should be a .mobileconfig file.",
},
identifierExists: {
condition: (reason: string) =>
reason.includes("MDMAppleConfigProfile.PayloadIdentifier"),
message:
"Couldnt upload. A configuration profile with this identifier (PayloadIdentifier) already exists.",
},
nameExists: {
condition: (reason: string) => reason.includes("PayloadDisplayName"),
message:
"Couldnt upload. A configuration profile with this name (PayloadDisplayName) already exists.",
},
encrypted: {
condition: (reason: string) => reason.includes("encrypted"),
message: "Couldnt upload. The file should be unencrypted.",
},
validXML: {
condition: (reason: string) => reason.includes("parsing XML"),
message: "Couldnt upload. The file should include valid XML.",
},
fileVault: {
condition: (reason: string) =>
reason.includes("unsupported PayloadType(s): com.apple.MCX.FileVault2"),
message:
"Couldnt upload. The configuration profile cant include FileVault settings. To control these settings, go to Disk encryption.",
},
default: {
condition: () => false,
message: "Couldnt upload. Please try again.",
},
};
export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
const apiReason = err.data.errors[0].reason;
const error = Object.values(UPLOAD_ERROR_MESSAGES).find((errType) =>
errType.condition(apiReason)
);
if (!error) {
return UPLOAD_ERROR_MESSAGES.default.message;
}
return error.message;
};
@@ -0,0 +1,37 @@
import React from "react";
import { AxiosResponse } from "axios";
import { IApiError } from "interfaces/errors";
/** We want to add some additional messageing to some of the error messages so
* we add them in this function. Otherwise, we'll just return the error message from the
* API.
*/
// eslint-disable-next-line import/prefer-default-export
export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
const apiReason = err.data.errors[0].reason;
if (
apiReason.includes(
"The configuration profile cant include BitLocker settings."
)
) {
return (
<span>
{apiReason} To control these settings, go to <b>Disk encryption</b>.
</span>
);
}
if (
apiReason.includes(
"The configuration profile cant include Windows update settings."
)
) {
return (
<span>
{apiReason} To control these settings, go to <b>OS updates</b>.
</span>
);
}
return apiReason;
};
@@ -24,7 +24,7 @@ import ScriptUploader from "./components/ScriptUploader";
const baseClass = "scripts";
const SCRIPTS_PER_PAGE = 10; // TODO: confirm this is the desired default
const SCRIPTS_PER_PAGE = 10;
interface IScriptsProps {
router: InjectedRouter; // v3
@@ -0,0 +1,52 @@
import React from "react";
import classnames from "classnames";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
const baseClass = "pagination-new";
interface IPaginationProps {
disableNext?: boolean;
disablePrev?: boolean;
onNextPage?: () => void;
onPrevPage?: () => void;
className?: string;
}
/**
* This is the new pagination component that we will want to replace other pagination
* components with. Going forward this should be the component used for pagination.
*/
const Pagination = ({
disableNext,
disablePrev,
onNextPage,
onPrevPage,
className,
}: IPaginationProps) => {
const classNames = classnames(baseClass, className);
return (
<div className={classNames}>
<Button
variant="unstyled"
disabled={disablePrev}
onClick={onPrevPage}
className={`${baseClass}__pagination-button`}
>
<Icon name="chevron-left" color="core-fleet-blue" /> Previous
</Button>
<Button
variant="unstyled"
disabled={disableNext}
onClick={onNextPage}
className={`${baseClass}__pagination-button`}
>
Next <Icon name="chevron-right" color="core-fleet-blue" />
</Button>
</div>
);
};
export default Pagination;
@@ -0,0 +1,16 @@
.pagination-new {
display: flex;
align-items: center;
gap: $pad-large;
&__pagination-button {
color: $core-vibrant-blue;
font-weight: $bold;
padding: $pad-small;
}
button:hover,
button:focus {
background-color: $ui-vibrant-blue-10;
}
}
@@ -0,0 +1 @@
export { default } from "./Pagination";
@@ -1,13 +1,13 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import { MacMdmProfileOperationType } from "interfaces/mdm";
import { ProfileOperationType } from "interfaces/mdm";
import OSSettingStatusCell from "./OSSettingStatusCell";
describe("OS setting status cell", () => {
it("Correctly displays the status text of a profile", () => {
const status = "verifying";
const operationType: MacMdmProfileOperationType = "install";
const operationType: ProfileOperationType = "install";
render(
<OSSettingStatusCell
@@ -22,7 +22,7 @@ describe("OS setting status cell", () => {
it("Correctly displays the tooltip text for a profile", async () => {
const status = "verifying";
const operationType: MacMdmProfileOperationType = "install";
const operationType: ProfileOperationType = "install";
const customRender = createCustomRenderer();
@@ -3,140 +3,28 @@ import ReactTooltip from "react-tooltip";
import { uniqueId } from "lodash";
import Icon from "components/Icon";
import { IconNames } from "components/icons";
import TextCell from "components/TableContainer/DataTable/TextCell";
import {
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
MacMdmProfileOperationType,
ProfileOperationType,
} from "interfaces/mdm";
import {
isMdmProfileStatus,
OsSettingsTableStatusValue,
} from "../OSSettingsTableConfig";
import TooltipContent, {
TooltipInnerContentFunc,
TooltipInnerContentOption,
} from "./components/Tooltip/TooltipContent";
import TooltipInnerContentActionRequired from "./components/Tooltip/ActionRequired";
import TooltipContent from "./components/Tooltip/TooltipContent";
import {
PROFILE_DISPLAY_CONFIG,
ProfileDisplayOption,
WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG,
} from "./helpers";
const baseClass = "os-setting-status-cell";
type ProfileDisplayOption = {
statusText: string;
iconName: IconNames;
tooltip: TooltipInnerContentOption | null;
} | null;
type OperationTypeOption = Record<
OsSettingsTableStatusValue,
ProfileDisplayOption
>;
type ProfileDisplayConfig = Record<
MacMdmProfileOperationType,
OperationTypeOption
>;
const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
install: {
pending: {
statusText: "Enforcing (pending)",
iconName: "pending-outline",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The hosts will receive the MDM command to turn on disk encryption " +
"when the hosts come online."
: "The host will receive the MDM command to install the configuration profile when the " +
"host comes online.",
},
action_required: {
statusText: "Action required (pending)",
iconName: "pending-outline",
tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc,
},
verified: {
statusText: "Verified",
iconName: "success",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host turned disk encryption on and sent the key to Fleet. " +
"Fleet verified with osquery."
: "The host installed the configuration profile. Fleet verified with osquery.",
},
verifying: {
statusText: "Verifying",
iconName: "success-outline",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host acknowledged the MDM command to turn on disk encryption. " +
"Fleet is verifying with osquery and retrieving the disk encryption key. " +
"This may take up to one hour."
: "The host acknowledged the MDM command to install the configuration profile. Fleet is " +
"verifying with osquery.",
},
failed: {
statusText: "Failed",
iconName: "error",
tooltip: null,
},
},
remove: {
pending: {
statusText: "Removing enforcement (pending)",
iconName: "pending-outline",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host will receive the MDM command to remove the disk encryption profile when the " +
"host comes online."
: "The host will receive the MDM command to remove the configuration profile when the host " +
"comes online.",
},
action_required: null, // should not be reached
verified: null, // should not be reached
verifying: null, // should not be reached
failed: {
statusText: "Failed",
iconName: "error",
tooltip: null,
},
},
};
type WindowsDiskEncryptionDisplayConfig = Omit<
OperationTypeOption,
"action_required"
>;
const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = {
verified: {
statusText: "Verified",
iconName: "success",
tooltip: () =>
"The host turned disk encryption on and sent the key to Fleet. Fleet verified with osquery.",
},
verifying: {
statusText: "Verifying",
iconName: "success-outline",
tooltip: () =>
"The host acknowledged the MDM command to turn on disk encryption. Fleet is verifying with osquery and retrieving " +
"the disk encryption key. This may take up to one hour.",
},
pending: {
statusText: "Enforcing (pending)",
iconName: "pending-outline",
tooltip: () =>
"The host will receive the MDM command to turn on disk encryption when the host comes online.",
},
failed: {
statusText: "Failed",
iconName: "error",
tooltip: null,
},
};
interface IOSSettingStatusCellProps {
status: OsSettingsTableStatusValue;
operationType: MacMdmProfileOperationType | null;
operationType: ProfileOperationType | null;
profileName: string;
}
@@ -0,0 +1,120 @@
import { ProfileOperationType } from "interfaces/mdm";
import { IconNames } from "components/icons";
import {
TooltipInnerContentFunc,
TooltipInnerContentOption,
} from "./components/Tooltip/TooltipContent";
import { OsSettingsTableStatusValue } from "../OSSettingsTableConfig";
import TooltipInnerContentActionRequired from "./components/Tooltip/ActionRequired";
export type ProfileDisplayOption = {
statusText: string;
iconName: IconNames;
tooltip: TooltipInnerContentOption | null;
} | null;
type OperationTypeOption = Record<
OsSettingsTableStatusValue,
ProfileDisplayOption
>;
type ProfileDisplayConfig = Record<ProfileOperationType, OperationTypeOption>;
export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
install: {
verified: {
statusText: "Verified",
iconName: "success",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host turned disk encryption on and sent the key to Fleet. " +
"Fleet verified with osquery."
: "The host applied the setting. Fleet verified with osquery.",
},
verifying: {
statusText: "Verifying",
iconName: "success-outline",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host acknowledged the MDM command to turn on disk encryption. " +
"Fleet is verifying with osquery and retrieving the disk encryption key. " +
"This may take up to one hour."
: "The host acknowledged the MDM command to apply the setting. Fleet is " +
"verifying with osquery.",
},
pending: {
statusText: "Enforcing (pending)",
iconName: "pending-outline",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The hosts will receive the MDM command to turn on disk encryption " +
"when the hosts come online."
: "The host will receive the MDM command to apply the settung when the " +
"host comes online.",
},
action_required: {
statusText: "Action required (pending)",
iconName: "pending-outline",
tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc,
},
failed: {
statusText: "Failed",
iconName: "error",
tooltip: null,
},
},
remove: {
pending: {
statusText: "Removing enforcement (pending)",
iconName: "pending-outline",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host will receive the MDM command to remove the disk encryption profile when the " +
"host comes online."
: "The host will receive the MDM command to remove the setting when the host " +
"comes online.",
},
action_required: null, // should not be reached
verified: null, // should not be reached
verifying: null, // should not be reached
failed: {
statusText: "Failed",
iconName: "error",
tooltip: null,
},
},
};
type WindowsDiskEncryptionDisplayConfig = Omit<
OperationTypeOption,
"action_required"
>;
export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = {
verified: {
statusText: "Verified",
iconName: "success",
tooltip: () =>
"The host turned disk encryption on and sent the key to Fleet. Fleet verified with osquery.",
},
verifying: {
statusText: "Verifying",
iconName: "success-outline",
tooltip: () =>
"The host acknowledged the MDM command to turn on disk encryption. Fleet is verifying with " +
"osquery and retrieving the disk encryption key. This may take up to one hour.",
},
pending: {
statusText: "Enforcing (pending)",
iconName: "pending-outline",
tooltip: () =>
"The host will receive the MDM command to turn on disk encryption when the host comes online.",
},
failed: {
statusText: "Failed",
iconName: "error",
tooltip: null,
},
};
@@ -98,21 +98,28 @@ const tableHeaders: IDataColumn[] = [
},
];
const makeWindowsRows = ({ os_settings }: IHostMdmData) => {
if (
!os_settings?.disk_encryption?.status ||
!isWindowsDiskEncryptionStatus(os_settings.disk_encryption.status)
) {
return null;
const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => {
const rows: ITableRowOsSettings[] = [];
if (profiles) {
rows.push(...profiles);
}
const rows: ITableRowOsSettings[] = [];
rows.push(
generateWinDiskEncryptionProfile(
os_settings.disk_encryption.status,
os_settings.disk_encryption.detail
)
);
if (
os_settings?.disk_encryption?.status &&
isWindowsDiskEncryptionStatus(os_settings.disk_encryption.status)
) {
rows.push(
generateWinDiskEncryptionProfile(
os_settings.disk_encryption.status,
os_settings.disk_encryption.detail
)
);
}
if (rows.length === 0 && !profiles) {
return null;
}
return rows;
};
+33 -66
View File
@@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { DiskEncryptionStatus, MdmProfileStatus } from "interfaces/mdm";
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
import {
DiskEncryptionStatus,
IMdmProfile,
MdmProfileStatus,
} from "interfaces/mdm";
import sendRequest from "services";
import endpoints from "utilities/endpoints";
import { buildQueryStringFromParams } from "utilities/url";
@@ -23,37 +26,19 @@ export type IDiskEncryptionSummaryResponse = Record<
IDiskEncryptionStatusAggregate
>;
// This function combines the profile status summary and the disk encryption summary
// to generate the aggregate profile status summary. We are doing this as a temporary
// solution until we have the API that will return the aggregate profile status summary
// from one call.
// TODO: API INTEGRATION: remove when API is implemented that returns windows
// data in the aggregate profile status summary.
const generateCombinedProfileStatusSummary = (
profileStatuses: ProfileStatusSummaryResponse,
diskEncryptionSummary: IDiskEncryptionSummaryResponse
): ProfileStatusSummaryResponse => {
const { verified, verifying, failed, pending } = profileStatuses;
const {
verified: verifiedDiskEncryption,
verifying: verifyingDiskEncryption,
failed: failedDiskEncryption,
action_required: actionRequiredDiskEncryption,
enforcing: enforcingDiskEncryption,
removing_enforcement: removingEnforcementDiskEncryption,
} = diskEncryptionSummary;
export interface IGetProfilesApiParams {
page?: number;
per_page?: number;
team_id?: number;
}
return {
verified: verified + verifiedDiskEncryption.windows,
verifying: verifying + verifyingDiskEncryption.windows,
failed: failed + failedDiskEncryption.windows,
pending:
pending +
actionRequiredDiskEncryption.windows +
enforcingDiskEncryption.windows +
removingEnforcementDiskEncryption.windows,
export interface IMdmProfilesResponse {
profiles: IMdmProfile[] | null;
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
};
}
const mdmService = {
downloadDeviceUserEnrollmentProfile: (token: string) => {
@@ -83,9 +68,12 @@ const mdmService = {
});
},
getProfiles: (teamId = APP_CONTEXT_NO_TEAM_ID) => {
const path = `${endpoints.MDM_PROFILES}?${buildQueryStringFromParams({
team_id: teamId,
getProfiles: (
params: IGetProfilesApiParams
): Promise<IMdmProfilesResponse> => {
const { MDM_PROFILES } = endpoints;
const path = `${MDM_PROFILES}?${buildQueryStringFromParams({
...params,
})}`;
return sendRequest("GET", path);
@@ -104,47 +92,26 @@ const mdmService = {
return sendRequest("POST", MDM_PROFILES, formData);
},
downloadProfile: (profileId: number) => {
downloadProfile: (profileId: number | string) => {
const { MDM_PROFILE } = endpoints;
return sendRequest("GET", MDM_PROFILE(profileId));
const path = `${MDM_PROFILE(profileId)}?${buildQueryStringFromParams({
alt: "media",
})}`;
return sendRequest("GET", path);
},
deleteProfile: (profileId: number) => {
deleteProfile: (profileId: number | string) => {
const { MDM_PROFILE } = endpoints;
return sendRequest("DELETE", MDM_PROFILE(profileId));
},
// TODO: API INTEGRATION: we need to rework this when we create API call that
// will return the aggregate statuses for windows included in the response.
// Currently to get windows data included we will need to make a separate call.
// We will likely change this to go back to single "getProfileStatusSummary" API call.
getAggregateProfileStatuses: async (
teamId = APP_CONTEXT_NO_TEAM_ID,
// TODO: WINDOWS FEATURE FLAG: remove when we windows feature is released.
includeWindows: boolean
) => {
// if we are not including windows we can just call the existing profile summary API
if (!includeWindows) {
return mdmService.getProfileStatusSummary(teamId);
getProfilesStatusSummary: (teamId: number) => {
let { MDM_PROFILES_STATUS_SUMMARY: path } = endpoints;
if (teamId) {
path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
}
// otherwise we have to make two calls and combine the results.
return mdmService
.getAggregateProfileStatusesWithWindows(teamId)
.then((res) => generateCombinedProfileStatusSummary(...res));
},
getAggregateProfileStatusesWithWindows: async (teamId: number) => {
return Promise.all([
mdmService.getProfileStatusSummary(teamId),
mdmService.getDiskEncryptionSummary(teamId),
]);
},
getProfileStatusSummary: (teamId = APP_CONTEXT_NO_TEAM_ID) => {
const path = `${
endpoints.MDM_PROFILES_AGGREGATE_STATUSES
}?${buildQueryStringFromParams({ team_id: teamId })}`;
return sendRequest("GET", path);
},
+7 -3
View File
@@ -47,10 +47,14 @@ export default {
MDM_APPLE_BM_KEYS: `/${API_VERSION}/fleet/mdm/apple/dep/key_pair`,
MDM_SUMMARY: `/${API_VERSION}/fleet/hosts/summary/mdm`,
MDM_REQUEST_CSR: `/${API_VERSION}/fleet/mdm/apple/request_csr`,
MDM_PROFILES: `/${API_VERSION}/fleet/mdm/apple/profiles`,
MDM_PROFILE: (id: number) => `/${API_VERSION}/fleet/mdm/apple/profiles/${id}`,
// MDM profile endpoints
MDM_PROFILES: `/${API_VERSION}/fleet/mdm/profiles`,
MDM_PROFILE: (id: number | string) =>
`/${API_VERSION}/fleet/mdm/profiles/${id}`,
MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`,
MDM_PROFILES_AGGREGATE_STATUSES: `/${API_VERSION}/fleet/mdm/apple/profiles/summary`,
MDM_PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/mdm/profiles/summary`,
MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`,
MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`,
MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => {
+5 -3
View File
@@ -1914,9 +1914,11 @@ func windowsConfigProfileForTest(t *testing.T, name, locURI string) *fleet.MDMWi
Name: name,
SyncML: []byte(fmt.Sprintf(`
<Replace>
<Target>
<LocURI>%s</LocURI>
</Target>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>
`, locURI)),
}
+1 -1
View File
@@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `carve_blocks` (
+6 -6
View File
@@ -590,9 +590,9 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
BootstrapPackage: optjson.SetString("bootstrap"),
MacOSSetupAssistant: optjson.SetString("assistant"),
},
//WindowsSettings: fleet.WindowsSettings{
// CustomSettings: optjson.SetSlice([]string{"foo", "bar"}),
//},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.SetSlice([]string{"foo", "bar"}),
},
},
},
})
@@ -609,9 +609,9 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
BootstrapPackage: optjson.SetString("bootstrap"),
MacOSSetupAssistant: optjson.SetString("assistant"),
},
//WindowsSettings: fleet.WindowsSettings{
// CustomSettings: optjson.SetSlice([]string{"foo", "bar"}),
//},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.SetSlice([]string{"foo", "bar"}),
},
}, mdm)
})
}
+6 -6
View File
@@ -161,7 +161,7 @@ type MDM struct {
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
// WindowsSettings WindowsSettings `json:"windows_settings"`
WindowsSettings WindowsSettings `json:"windows_settings"`
/////////////////////////////////////////////////////////////////
// WARNING: If you add to this struct make sure it's taken into
@@ -502,11 +502,11 @@ func (c *AppConfig) Copy() *AppConfig {
clone.Scripts = optjson.SetSlice(scripts)
}
// if c.MDM.WindowsSettings.CustomSettings.Set {
// windowsSettings := make([]string, len(c.MDM.WindowsSettings.CustomSettings.Value))
// copy(windowsSettings, c.MDM.WindowsSettings.CustomSettings.Value)
// clone.MDM.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
// }
if c.MDM.WindowsSettings.CustomSettings.Set {
windowsSettings := make([]string, len(c.MDM.WindowsSettings.CustomSettings.Value))
copy(windowsSettings, c.MDM.WindowsSettings.CustomSettings.Value)
clone.MDM.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
}
return &clone
}
+13 -13
View File
@@ -32,11 +32,11 @@ type TeamPayload struct {
// need to be able which part of the MDM config was provided in the request,
// so the fields are pointers to structs.
type TeamPayloadMDM struct {
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
MacOSSettings *MacOSSettings `json:"macos_settings"`
MacOSSetup *MacOSSetup `json:"macos_setup"`
// WindowsSettings *WindowsSettings `json:"windows_settings"`
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
MacOSUpdates *MacOSUpdates `json:"macos_updates"`
MacOSSettings *MacOSSettings `json:"macos_settings"`
MacOSSetup *MacOSSetup `json:"macos_setup"`
WindowsSettings *WindowsSettings `json:"windows_settings"`
}
// Team is the data representation for the "Team" concept (group of hosts and
@@ -154,7 +154,7 @@ type TeamMDM struct {
MacOSSettings MacOSSettings `json:"macos_settings"`
MacOSSetup MacOSSetup `json:"macos_setup"`
// WindowsSettings WindowsSettings `json:"windows_settings"`
WindowsSettings WindowsSettings `json:"windows_settings"`
// NOTE: TeamSpecMDM must be kept in sync with TeamMDM.
/////////////////////////////////////////////////////////////////
@@ -188,11 +188,11 @@ func (t *TeamMDM) Copy() *TeamMDM {
if t.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
clone.MacOSSettings.DeprecatedEnableDiskEncryption = ptr.Bool(*t.MacOSSettings.DeprecatedEnableDiskEncryption)
}
//if t.WindowsSettings.CustomSettings.Set {
// windowsSettings := make([]string, len(t.WindowsSettings.CustomSettings.Value))
// copy(windowsSettings, t.WindowsSettings.CustomSettings.Value)
// clone.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
//}
if t.WindowsSettings.CustomSettings.Set {
windowsSettings := make([]string, len(t.WindowsSettings.CustomSettings.Value))
copy(windowsSettings, t.WindowsSettings.CustomSettings.Value)
clone.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
}
return &clone
}
@@ -209,7 +209,7 @@ type TeamSpecMDM struct {
MacOSSettings map[string]interface{} `json:"macos_settings"`
MacOSSetup MacOSSetup `json:"macos_setup"`
// WindowsSettings WindowsSettings `json:"windows_settings"`
WindowsSettings WindowsSettings `json:"windows_settings"`
// NOTE: TeamMDM must be kept in sync with TeamSpecMDM.
}
@@ -419,7 +419,7 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
delete(mdmSpec.MacOSSettings, "enable_disk_encryption")
mdmSpec.MacOSSetup = t.Config.MDM.MacOSSetup
mdmSpec.EnableDiskEncryption = optjson.SetBool(t.Config.MDM.EnableDiskEncryption)
// mdmSpec.WindowsSettings = t.Config.MDM.WindowsSettings
mdmSpec.WindowsSettings = t.Config.MDM.WindowsSettings
return &TeamSpec{
Name: t.Name,
AgentOptions: agentOptions,
+1 -1
View File
@@ -68,7 +68,7 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
return errors.New("Only <Replace> supported as a top level element. Make sure you don't have other top level elements.")
}
for _, locURI := range element.FindElements("//Target/LocURI") {
for _, locURI := range element.FindElements("//Item/Target/LocURI") {
if locURI != nil {
if err := validateFleetProvidedLocURI(locURI.Text()); err != nil {
return err
+73 -8
View File
@@ -15,42 +15,85 @@ func TestValidateUserProvided(t *testing.T) {
{
name: "Valid XML with Replace",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace>`),
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>Custom/URI</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: false,
},
{
name: "Invalid Platform",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<SyncML xmlns="SYNCML:SYNCML1.2"><Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace></SyncML>`),
SyncML: []byte(`
<SyncML xmlns="SYNCML:SYNCML1.2">
<Replace>
<Item>
<Target><LocURI>Custom/URI</LocURI></Target>
</Item>
</Replace>
</SyncML>
`),
},
wantErr: true,
},
{
name: "Invalid XML Structure",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Add><Target><LocURI>Custom/URI</LocURI></Target></Add>`),
SyncML: []byte(`
<Add>
<Item>
<Target><LocURI>Custom/URI</LocURI></Target>
</Item>
</Add>
`),
},
wantErr: true,
},
{
name: "Reserved LocURI",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>./Device/Vendor/MSFT/BitLocker/Foo</LocURI></Target></Replace>`),
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>./Device/Vendor/MSFT/BitLocker/Foo</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: true,
},
{
name: "Reserved LocURI with implicit ./Device prefix",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>./Vendor/MSFT/BitLocker/Foo</LocURI></Target></Replace>`),
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>./Vendor/MSFT/BitLocker/Foo</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: true,
},
{
name: "XML with Multiple Replace Elements",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI1</LocURI></Target></Replace><Replace><Target><LocURI>Custom/URI2</LocURI></Target></Replace>`),
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>Custom/URI1</LocURI></Target>
</Item>
</Replace>
<Replace>
<Item>
<Target><LocURI>Custom/URI2</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: false,
},
@@ -64,14 +107,36 @@ func TestValidateUserProvided(t *testing.T) {
{
name: "XML with Multiple Replace Elements, One with Reserved LocURI",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace><Replace><Target><LocURI>./Device/Vendor/MSFT/BitLocker/Bar</LocURI></Target></Replace>`),
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>Custom/URI</LocURI></Target>
</Item>
</Replace>
<Replace>
<Item>
<Target><LocURI>./Device/Vendor/MSFT/BitLocker/Bar</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: true,
},
{
name: "XML with Mixed Replace and Add",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace><Add><Target><LocURI>Another/URI</LocURI></Target></Add>`),
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>Custom/URI</LocURI></Target>
</Item>
</Replace>
<Add>
<Item>
<Target><LocURI>Another/URI</LocURI></Target>
</Item>
</Add>
`),
},
wantErr: true,
},
+8 -8
View File
@@ -645,14 +645,14 @@ func (svc *Service) validateMDM(
}
}
// if !mdm.WindowsEnabledAndConfigured {
// if mdm.WindowsSettings.CustomSettings.Set &&
// len(mdm.WindowsSettings.CustomSettings.Value) > 0 &&
// !server.SliceStringsMatch(mdm.WindowsSettings.CustomSettings.Value, oldMdm.WindowsSettings.CustomSettings.Value) {
// invalid.Append("windows_settings.custom_settings",
// `Couldnt edit windows_settings.custom_settings. Windows MDM isnt turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)
// }
// }
if !mdm.WindowsEnabledAndConfigured {
if mdm.WindowsSettings.CustomSettings.Set &&
len(mdm.WindowsSettings.CustomSettings.Value) > 0 &&
!server.SliceStringsMatch(mdm.WindowsSettings.CustomSettings.Value, oldMdm.WindowsSettings.CustomSettings.Value) {
invalid.Append("windows_settings.custom_settings",
`Couldnt edit windows_settings.custom_settings. Windows MDM isnt turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)
}
}
if name := mdm.AppleBMDefaultTeam; name != "" && name != oldMdm.AppleBMDefaultTeam {
if !license.IsPremium() {
+5 -5
View File
@@ -813,7 +813,7 @@ func TestMDMAppleConfig(t *testing.T) {
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
// WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
},
}, {
name: "newDefaultTeamNoLicense",
@@ -841,7 +841,7 @@ func TestMDMAppleConfig(t *testing.T) {
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
// WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
},
}, {
name: "foundEdit",
@@ -854,7 +854,7 @@ func TestMDMAppleConfig(t *testing.T) {
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
// WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
},
}, {
name: "ssoFree",
@@ -873,7 +873,7 @@ func TestMDMAppleConfig(t *testing.T) {
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
// WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
},
}, {
name: "ssoAllFields",
@@ -895,7 +895,7 @@ func TestMDMAppleConfig(t *testing.T) {
MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}},
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
// WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
},
}, {
name: "ssoShortEntityID",
+38 -39
View File
@@ -380,8 +380,7 @@ func (c *Client) ApplyGroup(
}
if specs.AppConfig != nil {
windowsCustomSettings := []string{}
// windowsCustomSettings := extractAppCfgWindowsCustomSettings(specs.AppConfig)
windowsCustomSettings := extractAppCfgWindowsCustomSettings(specs.AppConfig)
macosCustomSettings := extractAppCfgMacOSCustomSettings(specs.AppConfig)
allCustomSettings := append(macosCustomSettings, windowsCustomSettings...)
@@ -644,42 +643,42 @@ func extractAppCfgMacOSCustomSettings(appCfg interface{}) []string {
return csStrings
}
//func extractAppCfgWindowsCustomSettings(appCfg interface{}) []string {
// asMap, ok := appCfg.(map[string]interface{})
// if !ok {
// return nil
// }
// mmdm, ok := asMap["mdm"].(map[string]interface{})
// if !ok {
// return nil
// }
// mos, ok := mmdm["windows_settings"].(map[string]interface{})
// if !ok || mos == nil {
// return nil
// }
//
// cs, ok := mos["custom_settings"]
// if !ok {
// // custom settings is not present
// return nil
// }
//
// csAny, ok := cs.([]interface{})
// if !ok || csAny == nil {
// // return a non-nil, empty slice instead, so the caller knows that the
// // custom_settings key was actually provided.
// return []string{}
// }
//
// csStrings := make([]string, 0, len(csAny))
// for _, v := range csAny {
// s, _ := v.(string)
// if s != "" {
// csStrings = append(csStrings, s)
// }
// }
// return csStrings
//}
func extractAppCfgWindowsCustomSettings(appCfg interface{}) []string {
asMap, ok := appCfg.(map[string]interface{})
if !ok {
return nil
}
mmdm, ok := asMap["mdm"].(map[string]interface{})
if !ok {
return nil
}
mos, ok := mmdm["windows_settings"].(map[string]interface{})
if !ok || mos == nil {
return nil
}
cs, ok := mos["custom_settings"]
if !ok {
// custom settings is not present
return nil
}
csAny, ok := cs.([]interface{})
if !ok || csAny == nil {
// return a non-nil, empty slice instead, so the caller knows that the
// custom_settings key was actually provided.
return []string{}
}
csStrings := make([]string, 0, len(csAny))
for _, v := range csAny {
s, _ := v.(string)
if s != "" {
csStrings = append(csStrings, s)
}
}
return csStrings
}
func extractAppCfgScripts(appCfg interface{}) []string {
asMap, ok := appCfg.(map[string]interface{})
@@ -721,7 +720,7 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]str
CustomSettings json.RawMessage `json:"custom_settings"`
} `json:"macos_settings"`
WindowsSettings struct {
CustomSettings json.RawMessage `json:"-"` // FIXME: allow unmarshalling
CustomSettings json.RawMessage `json:"custom_settings"`
} `json:"windows_settings"`
} `json:"mdm"`
}
+2 -7
View File
@@ -15,13 +15,8 @@ func (c *Client) ApplyAppConfig(payload interface{}, opts fleet.ApplySpecOptions
// ApplyNoTeamProfiles sends the list of profiles to be applied for the hosts
// in no team.
func (c *Client) ApplyNoTeamProfiles(profiles map[string][]byte, opts fleet.ApplySpecOptions) error {
var profilesBytes [][]byte
for _, pb := range profiles {
profilesBytes = append(profilesBytes, pb)
}
verb, path := "POST", "/api/latest/fleet/mdm/apple/profiles/batch"
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profilesBytes}, verb, path, nil, opts.RawQuery())
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profiles}, verb, path, nil, opts.RawQuery())
}
// GetAppConfig fetches the application config from the server API
+2 -7
View File
@@ -65,18 +65,13 @@ func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplySpecOptions
// ApplyTeamProfiles sends the list of profiles to be applied for the specified
// team.
func (c *Client) ApplyTeamProfiles(tmName string, profiles map[string][]byte, opts fleet.ApplySpecOptions) error {
var profilesBytes [][]byte
for _, pb := range profiles {
profilesBytes = append(profilesBytes, pb)
}
verb, path := "POST", "/api/latest/fleet/mdm/apple/profiles/batch"
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return err
}
query.Add("team_name", tmName)
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profilesBytes}, verb, path, nil, query.Encode())
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profiles}, verb, path, nil, query.Encode())
}
// ApplyPolicies sends the list of Policies to be applied to the
+105 -89
View File
@@ -99,94 +99,94 @@ spec:
}
}
//func TestExtractAppConfigWindowsCustomSettings(t *testing.T) {
// cases := []struct {
// desc string
// yaml string
// want []string
// }{
// {
// "no settings",
// `
//apiVersion: v1
//kind: config
//spec:
//`,
// nil,
// },
// {
// "no custom settings",
// `
//apiVersion: v1
//kind: config
//spec:
// org_info:
// org_name: "Fleet"
// mdm:
// windows_settings:
//`,
// nil,
// },
// {
// "empty custom settings",
// `
//apiVersion: v1
//kind: config
//spec:
// org_info:
// org_name: "Fleet"
// mdm:
// windows_settings:
// custom_settings:
//`,
// []string{},
// },
// {
// "custom settings specified",
// `
//apiVersion: v1
//kind: config
//spec:
// org_info:
// org_name: "Fleet"
// mdm:
// windows_settings:
// custom_settings:
// - "a"
// - "b"
//`,
// []string{"a", "b"},
// },
// {
// "empty and invalid custom settings",
// `
//apiVersion: v1
//kind: config
//spec:
// org_info:
// org_name: "Fleet"
// mdm:
// windows_settings:
// custom_settings:
// - "a"
// - ""
// - 4
// - "c"
//`,
// []string{"a", "c"},
// },
// }
// for _, c := range cases {
// t.Run(c.desc, func(t *testing.T) {
// specs, err := spec.GroupFromBytes([]byte(c.yaml))
// require.NoError(t, err)
// if specs.AppConfig != nil {
// got := extractAppCfgWindowsCustomSettings(specs.AppConfig)
// require.Equal(t, c.want, got)
// }
// })
// }
//}
func TestExtractAppConfigWindowsCustomSettings(t *testing.T) {
cases := []struct {
desc string
yaml string
want []string
}{
{
"no settings",
`
apiVersion: v1
kind: config
spec:
`,
nil,
},
{
"no custom settings",
`
apiVersion: v1
kind: config
spec:
org_info:
org_name: "Fleet"
mdm:
windows_settings:
`,
nil,
},
{
"empty custom settings",
`
apiVersion: v1
kind: config
spec:
org_info:
org_name: "Fleet"
mdm:
windows_settings:
custom_settings:
`,
[]string{},
},
{
"custom settings specified",
`
apiVersion: v1
kind: config
spec:
org_info:
org_name: "Fleet"
mdm:
windows_settings:
custom_settings:
- "a"
- "b"
`,
[]string{"a", "b"},
},
{
"empty and invalid custom settings",
`
apiVersion: v1
kind: config
spec:
org_info:
org_name: "Fleet"
mdm:
windows_settings:
custom_settings:
- "a"
- ""
- 4
- "c"
`,
[]string{"a", "c"},
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
specs, err := spec.GroupFromBytes([]byte(c.yaml))
require.NoError(t, err)
if specs.AppConfig != nil {
got := extractAppCfgWindowsCustomSettings(specs.AppConfig)
require.Equal(t, c.want, got)
}
})
}
}
func TestExtractTeamSpecsMDMCustomSettings(t *testing.T) {
cases := []struct {
@@ -214,6 +214,7 @@ spec:
name: Fleet
mdm:
macos_settings:
windows_settings:
---
apiVersion: v1
kind: team
@@ -222,6 +223,7 @@ spec:
name: Fleet2
mdm:
macos_settings:
windows_settings:
`,
nil,
},
@@ -236,6 +238,8 @@ spec:
mdm:
macos_settings:
custom_settings:
windows_settings:
custom_settings:
---
apiVersion: v1
kind: team
@@ -245,6 +249,8 @@ spec:
mdm:
macos_settings:
custom_settings:
windows_settings:
custom_settings:
`,
map[string][]string{"Fleet": {}, "Fleet2": {}},
},
@@ -261,8 +267,12 @@ spec:
custom_settings:
- "a"
- "b"
windows_settings:
custom_settings:
- "c"
- "d"
`,
map[string][]string{"Fleet": {"a", "b"}},
map[string][]string{"Fleet": {"a", "b", "c", "d"}},
},
{
"invalid custom settings",
@@ -279,6 +289,12 @@ spec:
- ""
- 42
- "c"
windows_settings:
custom_settings:
- "x"
- ""
- 24
- "y"
`,
map[string][]string{},
},
+10 -13
View File
@@ -537,18 +537,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
mdmAnyMW.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
// FIXME: endpoints are intentionally disabled to allow a release without this feature.
if false {
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
mdmAnyMW.POST("/api/_version_/fleet/mdm/profiles", newMDMConfigProfileEndpoint, newMDMConfigProfileRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/{profile_id_or_uuid}", getMDMConfigProfileEndpoint, getMDMConfigProfileRequest{})
mdmAnyMW.DELETE("/api/_version_/fleet/mdm/profiles/{profile_id_or_uuid}", deleteMDMConfigProfileEndpoint, deleteMDMConfigProfileRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles", listMDMConfigProfilesEndpoint, listMDMConfigProfilesRequest{})
// batch-apply is accessible even though MDM is not enabled, it needs
// to support the case where `fleetctl get config`'s output is used as
// input to `fleetctl apply`
ue.POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{})
}
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
mdmAnyMW.POST("/api/_version_/fleet/mdm/profiles", newMDMConfigProfileEndpoint, newMDMConfigProfileRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/{profile_id_or_uuid}", getMDMConfigProfileEndpoint, getMDMConfigProfileRequest{})
mdmAnyMW.DELETE("/api/_version_/fleet/mdm/profiles/{profile_id_or_uuid}", deleteMDMConfigProfileEndpoint, deleteMDMConfigProfileRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles", listMDMConfigProfilesEndpoint, listMDMConfigProfilesRequest{})
// the following set of mdm endpoints must always be accessible (even
// if MDM is not configured) as it bootstraps the setup of MDM
@@ -556,7 +549,6 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.POST("/api/_version_/fleet/mdm/apple/request_csr", requestMDMAppleCSREndpoint, requestMDMAppleCSRRequest{})
ue.POST("/api/_version_/fleet/mdm/apple/dep/key_pair", newMDMAppleDEPKeyPairEndpoint, nil)
ue.GET("/api/_version_/fleet/mdm/apple_bm", getAppleBMEndpoint, nil)
// Deprecated: POST /mdm/apple/profiles/batch is now deprecated, replaced by the
// platform-agnostic POST /mdm/apple/profiles/batch. It is still supported
// indefinitely for backwards compatibility.
@@ -566,6 +558,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// input to `fleetctl apply`
ue.POST("/api/_version_/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesEndpoint, batchSetMDMAppleProfilesRequest{})
// batch-apply is accessible even though MDM is not enabled, it needs
// to support the case where `fleetctl get config`'s output is used as
// input to `fleetctl apply`
ue.POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{})
errorLimiter := ratelimit.NewErrorMiddleware(limitStore)
// device-authenticated endpoints
@@ -140,9 +140,9 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
// because the WindowsSettings was marshalled to JSON to be saved in the DB,
// it did get marshalled, and then when unmarshalled it was set (but
// empty).
//WindowsSettings: fleet.WindowsSettings{
// CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
//},
WindowsSettings: fleet.WindowsSettings{
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
},
}, team.Config.MDM)
// an activity was created for team spec applied
File diff suppressed because it is too large Load Diff
+54 -54
View File
@@ -1029,8 +1029,8 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
{"duplicate profile name", 0, `<Replace>duplicate</Replace>`, true, "configuration profile with this name already exists."},
{"multiple Replace", 0, `<Replace>a</Replace><Replace>b</Replace>`, true, ""},
{"Replace and non-Replace", 0, `<Replace>a</Replace><Get>b</Get>`, true, "Only <Replace> supported as a top level element."},
{"BitLocker profile", 0, `<Replace><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Replace>`, true, "Custom configuration profiles can't include BitLocker settings."},
{"Windows updates profile", 0, `<Replace><Target><LocURI> ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoRebootForFeatureUpdates </LocURI></Target></Replace>`, true, "Custom configuration profiles can't include Windows updates settings."},
{"BitLocker profile", 0, `<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Item></Replace>`, true, "Custom configuration profiles can't include BitLocker settings."},
{"Windows updates profile", 0, `<Replace><Item><Target><LocURI> ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoRebootForFeatureUpdates </LocURI></Target></Item></Replace>`, true, "Custom configuration profiles can't include Windows updates settings."},
{"team empty profile", 1, "", true, "The file should include valid XML."},
{"team plist data", 1, string(mcBytesForTest("Foo", "Bar", "UUID")), true, "Only <Replace> supported as a top level element."},
@@ -1040,8 +1040,8 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
{"team duplicate profile name", 1, `<Replace>duplicate</Replace>`, true, "configuration profile with this name already exists."},
{"team multiple Replace", 1, `<Replace>a</Replace><Replace>b</Replace>`, true, ""},
{"team Replace and non-Replace", 1, `<Replace>a</Replace><Get>b</Get>`, true, "Only <Replace> supported as a top level element."},
{"team BitLocker profile", 1, `<Replace><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Replace>`, true, "Custom configuration profiles can't include BitLocker settings."},
{"team Windows updates profile", 1, `<Replace><Target><LocURI> ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoRebootForFeatureUpdates </LocURI></Target></Replace>`, true, "Custom configuration profiles can't include Windows updates settings."},
{"team BitLocker profile", 1, `<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Item></Replace>`, true, "Custom configuration profiles can't include BitLocker settings."},
{"team Windows updates profile", 1, `<Replace><Item><Target><LocURI> ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoRebootForFeatureUpdates </LocURI></Target></Item></Replace>`, true, "Custom configuration profiles can't include Windows updates settings."},
{"invalid team", 2, `<Replace></Replace>`, true, "not found"},
}
@@ -1093,7 +1093,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
return &fleet.Team{ID: id, Name: "team"}, nil
}
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, tmID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
@@ -1109,7 +1109,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
premium bool
teamID *uint
teamName *string
profiles [][]byte
profiles map[string][]byte
wantErr string
}{
{
@@ -1271,11 +1271,11 @@ func TestMDMBatchSetProfiles(t *testing.T) {
true,
ptr.Uint(1),
nil,
[][]byte{
mobileconfigForTest("N1", "I1"),
mobileconfigForTest("N1", "I2"),
map[string][]byte{
"N1": mobileconfigForTest("N1", "I1"),
"N2": mobileconfigForTest("N1", "I2"),
},
`More than one configuration profile have the same name `,
`The name provided for the profile must match the profile PayloadDisplayName: "N1"`,
},
{
"duplicate macOS profile identifier",
@@ -1283,12 +1283,12 @@ func TestMDMBatchSetProfiles(t *testing.T) {
true,
ptr.Uint(1),
nil,
[][]byte{
mobileconfigForTest("N1", "I1"),
mobileconfigForTest("N2", "I2"),
mobileconfigForTest("N3", "I1"),
map[string][]byte{
"N1": mobileconfigForTest("N1", "I1"),
"N2": mobileconfigForTest("N2", "I2"),
"N3": mobileconfigForTest("N3", "I1"),
},
`More than one configuration profile have the same identifier `,
`More than one configuration profile have the same identifier (PayloadIdentifier): "I1"`,
},
{
"only macOS",
@@ -1296,50 +1296,50 @@ func TestMDMBatchSetProfiles(t *testing.T) {
false,
nil,
nil,
[][]byte{
mobileconfigForTest("N1", "I1"),
mobileconfigForTest("N2", "I2"),
mobileconfigForTest("N3", "I3"),
map[string][]byte{
"N1": mobileconfigForTest("N1", "I1"),
"N2": mobileconfigForTest("N2", "I2"),
"N3": mobileconfigForTest("N3", "I3"),
},
``,
},
{
"mixed profiles",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
map[string][]byte{
"N1": syncMLForTest("./foo/bar"),
"N2": syncMLForTest("./baz"),
"N3": syncMLForTest("./zab"),
"N4": mobileconfigForTest("N4", "I1"),
"N5": mobileconfigForTest("N5", "I2"),
"N6": mobileconfigForTest("N6", "I3"),
},
``,
},
{
"only windows",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
map[string][]byte{
"N1": syncMLForTest("./foo/bar"),
"N2": syncMLForTest("./baz"),
"N3": syncMLForTest("./zab"),
},
``,
},
// {
// "mixed profiles",
// &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
// false,
// nil,
// nil,
// [][]byte{
// syncMLForTest("./foo/bar"),
// syncMLForTest("./baz"),
// syncMLForTest("./zab"),
// mobileconfigForTest("N4", "I1"),
// mobileconfigForTest("N5", "I2"),
// mobileconfigForTest("N6", "I3"),
// },
// ``,
// },
// {
// "only windows",
// &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
// false,
// nil,
// nil,
// [][]byte{
// syncMLForTest("./foo/bar"),
// syncMLForTest("./baz"),
// syncMLForTest("./zab"),
// },
// ``,
// },
{
"unsupported payload type",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[][]byte{
[]byte(`<?xml version="1.0" encoding="UTF-8"?>
map[string][]byte{
"foo": []byte(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
@@ -1379,7 +1379,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
defer func() { ds.BatchSetMDMAppleProfilesFuncInvoked = false }()
defer func() { ds.BatchSetMDMProfilesFuncInvoked = false }()
// prepare the context with the user and license
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
@@ -1389,15 +1389,15 @@ func TestMDMBatchSetProfiles(t *testing.T) {
}
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
err := svc.BatchSetMDMAppleProfiles(ctx, tt.teamID, tt.teamName, tt.profiles, false, false)
err := svc.BatchSetMDMProfiles(ctx, tt.teamID, tt.teamName, tt.profiles, false, false)
if tt.wantErr == "" {
require.NoError(t, err)
require.True(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
require.True(t, ds.BatchSetMDMProfilesFuncInvoked)
return
}
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
require.False(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
require.False(t, ds.BatchSetMDMProfilesFuncInvoked)
})
}
}
+36 -15
View File
@@ -2137,23 +2137,13 @@ func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger ki
p, ok := profileContents[profID]
if !ok {
// this should never happen
level.Info(logger).Log("warn", "missing profile contents", "profile_id", profID)
continue
return ctxerr.Wrap(ctx, err, "inserting commands for hosts")
}
// TODO(roberto): I think this should live separately in the
// Windows equivalent of Apple's Commander struct, but I'd like
// to keep it simpler for now until we understand more.
command := &fleet.MDMWindowsCommand{
CommandUUID: target.cmdUUID,
RawCommand: []byte(fmt.Sprintf(`
<Atomic>
<CmdID>%s</CmdID>
%s
</Atomic>
`, target.cmdUUID, p)),
// Atomic commands don't have a Target element.
TargetLocURI: "",
command, err := buildCommandFromProfileBytes(p, target.cmdUUID)
if err != nil {
level.Info(logger).Log("err", err, "profile_id", profID)
continue
}
if err := ds.MDMWindowsInsertCommandForHosts(ctx, target.hostUUIDs, command); err != nil {
return ctxerr.Wrap(ctx, err, "inserting commands for hosts")
@@ -2173,3 +2163,34 @@ func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger ki
return nil
}
// TODO(roberto): I think this should live separately in the
// Windows equivalent of Apple's Commander struct, but I'd like
// to keep it simpler for now until we understand more.
func buildCommandFromProfileBytes(profileBytes []byte, commandUUID string) (*fleet.MDMWindowsCommand, error) {
rawCommand := []byte(fmt.Sprintf(`<Atomic>%s</Atomic>`, profileBytes))
cmd := new(mdm_types.SyncMLCmd)
if err := xml.Unmarshal(rawCommand, cmd); err != nil {
return nil, fmt.Errorf("unmarshalling profile: %w", err)
}
// set the CmdID for the <Atomic> command
cmd.CmdID = commandUUID
// generate a CmdID for any nested <Replace>
for i := range cmd.ReplaceCommands {
cmd.ReplaceCommands[i].CmdID = uuid.NewString()
}
rawCommand, err := xml.Marshal(cmd)
if err != nil {
return nil, fmt.Errorf("marshalling command: %w", err)
}
command := &fleet.MDMWindowsCommand{
CommandUUID: commandUUID,
RawCommand: rawCommand,
// Atomic commands don't have a Target element.
TargetLocURI: "",
}
return command, nil
}
+38 -3
View File
@@ -359,11 +359,46 @@ func checkWrappedSyncMLCmd(tag string, data string) error {
return nil
}
func TestBuildCommandFromProfileBytes(t *testing.T) {
cmd, err := buildCommandFromProfileBytes([]byte("<Replace></Add>"), "")
require.Nil(t, cmd)
require.ErrorContains(t, err, "unmarshalling profile")
rawSyncML := syncMLForTest("foo/bar")
// build and generate a command
cmd, err = buildCommandFromProfileBytes(rawSyncML, "uuid-1")
require.Nil(t, err)
require.Equal(t, "uuid-1", cmd.CommandUUID)
require.Empty(t, cmd.TargetLocURI)
syncOne := new(mdm_types.SyncMLCmd)
err = xml.Unmarshal(cmd.RawCommand, syncOne)
require.NoError(t, err)
require.Len(t, syncOne.ReplaceCommands, 1)
require.NotEmpty(t, syncOne.ReplaceCommands[0].CmdID)
// build and generate a second command with the same syncml
cmd, err = buildCommandFromProfileBytes(rawSyncML, "uuid-2")
require.Nil(t, err)
require.Equal(t, "uuid-2", cmd.CommandUUID)
require.Empty(t, cmd.TargetLocURI)
syncTwo := new(mdm_types.SyncMLCmd)
err = xml.Unmarshal(cmd.RawCommand, syncTwo)
require.NoError(t, err)
require.Len(t, syncTwo.ReplaceCommands, 1)
require.NotEmpty(t, syncTwo.ReplaceCommands[0].CmdID)
// uuids of replaces are different
require.NotEqual(t, syncOne.ReplaceCommands[0].CmdID, syncTwo.ReplaceCommands[0].CmdID)
}
func syncMLForTest(locURI string) []byte {
return []byte(fmt.Sprintf(`
<Replace>
<Target>
<LocURI>%s</LocURI>
</Target>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>`, locURI))
}
+3 -4
View File
@@ -636,15 +636,14 @@ func mdmConfigurationRequiredEndpoints() []struct {
{"GET", "/api/latest/fleet/mdm/commands", false, false},
{"POST", "/api/fleet/orbit/disk_encryption_key", false, false},
{"GET", "/api/latest/fleet/mdm/disk_encryption/summary", false, true},
// FIXME: commenting out these endpoints to get the release out
// {"GET", "/api/latest/fleet/mdm/profiles/1", false, false},
// {"DELETE", "/api/latest/fleet/mdm/profiles/1", false, false},
{"GET", "/api/latest/fleet/mdm/profiles/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/profiles/1", false, false},
// TODO: this endpoint accepts multipart/form data that gets
// parsed before the MDM check, we need to refactor this
// function to return more information to the caller, or find a
// better way to test these endpoints.
//{"POST", "/api/latest/fleet/mdm/profiles", false, false},
// {"GET", "/api/latest/fleet/mdm/profiles", false, false},
{"GET", "/api/latest/fleet/mdm/profiles", false, false},
}
}