feature: windows profiles (#15349)
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
- add UI to upload, delete, download, and view windows custom MDM profiles.
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
// `Couldn’t edit windows_settings.custom_settings. Windows MDM isn’t 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",
|
||||
`Couldn’t edit windows_settings.custom_settings. Windows MDM isn’t 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+70
-22
@@ -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;
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+16
-3
@@ -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>•</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
|
||||
|
||||
+3
-14
@@ -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`}
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
|
||||
export const UPLOAD_ERROR_MESSAGES = {
|
||||
wrongType: {
|
||||
condition: () => false,
|
||||
message: "Couldn’t upload. The file should be a .mobileconfig file.",
|
||||
},
|
||||
identifierExists: {
|
||||
condition: (reason: string) =>
|
||||
reason.includes("MDMAppleConfigProfile.PayloadIdentifier"),
|
||||
message:
|
||||
"Couldn’t upload. A configuration profile with this identifier (PayloadIdentifier) already exists.",
|
||||
},
|
||||
nameExists: {
|
||||
condition: (reason: string) => reason.includes("PayloadDisplayName"),
|
||||
message:
|
||||
"Couldn’t upload. A configuration profile with this name (PayloadDisplayName) already exists.",
|
||||
},
|
||||
encrypted: {
|
||||
condition: (reason: string) => reason.includes("encrypted"),
|
||||
message: "Couldn’t upload. The file should be unencrypted.",
|
||||
},
|
||||
validXML: {
|
||||
condition: (reason: string) => reason.includes("parsing XML"),
|
||||
message: "Couldn’t upload. The file should include valid XML.",
|
||||
},
|
||||
fileVault: {
|
||||
condition: (reason: string) =>
|
||||
reason.includes("unsupported PayloadType(s): com.apple.MCX.FileVault2"),
|
||||
message:
|
||||
"Couldn’t upload. The configuration profile can’t include FileVault settings. To control these settings, go to Disk encryption.",
|
||||
},
|
||||
default: {
|
||||
condition: () => false,
|
||||
message: "Couldn’t 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;
|
||||
};
|
||||
+37
@@ -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 can’t include BitLocker settings."
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<span>
|
||||
{apiReason} To control these settings, go to <b>Disk encryption</b>.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
apiReason.includes(
|
||||
"The configuration profile can’t 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";
|
||||
+3
-3
@@ -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();
|
||||
|
||||
|
||||
+8
-120
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+120
@@ -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,
|
||||
},
|
||||
};
|
||||
+20
-13
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
|
||||
@@ -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` (
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
// `Couldn’t edit windows_settings.custom_settings. Windows MDM isn’t 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",
|
||||
`Couldn’t edit windows_settings.custom_settings. Windows MDM isn’t 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() {
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user