mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
feature: target profiles by labels (#16202)
for #14715 --------- Co-authored-by: Martin Angers <martin.n.angers@gmail.com> Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
This commit is contained in:
parent
901004e149
commit
7d00d5a41e
1
changes/15217-profile-labels-ui
Normal file
1
changes/15217-profile-labels-ui
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Updated UI with ability to target MDM profiles by label.
|
1
changes/15218-profile-labels-api
Normal file
1
changes/15218-profile-labels-api
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Added the profile's `labels` object to the response payload of `GET /mdm/profiles` (list configuration profiles) and `GET /mdm/profiles/{profile_uuid}` (get a configuration profile).
|
1
changes/15286-profile-labels-reconciliation
Normal file
1
changes/15286-profile-labels-reconciliation
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Added support for label-based MDM profiles reconciliation.
|
1
changes/15287-profile-verifier
Normal file
1
changes/15287-profile-verifier
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Adds ability for labeled profiles to be fetched for verification.
|
@ -261,7 +261,7 @@ spec:
|
|||||||
GracePeriodDays: optjson.SetInt(1),
|
GracePeriodDays: optjson.SetInt(1),
|
||||||
},
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileCfgPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileCfgPath}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +302,7 @@ spec:
|
|||||||
GracePeriodDays: optjson.SetInt(1),
|
GracePeriodDays: optjson.SetInt(1),
|
||||||
},
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{ // macos settings not provided, so not cleared
|
MacOSSettings: fleet.MacOSSettings{ // macos settings not provided, so not cleared
|
||||||
CustomSettings: []string{mobileCfgPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileCfgPath}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
newAgentOpts = json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)
|
newAgentOpts = json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)
|
||||||
@ -375,7 +375,7 @@ spec:
|
|||||||
GracePeriodDays: optjson.Int{Set: true},
|
GracePeriodDays: optjson.Int{Set: true},
|
||||||
},
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{},
|
CustomSettings: []fleet.MDMProfileSpec{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1094,7 +1094,7 @@ spec:
|
|||||||
GracePeriodDays: optjson.SetInt(0),
|
GracePeriodDays: optjson.SetInt(0),
|
||||||
},
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
|
||||||
},
|
},
|
||||||
WindowsEnabledAndConfigured: true,
|
WindowsEnabledAndConfigured: true,
|
||||||
}, currentAppConfig.MDM)
|
}, currentAppConfig.MDM)
|
||||||
@ -1136,7 +1136,7 @@ spec:
|
|||||||
GracePeriodDays: optjson.SetInt(0),
|
GracePeriodDays: optjson.SetInt(0),
|
||||||
},
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
|
||||||
},
|
},
|
||||||
WindowsEnabledAndConfigured: true,
|
WindowsEnabledAndConfigured: true,
|
||||||
}, currentAppConfig.MDM)
|
}, currentAppConfig.MDM)
|
||||||
@ -1176,7 +1176,7 @@ spec:
|
|||||||
assert.Equal(t, fleet.TeamMDM{
|
assert.Equal(t, fleet.TeamMDM{
|
||||||
EnableDiskEncryption: false,
|
EnableDiskEncryption: false,
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
|
||||||
},
|
},
|
||||||
MacOSUpdates: fleet.MacOSUpdates{
|
MacOSUpdates: fleet.MacOSUpdates{
|
||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
@ -1215,7 +1215,7 @@ spec:
|
|||||||
assert.Equal(t, fleet.TeamMDM{
|
assert.Equal(t, fleet.TeamMDM{
|
||||||
EnableDiskEncryption: false,
|
EnableDiskEncryption: false,
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
|
||||||
},
|
},
|
||||||
MacOSUpdates: fleet.MacOSUpdates{
|
MacOSUpdates: fleet.MacOSUpdates{
|
||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
@ -1251,7 +1251,7 @@ spec:
|
|||||||
assert.Equal(t, fleet.TeamMDM{
|
assert.Equal(t, fleet.TeamMDM{
|
||||||
EnableDiskEncryption: false,
|
EnableDiskEncryption: false,
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{mobileConfigPath},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
|
||||||
},
|
},
|
||||||
MacOSUpdates: fleet.MacOSUpdates{
|
MacOSUpdates: fleet.MacOSUpdates{
|
||||||
MinimumVersion: optjson.SetString("10.10.10"),
|
MinimumVersion: optjson.SetString("10.10.10"),
|
||||||
|
@ -80,9 +80,9 @@ func TestHostsTransferByLabel(t *testing.T) {
|
|||||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) ([]uint, error) {
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||||
require.Equal(t, []string{"label1"}, labels)
|
require.Equal(t, []string{"label1"}, labels)
|
||||||
return []uint{uint(11)}, nil
|
return map[string]uint{"label1": uint(11)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.ListHostsInLabelFunc = func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
ds.ListHostsInLabelFunc = func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||||
@ -136,9 +136,9 @@ func TestHostsTransferByStatus(t *testing.T) {
|
|||||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) ([]uint, error) {
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||||
require.Equal(t, []string{"label1"}, labels)
|
require.Equal(t, []string{"label1"}, labels)
|
||||||
return []uint{uint(11)}, nil
|
return map[string]uint{"label1": uint(11)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||||
@ -192,9 +192,9 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
|
|||||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) ([]uint, error) {
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||||
require.Equal(t, []string{"label1"}, labels)
|
require.Equal(t, []string{"label1"}, labels)
|
||||||
return []uint{uint(11)}, nil
|
return map[string]uint{"label1": uint(11)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||||
|
@ -51,7 +51,7 @@ func TestSavedLiveQuery(t *testing.T) {
|
|||||||
ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) {
|
ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) {
|
||||||
return []uint{1234}, nil
|
return []uint{1234}, nil
|
||||||
}
|
}
|
||||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) ([]uint, error) {
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||||
@ -195,7 +195,7 @@ func TestAdHocLiveQuery(t *testing.T) {
|
|||||||
ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) {
|
ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) {
|
||||||
return []uint{1234}, nil
|
return []uint{1234}, nil
|
||||||
}
|
}
|
||||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) ([]uint, error) {
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||||
|
@ -1023,7 +1023,7 @@ func (svc *Service) editTeamFromSpec(
|
|||||||
if spec.MDM.WindowsSettings.CustomSettings.Set {
|
if spec.MDM.WindowsSettings.CustomSettings.Set {
|
||||||
if !appCfg.MDM.WindowsEnabledAndConfigured &&
|
if !appCfg.MDM.WindowsEnabledAndConfigured &&
|
||||||
len(spec.MDM.WindowsSettings.CustomSettings.Value) > 0 &&
|
len(spec.MDM.WindowsSettings.CustomSettings.Value) > 0 &&
|
||||||
!server.SliceStringsMatch(team.Config.MDM.WindowsSettings.CustomSettings.Value, spec.MDM.WindowsSettings.CustomSettings.Value) {
|
!fleet.MDMProfileSpecsMatch(team.Config.MDM.WindowsSettings.CustomSettings.Value, spec.MDM.WindowsSettings.CustomSettings.Value) {
|
||||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("windows_settings.custom_settings",
|
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.`))
|
`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.`))
|
||||||
}
|
}
|
||||||
@ -1126,7 +1126,7 @@ func (svc *Service) applyTeamMacOSSettings(ctx context.Context, spec *fleet.Team
|
|||||||
|
|
||||||
customSettingsChanged := setFields["custom_settings"] &&
|
customSettingsChanged := setFields["custom_settings"] &&
|
||||||
len(applyUpon.CustomSettings) > 0 &&
|
len(applyUpon.CustomSettings) > 0 &&
|
||||||
!server.SliceStringsMatch(applyUpon.CustomSettings, oldCustomSettings)
|
!fleet.MDMProfileSpecsMatch(applyUpon.CustomSettings, oldCustomSettings)
|
||||||
|
|
||||||
if customSettingsChanged || (setFields["enable_disk_encryption"] && *applyUpon.DeprecatedEnableDiskEncryption) {
|
if customSettingsChanged || (setFields["enable_disk_encryption"] && *applyUpon.DeprecatedEnableDiskEncryption) {
|
||||||
field := "custom_settings"
|
field := "custom_settings"
|
||||||
|
22
frontend/components/icons/Upload.tsx
Normal file
22
frontend/components/icons/Upload.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Upload = () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="17"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 17 17"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M7.5 11C7.5 11.5523 7.94772 12 8.5 12C9.05228 12 9.5 11.5523 9.5 11V3.63504L10.8598 4.76822C11.2841 5.12179 11.9147 5.06446 12.2682 4.64018C12.6218 4.21591 12.5645 3.58534 12.1402 3.23178L9.14018 0.731779C8.76934 0.42274 8.23066 0.42274 7.85982 0.731779L4.85982 3.23178C4.43554 3.58534 4.37821 4.21591 4.73178 4.64018C5.08534 5.06446 5.71591 5.12179 6.14018 4.76822L7.5 3.63504L7.5 11ZM2.5 9.5C2.5 8.94771 2.05228 8.5 1.5 8.5C0.947715 8.5 0.5 8.94771 0.5 9.5L0.5 15.5C0.5 16.0523 0.947715 16.5 1.5 16.5H15.5C16.0523 16.5 16.5 16.0523 16.5 15.5V9.5C16.5 8.94771 16.0523 8.5 15.5 8.5C14.9477 8.5 14.5 8.94771 14.5 9.5V14.5H2.5L2.5 9.5Z"
|
||||||
|
fill="#6A67FE"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Upload;
|
@ -51,6 +51,7 @@ import Transfer from "./Transfer";
|
|||||||
import TrashCan from "./TrashCan";
|
import TrashCan from "./TrashCan";
|
||||||
import Profile from "./Profile";
|
import Profile from "./Profile";
|
||||||
import Download from "./Download";
|
import Download from "./Download";
|
||||||
|
import Upload from "./Upload";
|
||||||
import Refresh from "./Refresh";
|
import Refresh from "./Refresh";
|
||||||
|
|
||||||
// a mapping of the usable names of icons to the icon source.
|
// a mapping of the usable names of icons to the icon source.
|
||||||
@ -107,6 +108,7 @@ export const ICON_MAP = {
|
|||||||
"premium-feature": PremiumFeature,
|
"premium-feature": PremiumFeature,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
download: Download,
|
download: Download,
|
||||||
|
upload: Upload,
|
||||||
refresh: Refresh,
|
refresh: Refresh,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,6 +57,11 @@ export interface IMdmSummaryResponse {
|
|||||||
|
|
||||||
type ProfilePlatform = "darwin" | "windows";
|
type ProfilePlatform = "darwin" | "windows";
|
||||||
|
|
||||||
|
export interface IProfileLabel {
|
||||||
|
name: string;
|
||||||
|
broken: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IMdmProfile {
|
export interface IMdmProfile {
|
||||||
profile_uuid: string;
|
profile_uuid: string;
|
||||||
team_id: number;
|
team_id: number;
|
||||||
@ -66,6 +71,7 @@ export interface IMdmProfile {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
checksum: string | null; // null for windows profiles
|
checksum: string | null; // null for windows profiles
|
||||||
|
labels?: IProfileLabel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
|
export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
|
||||||
|
@ -14,4 +14,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-nav__card-container > .custom-settings {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import React, { useCallback, useContext, useRef, useState } from "react";
|
import React, { useCallback, useContext, useRef, useState } from "react";
|
||||||
import { InjectedRouter } from "react-router";
|
import { InjectedRouter } from "react-router";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
|
import PATHS from "router/paths";
|
||||||
|
|
||||||
|
import { AppContext } from "context/app";
|
||||||
|
import { NotificationContext } from "context/notification";
|
||||||
|
|
||||||
import { IMdmProfile } from "interfaces/mdm";
|
import { IMdmProfile } from "interfaces/mdm";
|
||||||
|
|
||||||
import mdmAPI, { IMdmProfilesResponse } from "services/entities/mdm";
|
import mdmAPI, { IMdmProfilesResponse } from "services/entities/mdm";
|
||||||
import { NotificationContext } from "context/notification";
|
|
||||||
import PATHS from "router/paths";
|
|
||||||
|
|
||||||
import CustomLink from "components/CustomLink";
|
import CustomLink from "components/CustomLink";
|
||||||
import SectionHeader from "components/SectionHeader";
|
import SectionHeader from "components/SectionHeader";
|
||||||
@ -16,10 +19,12 @@ import Pagination from "pages/ManageControlsPage/components/Pagination";
|
|||||||
|
|
||||||
import UploadList from "../../../components/UploadList";
|
import UploadList from "../../../components/UploadList";
|
||||||
|
|
||||||
|
import AddProfileCard from "./components/ProfileUploader/components/AddProfileCard";
|
||||||
|
import AddProfileModal from "./components/ProfileUploader/components/AddProfileModal";
|
||||||
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
|
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
|
||||||
|
import ProfileLabelsModal from "./components/ProfileLabelsModal/ProfileLabelsModal";
|
||||||
import ProfileListItem from "./components/ProfileListItem";
|
import ProfileListItem from "./components/ProfileListItem";
|
||||||
import ProfileListHeading from "./components/ProfileListHeading";
|
import ProfileListHeading from "./components/ProfileListHeading";
|
||||||
import ProfileUploader from "./components/ProfileUploader";
|
|
||||||
|
|
||||||
const PROFILES_PER_PAGE = 10;
|
const PROFILES_PER_PAGE = 10;
|
||||||
|
|
||||||
@ -41,7 +46,13 @@ const CustomSettings = ({
|
|||||||
onMutation,
|
onMutation,
|
||||||
}: ICustomSettingsProps) => {
|
}: ICustomSettingsProps) => {
|
||||||
const { renderFlash } = useContext(NotificationContext);
|
const { renderFlash } = useContext(NotificationContext);
|
||||||
|
const { isPremiumTier } = useContext(AppContext);
|
||||||
|
|
||||||
|
const [showAddProfileModal, setShowAddProfileModal] = useState(false);
|
||||||
|
const [
|
||||||
|
profileLabelsModalData,
|
||||||
|
setProfileLabelsModalData,
|
||||||
|
] = useState<IMdmProfile | null>(null);
|
||||||
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
|
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
|
||||||
|
|
||||||
const selectedProfile = useRef<IMdmProfile | null>(null);
|
const selectedProfile = useRef<IMdmProfile | null>(null);
|
||||||
@ -70,6 +81,8 @@ const CustomSettings = ({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const profiles = profilesData?.profiles;
|
||||||
|
const meta = profilesData?.meta;
|
||||||
|
|
||||||
const onUploadProfile = () => {
|
const onUploadProfile = () => {
|
||||||
refetchProfiles();
|
refetchProfiles();
|
||||||
@ -122,29 +135,33 @@ const CustomSettings = ({
|
|||||||
return <DataError />;
|
return <DataError />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!profiles?.length) {
|
||||||
!profilesData ||
|
|
||||||
!profilesData.profiles ||
|
|
||||||
profilesData.profiles.length === 0
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { profiles, meta } = profilesData;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UploadList
|
<UploadList
|
||||||
keyAttribute="profile_uuid"
|
keyAttribute="profile_uuid"
|
||||||
listItems={profiles}
|
listItems={profiles}
|
||||||
HeadingComponent={ProfileListHeading}
|
HeadingComponent={() =>
|
||||||
|
ProfileListHeading({
|
||||||
|
onClickAddProfile: () => setShowAddProfileModal(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
ListItemComponent={({ listItem }) => (
|
ListItemComponent={({ listItem }) => (
|
||||||
<ProfileListItem profile={listItem} onDelete={onClickDelete} />
|
<ProfileListItem
|
||||||
|
isPremium={!!isPremiumTier}
|
||||||
|
profile={listItem}
|
||||||
|
setProfileLabelsModalData={setProfileLabelsModalData}
|
||||||
|
onDelete={onClickDelete}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
<Pagination
|
||||||
className={`${baseClass}__pagination-controls`}
|
className={`${baseClass}__pagination-controls`}
|
||||||
disableNext={!meta.has_next_results}
|
disableNext={!meta?.has_next_results}
|
||||||
disablePrev={!meta.has_previous_results}
|
disablePrev={!meta?.has_previous_results}
|
||||||
onNextPage={onNextPage}
|
onNextPage={onNextPage}
|
||||||
onPrevPage={onPrevPage}
|
onPrevPage={onPrevPage}
|
||||||
/>
|
/>
|
||||||
@ -164,10 +181,21 @@ const CustomSettings = ({
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{renderProfileList()}
|
{renderProfileList()}
|
||||||
<ProfileUploader
|
{!isLoadingProfiles && !isErrorProfiles && !profiles?.length && (
|
||||||
currentTeamId={currentTeamId}
|
<AddProfileCard
|
||||||
onUpload={onUploadProfile}
|
baseClass="add-profile"
|
||||||
/>
|
setShowModal={setShowAddProfileModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showAddProfileModal && (
|
||||||
|
<AddProfileModal
|
||||||
|
baseClass="add-profile"
|
||||||
|
currentTeamId={currentTeamId}
|
||||||
|
isPremiumTier={!!isPremiumTier}
|
||||||
|
onUpload={onUploadProfile}
|
||||||
|
setShowModal={setShowAddProfileModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showDeleteProfileModal && selectedProfile.current && (
|
{showDeleteProfileModal && selectedProfile.current && (
|
||||||
<DeleteProfileModal
|
<DeleteProfileModal
|
||||||
profileName={selectedProfile.current?.name}
|
profileName={selectedProfile.current?.name}
|
||||||
@ -176,6 +204,13 @@ const CustomSettings = ({
|
|||||||
onDelete={onDeleteProfile}
|
onDelete={onDeleteProfile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!!isPremiumTier && !!profileLabelsModalData?.labels?.length && (
|
||||||
|
<ProfileLabelsModal
|
||||||
|
baseClass={baseClass}
|
||||||
|
profile={profileLabelsModalData}
|
||||||
|
setModalData={setProfileLabelsModalData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
.custom-settings {
|
.custom-settings {
|
||||||
|
.section-header {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 12px 0;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
font-size: $x-small;
|
font-size: $x-small;
|
||||||
margin: $pad-xxlarge 0;
|
margin: $pad-large 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__profiles-header {
|
&__profiles-header {
|
||||||
@ -28,4 +39,183 @@
|
|||||||
&__file-uploader {
|
&__file-uploader {
|
||||||
margin-top: $pad-xxlarge;
|
margin-top: $pad-xxlarge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__labels-list {
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid $ui-fleet-black-10;
|
||||||
|
|
||||||
|
&--label {
|
||||||
|
display: flex;
|
||||||
|
height: 41px;
|
||||||
|
padding: 0 $pad-large;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid $ui-fleet-black-10;
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
gap: $pad-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list {
|
||||||
|
&__list {
|
||||||
|
.list-item__label-count {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.list-item__actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:hover {
|
||||||
|
background-color: $ui-fleet-blue-10;
|
||||||
|
.list-item__labels {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.list-item__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: $pad-xxlarge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-profile {
|
||||||
|
&__card--content-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $pad-medium;
|
||||||
|
padding: 28.5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__modal-content-wrap {
|
||||||
|
margin-top: $pad-large;
|
||||||
|
|
||||||
|
.add-profile__file {
|
||||||
|
padding: $pad-medium $pad-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-chooser {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: $pad-medium;
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--button-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $pad-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected-file {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&--details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&--name {
|
||||||
|
font-size: $x-small;
|
||||||
|
font-weight: $bold;
|
||||||
|
}
|
||||||
|
&--platform {
|
||||||
|
font-size: $xx-small;
|
||||||
|
color: $ui-fleet-black-75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile-graphic {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $pad-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: $pad-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__target {
|
||||||
|
margin: $pad-large 0 $pad-small 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
margin: $pad-medium 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-labels {
|
||||||
|
display: flex;
|
||||||
|
height: 187px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $pad-small;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $ui-fleet-black-75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkboxes {
|
||||||
|
display: flex;
|
||||||
|
max-height: 187px;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
border: 1px solid $ui-fleet-black-10;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
margin: 69.5px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
width: 100%;
|
||||||
|
padding: $pad-small $pad-medium;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid $ui-fleet-black-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field--checkbox {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label-name {
|
||||||
|
padding-left: $pad-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fleet-checkbox {
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
width: 490px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Modal from "components/Modal";
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
|
import { IMdmProfile, IProfileLabel } from "interfaces/mdm";
|
||||||
|
import InfoBanner from "components/InfoBanner";
|
||||||
|
import TooltipWrapper from "components/TooltipWrapper";
|
||||||
|
import Icon from "components/Icon";
|
||||||
|
|
||||||
|
const ModalDescription = ({
|
||||||
|
baseClass,
|
||||||
|
profileName,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
profileName: string;
|
||||||
|
}) => (
|
||||||
|
<div className={`${baseClass}__description`}>
|
||||||
|
<b>{profileName}</b> will only be applied to hosts that have all these
|
||||||
|
labels:
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BrokenLabelWarning = () => (
|
||||||
|
<InfoBanner color="yellow">
|
||||||
|
<span>
|
||||||
|
The configuration profile is{" "}
|
||||||
|
<TooltipWrapper
|
||||||
|
tipContent={`It won’t be applied to new hosts because one or more labels are deleted. To apply the profile to new hosts, please delete it and upload a new profile.`}
|
||||||
|
underline
|
||||||
|
>
|
||||||
|
broken
|
||||||
|
</TooltipWrapper>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</InfoBanner>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LabelsList = ({
|
||||||
|
baseClass,
|
||||||
|
labels,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
labels: IProfileLabel[];
|
||||||
|
}) => (
|
||||||
|
<div className={`${baseClass}__labels-list`}>
|
||||||
|
{labels.map((label) => (
|
||||||
|
<div key={label.name} className={`${baseClass}__labels-list--label`}>
|
||||||
|
{label.name}
|
||||||
|
{label.broken && (
|
||||||
|
<span className={`${baseClass}__labels-list--label warning`}>
|
||||||
|
<Icon name="warning" />
|
||||||
|
Label deleted
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IProfileLabelsModalProps {
|
||||||
|
baseClass: string;
|
||||||
|
profile: IMdmProfile | null;
|
||||||
|
setModalData: React.Dispatch<React.SetStateAction<IMdmProfile | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileLabelsModal = ({
|
||||||
|
baseClass,
|
||||||
|
profile,
|
||||||
|
setModalData,
|
||||||
|
}: IProfileLabelsModalProps) => {
|
||||||
|
if (!profile?.labels?.length) {
|
||||||
|
// caller ensures this never happens
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="Custom target" onExit={() => setModalData(null)}>
|
||||||
|
<div className={`${baseClass}__modal-content-wrap`}>
|
||||||
|
{profile.labels.some((label) => label.broken) && <BrokenLabelWarning />}
|
||||||
|
<ModalDescription baseClass={baseClass} profileName={profile.name} />
|
||||||
|
<LabelsList baseClass={baseClass} labels={profile.labels} />
|
||||||
|
<div className="modal-cta-wrap">
|
||||||
|
<Button variant="brand" onClick={() => setModalData(null)}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileLabelsModal;
|
@ -1,12 +1,33 @@
|
|||||||
|
import Icon from "components/Icon";
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const baseClass = "profile-list-heading";
|
const baseClass = "profile-list-heading";
|
||||||
|
|
||||||
const ProfileListHeading = () => {
|
interface IProfileListHeadingProps {
|
||||||
|
onClickAddProfile?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileListHeading = ({
|
||||||
|
onClickAddProfile,
|
||||||
|
}: IProfileListHeadingProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<span>Configuration profile</span>
|
<span className={`${baseClass}__profile-name-heading`}>
|
||||||
<span className={`${baseClass}__actions-heading`}>Actions</span>
|
Configuration profile
|
||||||
|
</span>
|
||||||
|
<span className={`${baseClass}__actions-heading`}>
|
||||||
|
<Button
|
||||||
|
variant="text-icon"
|
||||||
|
className={`${baseClass}__add-button`}
|
||||||
|
onClick={onClickAddProfile}
|
||||||
|
>
|
||||||
|
<span className={`${baseClass}__icon-wrap`}>
|
||||||
|
<Icon name="plus" />
|
||||||
|
Add profile
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
.profile-list-heading {
|
.profile-list-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: $x-small;
|
font-size: $x-small;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
|
|
||||||
&__actions-heading {
|
&__profile-name-heading {
|
||||||
text-align: right;
|
align-content: center;
|
||||||
margin-right: 40px; // align with left side of buttons below it
|
}
|
||||||
|
|
||||||
|
&__icon-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $pad-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,31 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
import { IMdmProfile } from "interfaces/mdm";
|
import { IMdmProfile } from "interfaces/mdm";
|
||||||
import mdmAPI from "services/entities/mdm";
|
import mdmAPI from "services/entities/mdm";
|
||||||
|
|
||||||
import Button from "components/buttons/Button";
|
import Button from "components/buttons/Button";
|
||||||
|
import Graphic from "components/Graphic";
|
||||||
import Icon from "components/Icon";
|
import Icon from "components/Icon";
|
||||||
import ListItem from "components/ListItem";
|
|
||||||
|
import { pluralize } from "utilities/helpers";
|
||||||
|
|
||||||
const baseClass = "profile-list-item";
|
const baseClass = "profile-list-item";
|
||||||
|
|
||||||
|
const LabelCount = ({
|
||||||
|
className,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
className: string;
|
||||||
|
count: number;
|
||||||
|
}) => (
|
||||||
|
<div className={`${className}__labels--count`}>
|
||||||
|
{`${count} ${pluralize(count, "label", "s", "")}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
interface IProfileDetailsProps {
|
interface IProfileDetailsProps {
|
||||||
platform: string;
|
platform: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -33,50 +48,77 @@ const ProfileDetails = ({ platform, createdAt }: IProfileDetailsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IProfileListItemProps {
|
interface IProfileListItemProps {
|
||||||
|
isPremium: boolean;
|
||||||
profile: IMdmProfile;
|
profile: IMdmProfile;
|
||||||
onDelete: (profile: IMdmProfile) => void;
|
onDelete: (profile: IMdmProfile) => void;
|
||||||
|
setProfileLabelsModalData: React.Dispatch<
|
||||||
|
React.SetStateAction<IMdmProfile | null>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileListItem = ({ profile, onDelete }: IProfileListItemProps) => {
|
const ProfileListItem = ({
|
||||||
|
isPremium,
|
||||||
|
profile,
|
||||||
|
onDelete,
|
||||||
|
setProfileLabelsModalData,
|
||||||
|
}: IProfileListItemProps) => {
|
||||||
|
const { created_at, labels, name, platform, profile_uuid } = profile;
|
||||||
|
const subClass = "list-item";
|
||||||
|
|
||||||
const onClickDownload = async () => {
|
const onClickDownload = async () => {
|
||||||
const fileContent = await mdmAPI.downloadProfile(profile.profile_uuid);
|
const fileContent = await mdmAPI.downloadProfile(profile_uuid);
|
||||||
const formatDate = format(new Date(), "yyyy-MM-dd");
|
const formatDate = format(new Date(), "yyyy-MM-dd");
|
||||||
const extension = profile.platform === "darwin" ? "mobileconfig" : "xml";
|
const extension = platform === "darwin" ? "mobileconfig" : "xml";
|
||||||
const filename = `${formatDate}_${profile.name}.${extension}`;
|
const filename = `${formatDate}_${name}.${extension}`;
|
||||||
const file = new File([fileContent], filename);
|
const file = new File([fileContent], filename);
|
||||||
FileSaver.saveAs(file);
|
FileSaver.saveAs(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<div className={classnames(subClass, baseClass)}>
|
||||||
className={baseClass}
|
<div className={`${subClass}__main-content`}>
|
||||||
graphic="file-configuration-profile"
|
<Graphic name="file-configuration-profile" />
|
||||||
title={profile.name}
|
<div className={`${subClass}__info`}>
|
||||||
details={
|
<span className={`${subClass}__title`}>{name}</span>
|
||||||
<ProfileDetails
|
<div className={`${subClass}__details`}>
|
||||||
platform={profile.platform}
|
<ProfileDetails platform={platform} createdAt={created_at} />
|
||||||
createdAt={profile.created_at}
|
</div>
|
||||||
/>
|
</div>
|
||||||
}
|
</div>
|
||||||
actions={
|
<div className={`${subClass}__actions-wrap`}>
|
||||||
<>
|
{isPremium && !!labels?.length && (
|
||||||
|
<div className={`${subClass}__labels`}>
|
||||||
|
{labels?.some((l) => l.broken) && <Icon name="warning" />}
|
||||||
|
<LabelCount className={subClass} count={labels.length} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`${subClass}__actions`}>
|
||||||
|
{isPremium && !!labels?.length && (
|
||||||
|
<Button
|
||||||
|
className={`${subClass}__action-button`}
|
||||||
|
variant="text-icon"
|
||||||
|
onClick={() => setProfileLabelsModalData({ ...profile })}
|
||||||
|
>
|
||||||
|
<Icon name="filter" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className={`${baseClass}__action-button`}
|
className={`${subClass}__action-button`}
|
||||||
variant="text-icon"
|
variant="text-icon"
|
||||||
onClick={onClickDownload}
|
onClick={onClickDownload}
|
||||||
>
|
>
|
||||||
<Icon name="download" />
|
<Icon name="download" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={`${baseClass}__action-button`}
|
className={`${subClass}__action-button`}
|
||||||
variant="text-icon"
|
variant="text-icon"
|
||||||
onClick={() => onDelete(profile)}
|
onClick={() => onDelete(profile)}
|
||||||
>
|
>
|
||||||
<Icon name="trash" color="ui-fleet-black-75" />
|
<Icon name="trash" color="ui-fleet-black-75" />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
}
|
</div>
|
||||||
/>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,4 +16,24 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-item__actions-wrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item__labels {
|
||||||
|
display: flex;
|
||||||
|
gap: $pad-small;
|
||||||
|
|
||||||
|
&--count {
|
||||||
|
align-self: center;
|
||||||
|
color: $ui-fleet-black-75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.list-item__labels {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import React, { useContext, useState } from "react";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
|
|
||||||
import { IApiError } from "interfaces/errors";
|
|
||||||
import { NotificationContext } from "context/notification";
|
|
||||||
import mdmAPI from "services/entities/mdm";
|
|
||||||
|
|
||||||
import FileUploader from "components/FileUploader";
|
|
||||||
|
|
||||||
import { getErrorMessage } from "./helpers";
|
|
||||||
|
|
||||||
const baseClass = "profile-uploader";
|
|
||||||
|
|
||||||
interface IProfileUploaderProps {
|
|
||||||
currentTeamId: number;
|
|
||||||
onUpload: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProfileUploader = ({
|
|
||||||
currentTeamId,
|
|
||||||
onUpload,
|
|
||||||
}: IProfileUploaderProps) => {
|
|
||||||
const [showLoading, setShowLoading] = useState(false);
|
|
||||||
|
|
||||||
const { renderFlash } = useContext(NotificationContext);
|
|
||||||
|
|
||||||
const onFileUpload = async (files: FileList | null) => {
|
|
||||||
setShowLoading(true);
|
|
||||||
|
|
||||||
if (!files || files.length === 0) {
|
|
||||||
setShowLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = files[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mdmAPI.uploadProfile(file, currentTeamId);
|
|
||||||
renderFlash("success", "Successfully uploaded!");
|
|
||||||
onUpload();
|
|
||||||
} catch (e) {
|
|
||||||
const error = e as AxiosResponse<IApiError>;
|
|
||||||
const errMessage = getErrorMessage(error);
|
|
||||||
renderFlash("error", errMessage);
|
|
||||||
} finally {
|
|
||||||
setShowLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FileUploader
|
|
||||||
graphicName="file-configuration-profile"
|
|
||||||
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`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileUploader;
|
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Card from "components/Card";
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
|
import ProfileGraphic from "./AddProfileGraphic";
|
||||||
|
|
||||||
|
const AddProfileCard = ({
|
||||||
|
baseClass,
|
||||||
|
setShowModal,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) => (
|
||||||
|
<Card color="gray" className={`${baseClass}__card`}>
|
||||||
|
<div className={`${baseClass}__card--content-wrap`}>
|
||||||
|
<ProfileGraphic baseClass={baseClass} showMessage />
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__card--add-button`}
|
||||||
|
variant="brand"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
Add profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AddProfileCard;
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Graphic from "components/Graphic";
|
||||||
|
|
||||||
|
const ALLOWED_FILE_TYPES_MESSAGE =
|
||||||
|
"Configuration profile (.mobileconfig for macOS or .xml for Windows)";
|
||||||
|
|
||||||
|
const ProfileGraphic = ({
|
||||||
|
baseClass,
|
||||||
|
showMessage,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
showMessage?: boolean;
|
||||||
|
}) => (
|
||||||
|
<div className={`${baseClass}__profile-graphic`}>
|
||||||
|
<Graphic
|
||||||
|
key={`file-configuration-profile-graphic`}
|
||||||
|
className={`${baseClass}__graphic`}
|
||||||
|
name={"file-configuration-profile"}
|
||||||
|
/>
|
||||||
|
{showMessage && (
|
||||||
|
<span className={`${baseClass}__profile-graphic--message`}>
|
||||||
|
{ALLOWED_FILE_TYPES_MESSAGE}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ProfileGraphic;
|
@ -0,0 +1,342 @@
|
|||||||
|
import React, { useCallback, useContext, useRef, useState } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { NotificationContext } from "context/notification";
|
||||||
|
|
||||||
|
import { IApiError } from "interfaces/errors";
|
||||||
|
import { ILabelSummary } from "interfaces/label";
|
||||||
|
|
||||||
|
import labelsAPI from "services/entities/labels";
|
||||||
|
import mdmAPI from "services/entities/mdm";
|
||||||
|
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
|
import Card from "components/Card";
|
||||||
|
import Checkbox from "components/forms/fields/Checkbox";
|
||||||
|
import DataError from "components/DataError";
|
||||||
|
import Icon from "components/Icon";
|
||||||
|
import Modal from "components/Modal";
|
||||||
|
import Radio from "components/forms/fields/Radio";
|
||||||
|
import Spinner from "components/Spinner";
|
||||||
|
|
||||||
|
import ProfileGraphic from "./AddProfileGraphic";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_ERROR_MESSAGE,
|
||||||
|
getErrorMessage,
|
||||||
|
parseFile,
|
||||||
|
listNamesFromSelectedLabels,
|
||||||
|
} from "../helpers";
|
||||||
|
|
||||||
|
const FileChooser = ({
|
||||||
|
baseClass,
|
||||||
|
isLoading,
|
||||||
|
onFileOpen,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
onFileOpen: (files: FileList | null) => void;
|
||||||
|
}) => (
|
||||||
|
<div className={`${baseClass}__file-chooser`}>
|
||||||
|
<ProfileGraphic baseClass={baseClass} showMessage />
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__upload-button`}
|
||||||
|
variant="text-icon"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
<label htmlFor="upload-profile">
|
||||||
|
<span className={`${baseClass}__file-chooser--button-wrap`}>
|
||||||
|
<Icon name="upload" />
|
||||||
|
Choose file
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
accept={".mobileconfig,application/x-apple-aspen-config,.xml"}
|
||||||
|
id="upload-profile"
|
||||||
|
type="file"
|
||||||
|
onChange={(e) => {
|
||||||
|
onFileOpen(e.target.files);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FileDetails = ({
|
||||||
|
baseClass,
|
||||||
|
details: { name, platform },
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
details: {
|
||||||
|
name: string;
|
||||||
|
platform: string;
|
||||||
|
};
|
||||||
|
}) => (
|
||||||
|
<div className={`${baseClass}__selected-file`}>
|
||||||
|
<ProfileGraphic baseClass={baseClass} />
|
||||||
|
<div className={`${baseClass}__selected-file--details`}>
|
||||||
|
<div className={`${baseClass}__selected-file--details--name`}>{name}</div>
|
||||||
|
<div className={`${baseClass}__selected-file--details--platform`}>
|
||||||
|
{platform}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TargetChooser = ({
|
||||||
|
baseClass,
|
||||||
|
selectedTarget,
|
||||||
|
setSelectedTarget,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
selectedTarget: string;
|
||||||
|
setSelectedTarget: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`form-field`}>
|
||||||
|
<div className="form-field__label">Target</div>
|
||||||
|
<Radio
|
||||||
|
className={`${baseClass}__radio-input`}
|
||||||
|
label={"All hosts"}
|
||||||
|
id={"all-hosts-target-radio-btn"}
|
||||||
|
checked={selectedTarget === "All hosts"}
|
||||||
|
value={"All hosts"}
|
||||||
|
name={"all-hosts-target"}
|
||||||
|
onChange={setSelectedTarget}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
className={`${baseClass}__radio-input`}
|
||||||
|
label={"Custom"}
|
||||||
|
id={"custom-target-radio-btn"}
|
||||||
|
checked={selectedTarget === "Custom"}
|
||||||
|
value={"Custom"}
|
||||||
|
name={"custom-target"}
|
||||||
|
onChange={setSelectedTarget}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelChooser = ({
|
||||||
|
baseClass,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
labels,
|
||||||
|
selectedLabels,
|
||||||
|
setSelectedLabels,
|
||||||
|
}: {
|
||||||
|
baseClass: string;
|
||||||
|
isError: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
labels: ILabelSummary[];
|
||||||
|
selectedLabels: Record<string, boolean>;
|
||||||
|
setSelectedLabels: React.Dispatch<
|
||||||
|
React.SetStateAction<Record<string, boolean>>
|
||||||
|
>;
|
||||||
|
}) => {
|
||||||
|
const updateSelectedLabels = useCallback(
|
||||||
|
({ name, value }: { name: string; value: boolean }) => {
|
||||||
|
setSelectedLabels((prevItems) => ({ ...prevItems, [name]: value }));
|
||||||
|
},
|
||||||
|
[setSelectedLabels]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`${baseClass}__description`}>
|
||||||
|
Profile will only be applied to hosts that have all these labels:
|
||||||
|
</div>
|
||||||
|
<div className={`${baseClass}__checkboxes`}>
|
||||||
|
{isLoading && <Spinner centered={false} />}
|
||||||
|
{!isLoading && isError && <DataError />}
|
||||||
|
{!isLoading && !isError && !labels.length && (
|
||||||
|
<div className={`${baseClass}__no-labels`}>
|
||||||
|
<b>No labels exist in Fleet</b>
|
||||||
|
<span>Add labels to target specific hosts.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
!isError &&
|
||||||
|
!!labels.length &&
|
||||||
|
labels.map((label) => {
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__label`} key={label.name}>
|
||||||
|
<Checkbox
|
||||||
|
className={`${baseClass}__checkbox`}
|
||||||
|
name={label.name}
|
||||||
|
value={!!selectedLabels[label.name]}
|
||||||
|
onChange={updateSelectedLabels}
|
||||||
|
parseTarget
|
||||||
|
/>
|
||||||
|
<div className={`${baseClass}__label-name`}>{label.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IAddProfileModalProps {
|
||||||
|
baseClass: string;
|
||||||
|
currentTeamId: number;
|
||||||
|
isPremiumTier: boolean;
|
||||||
|
onUpload: () => void;
|
||||||
|
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddProfileModal = ({
|
||||||
|
baseClass,
|
||||||
|
currentTeamId,
|
||||||
|
isPremiumTier,
|
||||||
|
onUpload,
|
||||||
|
setShowModal,
|
||||||
|
}: IAddProfileModalProps) => {
|
||||||
|
const { renderFlash } = useContext(NotificationContext);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [fileDetails, setFileDetails] = useState<{
|
||||||
|
name: string;
|
||||||
|
platform: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [selectedTarget, setSelectedTarget] = useState("All hosts"); // "All hosts" | "Custom"
|
||||||
|
const [selectedLabels, setSelectedLabels] = useState<Record<string, boolean>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileRef = useRef<File | null>(null);
|
||||||
|
|
||||||
|
// NOTE: labels are not automatically refetched in the current implementation
|
||||||
|
const {
|
||||||
|
data: labels,
|
||||||
|
isLoading: isLoadingLabels,
|
||||||
|
isFetching: isFetchingLabels,
|
||||||
|
isError: isErrorLabels,
|
||||||
|
// refetch: refetchLabels,
|
||||||
|
} = useQuery<ILabelSummary[], Error>(
|
||||||
|
["custom_labels"], // NOTE: consider adding selectedTarget to the queryKey to refetch labels when target changes
|
||||||
|
() =>
|
||||||
|
labelsAPI
|
||||||
|
.summary()
|
||||||
|
.then((res) => res.labels.filter((l) => l.label_type !== "builtin")),
|
||||||
|
|
||||||
|
{
|
||||||
|
enabled: isPremiumTier,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false,
|
||||||
|
staleTime: 10000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDone = useCallback(() => {
|
||||||
|
fileRef.current = null;
|
||||||
|
setFileDetails(null);
|
||||||
|
setSelectedLabels({});
|
||||||
|
setShowModal(false);
|
||||||
|
}, [fileRef, setShowModal]);
|
||||||
|
|
||||||
|
const onFileUpload = async () => {
|
||||||
|
if (!fileRef.current) {
|
||||||
|
renderFlash("error", DEFAULT_ERROR_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = fileRef.current;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await mdmAPI.uploadProfile({
|
||||||
|
file,
|
||||||
|
teamId: currentTeamId,
|
||||||
|
labels: listNamesFromSelectedLabels(selectedLabels),
|
||||||
|
});
|
||||||
|
renderFlash("success", "Successfully uploaded!");
|
||||||
|
onUpload();
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: cleanup this error handling
|
||||||
|
renderFlash("error", getErrorMessage(e as AxiosResponse<IApiError>));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFileOpen = async (files: FileList | null) => {
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const file = files[0];
|
||||||
|
fileRef.current = file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [name, platform] = await parseFile(file);
|
||||||
|
setFileDetails({ name, platform });
|
||||||
|
} catch (e) {
|
||||||
|
renderFlash("error", "Invalid file type");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="Add profile" onExit={onDone}>
|
||||||
|
<>
|
||||||
|
{isPremiumTier && isLoadingLabels && <Spinner />}
|
||||||
|
{isPremiumTier && !isLoadingLabels && isErrorLabels && <DataError />}
|
||||||
|
{(!isPremiumTier || (!isLoadingLabels && !isErrorLabels)) && (
|
||||||
|
<div className={`${baseClass}__modal-content-wrap`}>
|
||||||
|
<Card color="gray" className={`${baseClass}__file`}>
|
||||||
|
{!fileDetails ? (
|
||||||
|
<FileChooser
|
||||||
|
baseClass={baseClass}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onFileOpen={onFileOpen}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FileDetails baseClass={baseClass} details={fileDetails} />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
{isPremiumTier && (
|
||||||
|
<div className={`${baseClass}__target`}>
|
||||||
|
<TargetChooser
|
||||||
|
baseClass={baseClass}
|
||||||
|
selectedTarget={selectedTarget}
|
||||||
|
setSelectedTarget={setSelectedTarget}
|
||||||
|
/>
|
||||||
|
{selectedTarget === "Custom" && (
|
||||||
|
<LabelChooser
|
||||||
|
baseClass={baseClass}
|
||||||
|
isError={isErrorLabels}
|
||||||
|
isLoading={isFetchingLabels}
|
||||||
|
labels={labels || []}
|
||||||
|
selectedLabels={selectedLabels}
|
||||||
|
setSelectedLabels={setSelectedLabels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`${baseClass}__button-wrap`}>
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__add-profile-button`}
|
||||||
|
variant="brand"
|
||||||
|
onClick={onFileUpload}
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={
|
||||||
|
// TODO: consider adding tooltip to explain why button is disabled
|
||||||
|
(selectedTarget === "Custom" &&
|
||||||
|
!listNamesFromSelectedLabels(selectedLabels).length) ||
|
||||||
|
!fileDetails
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddProfileModal;
|
@ -2,13 +2,107 @@ import React from "react";
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { IApiError } from "interfaces/errors";
|
import { IApiError } from "interfaces/errors";
|
||||||
|
|
||||||
|
// TODO: mobileconfig parser is a work in progress and not yet used in production
|
||||||
|
// https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles#3234127
|
||||||
|
const parseMobileconfig = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file);
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
reader.onabort = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
// parse mobile as xml
|
||||||
|
const xmlDoc = new DOMParser().parseFromString(
|
||||||
|
reader.result as string,
|
||||||
|
"text/xml"
|
||||||
|
);
|
||||||
|
// check for any parser errors
|
||||||
|
const parserErrors = xmlDoc.getElementsByTagName("parsererror");
|
||||||
|
if (parserErrors.length > 0) {
|
||||||
|
console.warn("parserErrors", parserErrors);
|
||||||
|
throw new Error("Invalid file: parser error");
|
||||||
|
}
|
||||||
|
// get the top-level object, we assume it is the first `<dict>` element in the `<plist>`
|
||||||
|
// https://developer.apple.com/documentation/devicemanagement/toplevel
|
||||||
|
const tlo = xmlDoc.getElementsByTagName("dict")?.[0];
|
||||||
|
if (tlo?.parentElement?.tagName !== "plist") {
|
||||||
|
throw new Error("Invalid file: missing plist");
|
||||||
|
}
|
||||||
|
// get the payload display name from the top-level object, note that there may be other
|
||||||
|
// `<dict>` elements in the `<plist>`, some of which contain `<key>PayloadDisplayName</key>`
|
||||||
|
// elements, but we ignore those for now
|
||||||
|
const pdnKey = Array.from(tlo.children).find(
|
||||||
|
(child) =>
|
||||||
|
child.tagName === "key" &&
|
||||||
|
child.textContent === "PayloadDisplayName"
|
||||||
|
);
|
||||||
|
const pdnVal =
|
||||||
|
(pdnKey?.nextElementSibling?.tagName === "string" &&
|
||||||
|
pdnKey?.nextElementSibling?.textContent) ||
|
||||||
|
"";
|
||||||
|
// if the payload display name is empty, use the file name
|
||||||
|
const result = pdnVal || file.name;
|
||||||
|
console.log("parseMobileconfig result: ", result);
|
||||||
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseFile = async (file: File): Promise<[string, string]> => {
|
||||||
|
// get the file name and extension
|
||||||
|
const nameParts = file.name.split(".");
|
||||||
|
const name = nameParts.slice(0, -1).join(".");
|
||||||
|
const ext = nameParts.slice(-1)[0];
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case "xml": {
|
||||||
|
return [name, "Windows"];
|
||||||
|
}
|
||||||
|
case "mobileconfig": {
|
||||||
|
// // TODO: enable this once mobileconfig parser is vetted
|
||||||
|
// try {
|
||||||
|
// const parsedName = await parseMobileConfig(file);
|
||||||
|
// return [parsedName, "macOS"];
|
||||||
|
// } catch (e) {
|
||||||
|
// console.log("error", e);
|
||||||
|
// return [name, "macOS"];
|
||||||
|
// }
|
||||||
|
return [name, "macOS"];
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Invalid file type: ${ext}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listNamesFromSelectedLabels = (dict: Record<string, boolean>) => {
|
||||||
|
return Object.entries(dict).reduce((acc, [labelName, isSelected]) => {
|
||||||
|
if (isSelected) {
|
||||||
|
acc.push(labelName);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as string[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_ERROR_MESSAGE =
|
||||||
|
"Couldn’t add configuration profile. Please try again.";
|
||||||
|
|
||||||
/** We want to add some additional messageing to some of the error messages so
|
/** 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
|
* we add them in this function. Otherwise, we'll just return the error message from the
|
||||||
* API.
|
* API.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
|
export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
|
||||||
const apiReason = err.data.errors[0].reason;
|
const apiReason = err?.data?.errors?.[0]?.reason;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
apiReason.includes(
|
apiReason.includes(
|
||||||
@ -33,5 +127,5 @@ export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return apiReason;
|
return apiReason || DEFAULT_ERROR_MESSAGE;
|
||||||
};
|
};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { default } from "./ProfileUploader";
|
|
@ -40,6 +40,12 @@ export interface IMdmProfilesResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUploadProfileApiParams {
|
||||||
|
file: File;
|
||||||
|
teamId?: number;
|
||||||
|
labels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const mdmService = {
|
const mdmService = {
|
||||||
downloadDeviceUserEnrollmentProfile: (token: string) => {
|
downloadDeviceUserEnrollmentProfile: (token: string) => {
|
||||||
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
|
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
|
||||||
@ -79,7 +85,7 @@ const mdmService = {
|
|||||||
return sendRequest("GET", path);
|
return sendRequest("GET", path);
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadProfile: (file: File, teamId?: number) => {
|
uploadProfile: ({ file, teamId, labels }: IUploadProfileApiParams) => {
|
||||||
const { MDM_PROFILES } = endpoints;
|
const { MDM_PROFILES } = endpoints;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@ -89,6 +95,10 @@ const mdmService = {
|
|||||||
formData.append("team_id", teamId.toString());
|
formData.append("team_id", teamId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labels?.forEach((label) => {
|
||||||
|
formData.append("labels", label);
|
||||||
|
});
|
||||||
|
|
||||||
return sendRequest("POST", MDM_PROFILES, formData);
|
return sendRequest("POST", MDM_PROFILES, formData);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -53,6 +53,31 @@ import { IScheduledQueryStats } from "interfaces/scheduled_query_stats";
|
|||||||
const ORG_INFO_ATTRS = ["org_name", "org_logo_url"];
|
const ORG_INFO_ATTRS = ["org_name", "org_logo_url"];
|
||||||
const ADMIN_ATTRS = ["email", "name", "password", "password_confirmation"];
|
const ADMIN_ATTRS = ["email", "name", "password", "password_confirmation"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param count The number of items.
|
||||||
|
* @param root The root of the word, omitting any suffixs.
|
||||||
|
* @param pluralSuffix The suffix to add to the root if the count is not 1.
|
||||||
|
* @param singularSuffix The suffix to add to the root if the count is 1.
|
||||||
|
* @returns A string with the root and the appropriate suffix.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* pluralize(1, "hero", "es", "") // "hero"
|
||||||
|
* pluralize(0, "hero", "es", "") // "heroes"
|
||||||
|
* pluralize(1, "fair", "ies", "y") // "fairy"
|
||||||
|
* pluralize(2, "fair", "ies", "y") // "fairies"
|
||||||
|
* pluralize(1, "dragon") // "dragon"
|
||||||
|
* pluralize(2, "dragon") // "dragons"
|
||||||
|
*/
|
||||||
|
export const pluralize = (
|
||||||
|
count: number,
|
||||||
|
root: string,
|
||||||
|
pluralSuffix: string,
|
||||||
|
singularSuffix: string
|
||||||
|
) => {
|
||||||
|
return `${root}${count !== 1 ? pluralSuffix : singularSuffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const addGravatarUrlToResource = (resource: any): any => {
|
export const addGravatarUrlToResource = (resource: any): any => {
|
||||||
const { email } = resource;
|
const { email } = resource;
|
||||||
const gravatarAvailable =
|
const gravatarAvailable =
|
||||||
@ -844,6 +869,7 @@ export const getUniqueColumnNamesFromRows = (rows: any[]) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
pluralize,
|
||||||
addGravatarUrlToResource,
|
addGravatarUrlToResource,
|
||||||
formatConfigDataForServer,
|
formatConfigDataForServer,
|
||||||
formatLabelResponse,
|
formatLabelResponse,
|
||||||
|
@ -565,7 +565,7 @@ func TestCachedTeamMDMConfig(t *testing.T) {
|
|||||||
Deadline: optjson.SetString("1992-03-01"),
|
Deadline: optjson.SetString("1992-03-01"),
|
||||||
},
|
},
|
||||||
MacOSSettings: fleet.MacOSSettings{
|
MacOSSettings: fleet.MacOSSettings{
|
||||||
CustomSettings: []string{"a", "b"},
|
CustomSettings: []fleet.MDMProfileSpec{{Path: "a"}, {Path: "b"}},
|
||||||
DeprecatedEnableDiskEncryption: ptr.Bool(false),
|
DeprecatedEnableDiskEncryption: ptr.Bool(false),
|
||||||
},
|
},
|
||||||
MacOSSetup: fleet.MacOSSetup{
|
MacOSSetup: fleet.MacOSSetup{
|
||||||
@ -614,7 +614,7 @@ func TestCachedTeamMDMConfig(t *testing.T) {
|
|||||||
require.False(t, mockedDS.TeamMDMConfigFuncInvoked)
|
require.False(t, mockedDS.TeamMDMConfigFuncInvoked)
|
||||||
|
|
||||||
// changing some deep value doesn't affect the stored value
|
// changing some deep value doesn't affect the stored value
|
||||||
mdmConfig.MacOSSettings.CustomSettings[0] = "c"
|
mdmConfig.MacOSSettings.CustomSettings[0] = fleet.MDMProfileSpec{Path: "c"}
|
||||||
require.NotEqual(t, testMDMConfig, *mdmConfig)
|
require.NotEqual(t, testMDMConfig, *mdmConfig)
|
||||||
|
|
||||||
// saving a team updates config in cache
|
// saving a team updates config in cache
|
||||||
|
@ -38,31 +38,48 @@ INSERT INTO
|
|||||||
teamID = *cp.TeamID
|
teamID = *cp.TeamID
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt,
|
var profileID int64
|
||||||
profUUID, teamID, cp.Identifier, cp.Name, cp.Mobileconfig, cp.Mobileconfig, cp.Name, teamID)
|
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
|
res, err := tx.ExecContext(ctx, stmt,
|
||||||
|
profUUID, teamID, cp.Identifier, cp.Name, cp.Mobileconfig, cp.Mobileconfig, cp.Name, teamID)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case isDuplicate(err):
|
||||||
|
return ctxerr.Wrap(ctx, formatErrorDuplicateConfigProfile(err, &cp))
|
||||||
|
default:
|
||||||
|
return ctxerr.Wrap(ctx, err, "creating new apple mdm config profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aff, _ := res.RowsAffected()
|
||||||
|
if aff == 0 {
|
||||||
|
return &existsError{
|
||||||
|
ResourceType: "MDMAppleConfigProfile.PayloadDisplayName",
|
||||||
|
Identifier: cp.Name,
|
||||||
|
TeamID: cp.TeamID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// record the ID as we want to return a fleet.Profile instance with it
|
||||||
|
// filled in.
|
||||||
|
profileID, _ = res.LastInsertId()
|
||||||
|
|
||||||
|
for i := range cp.Labels {
|
||||||
|
cp.Labels[i].ProfileUUID = profUUID
|
||||||
|
}
|
||||||
|
if err := batchSetProfileLabelAssociationsDB(ctx, tx, cp.Labels, "darwin"); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "inserting darwin profile label associations")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
return nil, ctxerr.Wrap(ctx, err, "inserting profile and label associations")
|
||||||
case isDuplicate(err):
|
|
||||||
return nil, ctxerr.Wrap(ctx, formatErrorDuplicateConfigProfile(err, &cp))
|
|
||||||
default:
|
|
||||||
return nil, ctxerr.Wrap(ctx, err, "creating new apple mdm config profile")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aff, _ := res.RowsAffected()
|
|
||||||
if aff == 0 {
|
|
||||||
return nil, &existsError{
|
|
||||||
ResourceType: "MDMAppleConfigProfile.PayloadDisplayName",
|
|
||||||
Identifier: cp.Name,
|
|
||||||
TeamID: cp.TeamID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
id, _ := res.LastInsertId()
|
|
||||||
|
|
||||||
return &fleet.MDMAppleConfigProfile{
|
return &fleet.MDMAppleConfigProfile{
|
||||||
ProfileUUID: profUUID,
|
ProfileUUID: profUUID,
|
||||||
ProfileID: uint(id),
|
ProfileID: uint(profileID),
|
||||||
Identifier: cp.Identifier,
|
Identifier: cp.Identifier,
|
||||||
Name: cp.Name,
|
Name: cp.Name,
|
||||||
Mobileconfig: cp.Mobileconfig,
|
Mobileconfig: cp.Mobileconfig,
|
||||||
@ -172,6 +189,19 @@ WHERE
|
|||||||
return nil, ctxerr.Wrap(ctx, err, "get mdm apple config profile")
|
return nil, ctxerr.Wrap(ctx, err, "get mdm apple config profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the labels for that profile, except if the profile was loaded by the
|
||||||
|
// old (deprecated) endpoint.
|
||||||
|
if uuid != "" {
|
||||||
|
labels, err := ds.listProfileLabelsForProfiles(ctx, nil, []string{res.ProfileUUID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(labels) > 0 {
|
||||||
|
// ensure we leave Labels nil if there are none
|
||||||
|
res.Labels = labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1150,6 +1180,7 @@ func (ds *Datastore) batchSetMDMAppleProfilesDB(
|
|||||||
const loadExistingProfiles = `
|
const loadExistingProfiles = `
|
||||||
SELECT
|
SELECT
|
||||||
identifier,
|
identifier,
|
||||||
|
profile_uuid,
|
||||||
mobileconfig
|
mobileconfig
|
||||||
FROM
|
FROM
|
||||||
mdm_apple_configuration_profiles
|
mdm_apple_configuration_profiles
|
||||||
@ -1245,6 +1276,40 @@ ON DUPLICATE KEY UPDATE
|
|||||||
return ctxerr.Wrapf(ctx, err, "insert new/edited profile with identifier %q", p.Identifier)
|
return ctxerr.Wrapf(ctx, err, "insert new/edited profile with identifier %q", p.Identifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build a list of labels so the associations can be batch-set all at once
|
||||||
|
// TODO: with minor changes this chunk of code could be shared
|
||||||
|
// between macOS and Windows, but at the time of this
|
||||||
|
// implementation we're under tight time constraints.
|
||||||
|
incomingLabels := []fleet.ConfigurationProfileLabel{}
|
||||||
|
if len(incomingIdents) > 0 {
|
||||||
|
var newlyInsertedProfs []*fleet.MDMAppleConfigProfile
|
||||||
|
// load current profiles (again) that match the incoming profiles by name to grab their uuids
|
||||||
|
stmt, args, err := sqlx.In(loadExistingProfiles, profTeamID, incomingIdents)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "build query to load newly inserted profiles")
|
||||||
|
}
|
||||||
|
if err := sqlx.SelectContext(ctx, tx, &newlyInsertedProfs, stmt, args...); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "load newly inserted profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newlyInsertedProf := range newlyInsertedProfs {
|
||||||
|
incomingProf, ok := incomingProfs[newlyInsertedProf.Identifier]
|
||||||
|
if !ok {
|
||||||
|
return ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range incomingProf.Labels {
|
||||||
|
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||||
|
incomingLabels = append(incomingLabels, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert label associations
|
||||||
|
if err := batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels, "darwin"); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "inserting apple profile label associations")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1316,39 +1381,50 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const desiredStateStmt = `
|
// TODO(mna): the conditions here (and in toRemoveStmt) are subtly different
|
||||||
SELECT
|
// than the ones in ListMDMAppleProfilesToInstall/Remove, so I'm keeping
|
||||||
ds.profile_uuid as profile_uuid,
|
// those statements distinct to avoid introducing a subtle bug, but we should
|
||||||
ds.host_uuid as host_uuid,
|
// take the time to properly analyze this and try to reuse
|
||||||
ds.profile_identifier as profile_identifier,
|
// ListMDMAppleProfilesToInstall/Remove as we do in the Windows equivalent
|
||||||
ds.profile_name as profile_name,
|
// method.
|
||||||
ds.checksum as checksum
|
//
|
||||||
FROM (
|
// I.e. for toInstallStmt, this is missing:
|
||||||
SELECT
|
// -- profiles in A and B with operation type "install" and NULL status
|
||||||
macp.profile_uuid,
|
// but I believe it would be a no-op and no harm in adding (status is
|
||||||
h.uuid as host_uuid,
|
// already NULL).
|
||||||
macp.identifier as profile_identifier,
|
//
|
||||||
macp.name as profile_name,
|
// And for toRemoveStmt, this is different:
|
||||||
macp.checksum as checksum
|
// -- except "remove" operations in any state
|
||||||
FROM mdm_apple_configuration_profiles macp
|
// vs
|
||||||
JOIN hosts h ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
// -- except "remove" operations in a terminal state or already pending
|
||||||
JOIN nano_enrollments ne ON ne.device_id = h.uuid
|
// but again I believe it would be a no-op and no harm in making them the
|
||||||
WHERE h.platform = 'darwin' AND ne.enabled = 1 AND ne.type = 'Device' AND h.uuid IN (?)
|
// same (if I'm understanding correctly, the only difference is that it
|
||||||
) as ds
|
// considers "remove" operations that have NULL status, which it would
|
||||||
|
// update to make its status to NULL).
|
||||||
|
|
||||||
|
toInstallStmt := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
ds.profile_uuid as profile_uuid,
|
||||||
|
ds.host_uuid as host_uuid,
|
||||||
|
ds.profile_identifier as profile_identifier,
|
||||||
|
ds.profile_name as profile_name,
|
||||||
|
ds.checksum as checksum
|
||||||
|
FROM ( %s ) as ds
|
||||||
LEFT JOIN host_mdm_apple_profiles hmap
|
LEFT JOIN host_mdm_apple_profiles hmap
|
||||||
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
|
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
|
||||||
WHERE
|
WHERE
|
||||||
-- profile has been updated
|
-- profile has been updated
|
||||||
( hmap.checksum != ds.checksum ) OR
|
( hmap.checksum != ds.checksum ) OR
|
||||||
-- profiles in A but not in B
|
-- profiles in A but not in B
|
||||||
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
|
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
|
||||||
-- profiles in A and B but with operation type "remove"
|
-- profiles in A and B but with operation type "remove"
|
||||||
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) )`
|
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) )
|
||||||
|
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||||
|
|
||||||
// TODO: if a very large number (~65K) of host uuids was matched (via
|
// TODO: if a very large number (~65K) of host uuids was matched (via
|
||||||
// uuids, teams or profile IDs), could result in too many placeholders (not
|
// uuids, teams or profile IDs), could result in too many placeholders (not
|
||||||
// an immediate concern).
|
// an immediate concern).
|
||||||
stmt, args, err := sqlx.In(desiredStateStmt, uuids, fleet.MDMOperationTypeRemove)
|
stmt, args, err := sqlx.In(toInstallStmt, uuids, uuids, fleet.MDMOperationTypeRemove)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "building profiles to install statement")
|
return ctxerr.Wrap(ctx, err, "building profiles to install statement")
|
||||||
}
|
}
|
||||||
@ -1359,39 +1435,40 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||||||
return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute")
|
return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute")
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentStateStmt = `
|
toRemoveStmt := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
hmap.profile_uuid as profile_uuid,
|
hmap.profile_uuid as profile_uuid,
|
||||||
hmap.host_uuid as host_uuid,
|
hmap.host_uuid as host_uuid,
|
||||||
hmap.profile_identifier as profile_identifier,
|
hmap.profile_identifier as profile_identifier,
|
||||||
hmap.profile_name as profile_name,
|
hmap.profile_name as profile_name,
|
||||||
hmap.checksum as checksum,
|
hmap.checksum as checksum,
|
||||||
hmap.status as status,
|
hmap.status as status,
|
||||||
hmap.operation_type as operation_type,
|
hmap.operation_type as operation_type,
|
||||||
COALESCE(hmap.detail, '') as detail,
|
COALESCE(hmap.detail, '') as detail,
|
||||||
hmap.command_uuid as command_uuid
|
hmap.command_uuid as command_uuid
|
||||||
FROM (
|
FROM ( %s ) as ds
|
||||||
SELECT
|
|
||||||
h.uuid, macp.profile_uuid
|
|
||||||
FROM mdm_apple_configuration_profiles macp
|
|
||||||
JOIN hosts h ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
|
||||||
JOIN nano_enrollments ne ON ne.device_id = h.uuid
|
|
||||||
WHERE h.platform = 'darwin' AND ne.enabled = 1 AND ne.type = 'Device' AND h.uuid IN (?)
|
|
||||||
) as ds
|
|
||||||
RIGHT JOIN host_mdm_apple_profiles hmap
|
RIGHT JOIN host_mdm_apple_profiles hmap
|
||||||
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.uuid
|
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
|
||||||
WHERE
|
WHERE
|
||||||
hmap.host_uuid IN (?)
|
hmap.host_uuid IN (?) AND
|
||||||
-- profiles that are in B but not in A
|
-- profiles that are in B but not in A
|
||||||
AND ds.profile_uuid IS NULL AND ds.uuid IS NULL
|
ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND
|
||||||
-- except "remove" operations in any state
|
-- except "remove" operations in any state
|
||||||
AND ( hmap.operation_type IS NULL OR hmap.operation_type != ? )
|
( hmap.operation_type IS NULL OR hmap.operation_type != ? ) AND
|
||||||
`
|
-- except "would be removed" profiles if they are a broken label-based profile
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE
|
||||||
|
mcpl.apple_profile_uuid = hmap.profile_uuid AND
|
||||||
|
mcpl.label_id IS NULL
|
||||||
|
)
|
||||||
|
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||||
|
|
||||||
// TODO: if a very large number (~65K) of host uuids was matched (via
|
// TODO: if a very large number (~65K) of host uuids was matched (via
|
||||||
// uuids, teams or profile IDs), could result in too many placeholders (not
|
// uuids, teams or profile IDs), could result in too many placeholders (not
|
||||||
// an immediate concern). Note that uuids are provided twice.
|
// an immediate concern). Note that uuids are provided twice.
|
||||||
stmt, args, err = sqlx.In(currentStateStmt, uuids, uuids, fleet.MDMOperationTypeRemove)
|
stmt, args, err = sqlx.In(toRemoveStmt, uuids, uuids, uuids, fleet.MDMOperationTypeRemove)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "building profiles to remove statement")
|
return ctxerr.Wrap(ctx, err, "building profiles to remove statement")
|
||||||
}
|
}
|
||||||
@ -1534,19 +1611,79 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appleMDMProfilesDesiredStateQuery = `
|
||||||
|
-- non label-based profiles
|
||||||
|
SELECT
|
||||||
|
macp.profile_uuid,
|
||||||
|
h.uuid as host_uuid,
|
||||||
|
macp.identifier as profile_identifier,
|
||||||
|
macp.name as profile_name,
|
||||||
|
macp.checksum as checksum,
|
||||||
|
0 as count_profile_labels,
|
||||||
|
0 as count_host_labels
|
||||||
|
FROM
|
||||||
|
mdm_apple_configuration_profiles macp
|
||||||
|
JOIN hosts h
|
||||||
|
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
||||||
|
JOIN nano_enrollments ne
|
||||||
|
ON ne.device_id = h.uuid
|
||||||
|
WHERE
|
||||||
|
h.platform = 'darwin' AND
|
||||||
|
ne.enabled = 1 AND
|
||||||
|
ne.type = 'Device' AND
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE mcpl.apple_profile_uuid = macp.profile_uuid
|
||||||
|
) AND
|
||||||
|
( %s )
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- label-based profiles where the host is a member of all the labels
|
||||||
|
SELECT
|
||||||
|
macp.profile_uuid,
|
||||||
|
h.uuid as host_uuid,
|
||||||
|
macp.identifier as profile_identifier,
|
||||||
|
macp.name as profile_name,
|
||||||
|
macp.checksum as checksum,
|
||||||
|
COUNT(*) as count_profile_labels,
|
||||||
|
COUNT(lm.label_id) as count_host_labels
|
||||||
|
FROM
|
||||||
|
mdm_apple_configuration_profiles macp
|
||||||
|
JOIN hosts h
|
||||||
|
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
||||||
|
JOIN nano_enrollments ne
|
||||||
|
ON ne.device_id = h.uuid
|
||||||
|
JOIN mdm_configuration_profile_labels mcpl
|
||||||
|
ON mcpl.apple_profile_uuid = macp.profile_uuid
|
||||||
|
LEFT OUTER JOIN label_membership lm
|
||||||
|
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||||
|
WHERE
|
||||||
|
h.platform = 'darwin' AND
|
||||||
|
ne.enabled = 1 AND
|
||||||
|
ne.type = 'Device' AND
|
||||||
|
( %s )
|
||||||
|
GROUP BY
|
||||||
|
macp.profile_uuid, h.uuid, macp.identifier, macp.name, macp.checksum
|
||||||
|
HAVING
|
||||||
|
count_profile_labels > 0 AND count_host_labels = count_profile_labels
|
||||||
|
`
|
||||||
|
|
||||||
func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
||||||
// The query below is a set difference between:
|
// The query below is a set difference between:
|
||||||
//
|
//
|
||||||
// - Set A (ds), the desired state, can be obtained from a JOIN between
|
// - Set A (ds), the "desired state", can be obtained from a JOIN between
|
||||||
// mdm_apple_configuration_profiles and hosts.
|
// mdm_apple_configuration_profiles and hosts.
|
||||||
// - Set B, the current state given by host_mdm_apple_profiles.
|
//
|
||||||
|
// - Set B, the "current state" given by host_mdm_apple_profiles.
|
||||||
//
|
//
|
||||||
// A - B gives us the profiles that need to be installed:
|
// A - B gives us the profiles that need to be installed:
|
||||||
//
|
//
|
||||||
// - profiles that are in A but not in B
|
// - profiles that are in A but not in B
|
||||||
//
|
//
|
||||||
// - profiles which contents have changed, but their identifier are
|
// - profiles which contents have changed, but their identifier are
|
||||||
// the same (by matching checksums)
|
// the same (by checking the checksums)
|
||||||
//
|
//
|
||||||
// - profiles that are in A and in B, but with an operation type of
|
// - profiles that are in A and in B, but with an operation type of
|
||||||
// "remove", regardless of the status. (technically, if status is NULL then
|
// "remove", regardless of the status. (technically, if status is NULL then
|
||||||
@ -1562,39 +1699,35 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee
|
|||||||
// and a NULL status. Other statuses mean that the operation is already in
|
// and a NULL status. Other statuses mean that the operation is already in
|
||||||
// flight (pending), the operation has been completed but is still subject
|
// flight (pending), the operation has been completed but is still subject
|
||||||
// to independent verification by Fleet (verifying), or has reached a terminal
|
// to independent verification by Fleet (verifying), or has reached a terminal
|
||||||
// state (failed or verified). If the profile's content is edited, all relevant hosts will
|
// state (failed or verified). If the profile's content is edited, all
|
||||||
// be marked as status NULL so that it gets re-installed.
|
// relevant hosts will be marked as status NULL so that it gets
|
||||||
query := `
|
// re-installed.
|
||||||
SELECT
|
//
|
||||||
ds.profile_uuid,
|
// Note that for label-based profiles, only fully-satisfied profiles are
|
||||||
ds.host_uuid,
|
// considered for installation. This means that a broken label-based profile,
|
||||||
ds.profile_identifier,
|
// where one of the labels does not exist anymore, will not be considered for
|
||||||
ds.profile_name,
|
// installation.
|
||||||
ds.checksum
|
|
||||||
FROM (
|
query := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
macp.profile_uuid,
|
ds.profile_uuid,
|
||||||
h.uuid as host_uuid,
|
ds.host_uuid,
|
||||||
macp.identifier as profile_identifier,
|
ds.profile_identifier,
|
||||||
macp.name as profile_name,
|
ds.profile_name,
|
||||||
macp.checksum as checksum
|
ds.checksum
|
||||||
FROM mdm_apple_configuration_profiles macp
|
FROM ( %s ) as ds
|
||||||
JOIN hosts h ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
LEFT JOIN host_mdm_apple_profiles hmap
|
||||||
JOIN nano_enrollments ne ON ne.device_id = h.uuid
|
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
|
||||||
WHERE h.platform = 'darwin' AND ne.enabled = 1 AND ne.type = 'Device'
|
WHERE
|
||||||
) as ds
|
-- profile has been updated
|
||||||
LEFT JOIN host_mdm_apple_profiles hmap
|
( hmap.checksum != ds.checksum ) OR
|
||||||
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
|
-- profiles in A but not in B
|
||||||
WHERE
|
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
|
||||||
-- profile has been updated
|
-- profiles in A and B but with operation type "remove"
|
||||||
( hmap.checksum != ds.checksum ) OR
|
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) ) OR
|
||||||
-- profiles in A but not in B
|
-- profiles in A and B with operation type "install" and NULL status
|
||||||
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
|
( hmap.host_uuid IS NOT NULL AND hmap.operation_type = ? AND hmap.status IS NULL )
|
||||||
-- profiles in A and B but with operation type "remove"
|
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "TRUE", "TRUE"))
|
||||||
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) ) OR
|
|
||||||
-- profiles in A and B with operation type "install" and NULL status
|
|
||||||
( hmap.host_uuid IS NOT NULL AND hmap.operation_type = ? AND hmap.status IS NULL )
|
|
||||||
`
|
|
||||||
|
|
||||||
var profiles []*fleet.MDMAppleProfilePayload
|
var profiles []*fleet.MDMAppleProfilePayload
|
||||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove, fleet.MDMOperationTypeInstall)
|
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove, fleet.MDMOperationTypeInstall)
|
||||||
@ -1604,9 +1737,10 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee
|
|||||||
func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
||||||
// The query below is a set difference between:
|
// The query below is a set difference between:
|
||||||
//
|
//
|
||||||
// - Set A (ds), the desired state, can be obtained from a JOIN between
|
// - Set A (ds), the "desired state", can be obtained from a JOIN between
|
||||||
// mdm_apple_configuration_profiles and hosts.
|
// mdm_apple_configuration_profiles and hosts.
|
||||||
// - Set B, the current state given by host_mdm_apple_profiles.
|
//
|
||||||
|
// - Set B, the "current state" given by host_mdm_apple_profiles.
|
||||||
//
|
//
|
||||||
// B - A gives us the profiles that need to be removed:
|
// B - A gives us the profiles that need to be removed:
|
||||||
//
|
//
|
||||||
@ -1619,31 +1753,43 @@ func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet
|
|||||||
// Any other case are profiles that are in both B and A, and as such are
|
// Any other case are profiles that are in both B and A, and as such are
|
||||||
// processed by the ListMDMAppleProfilesToInstall method (since they are in
|
// processed by the ListMDMAppleProfilesToInstall method (since they are in
|
||||||
// both, their desired state is necessarily to be installed).
|
// both, their desired state is necessarily to be installed).
|
||||||
query := `
|
//
|
||||||
SELECT
|
// Note that for label-based profiles, only those that are fully-sastisfied
|
||||||
hmap.profile_uuid,
|
// by the host are considered for install (are part of the desired state used
|
||||||
hmap.profile_identifier,
|
// to compute the ones to remove). However, as a special case, a broken
|
||||||
hmap.profile_name,
|
// label-based profile will NOT be removed from a host where it was
|
||||||
hmap.host_uuid,
|
// previously installed. However, if a host used to satisfy a label-based
|
||||||
hmap.checksum,
|
// profile but no longer does (and that label-based profile is not "broken"),
|
||||||
hmap.operation_type,
|
// the profile will be removed from the host.
|
||||||
COALESCE(hmap.detail, '') as detail,
|
|
||||||
hmap.status,
|
query := fmt.Sprintf(`
|
||||||
hmap.command_uuid
|
SELECT
|
||||||
FROM (
|
hmap.profile_uuid,
|
||||||
SELECT h.uuid, macp.profile_uuid
|
hmap.profile_identifier,
|
||||||
FROM mdm_apple_configuration_profiles macp
|
hmap.profile_name,
|
||||||
JOIN hosts h ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
hmap.host_uuid,
|
||||||
JOIN nano_enrollments ne ON ne.device_id = h.uuid
|
hmap.checksum,
|
||||||
WHERE h.platform = 'darwin' AND ne.enabled = 1 AND ne.type = 'Device'
|
hmap.operation_type,
|
||||||
) as ds
|
COALESCE(hmap.detail, '') as detail,
|
||||||
RIGHT JOIN host_mdm_apple_profiles hmap
|
hmap.status,
|
||||||
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.uuid
|
hmap.command_uuid
|
||||||
-- profiles that are in B but not in A
|
FROM ( %s ) as ds
|
||||||
WHERE ds.profile_uuid IS NULL AND ds.uuid IS NULL
|
RIGHT JOIN host_mdm_apple_profiles hmap
|
||||||
-- except "remove" operations in a terminal state or already pending
|
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
|
||||||
AND ( hmap.operation_type IS NULL OR hmap.operation_type != ? OR hmap.status IS NULL )
|
WHERE
|
||||||
`
|
-- profiles that are in B but not in A
|
||||||
|
ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND
|
||||||
|
-- except "remove" operations in a terminal state or already pending
|
||||||
|
( hmap.operation_type IS NULL OR hmap.operation_type != ? OR hmap.status IS NULL ) AND
|
||||||
|
-- except "would be removed" profiles if they are a broken label-based profile
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE
|
||||||
|
mcpl.apple_profile_uuid = hmap.profile_uuid AND
|
||||||
|
mcpl.label_id IS NULL
|
||||||
|
)
|
||||||
|
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "TRUE", "TRUE"))
|
||||||
|
|
||||||
var profiles []*fleet.MDMAppleProfilePayload
|
var profiles []*fleet.MDMAppleProfilePayload
|
||||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove)
|
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove)
|
||||||
|
@ -39,6 +39,7 @@ func TestMDMApple(t *testing.T) {
|
|||||||
fn func(t *testing.T, ds *Datastore)
|
fn func(t *testing.T, ds *Datastore)
|
||||||
}{
|
}{
|
||||||
{"TestNewMDMAppleConfigProfileDuplicateName", testNewMDMAppleConfigProfileDuplicateName},
|
{"TestNewMDMAppleConfigProfileDuplicateName", testNewMDMAppleConfigProfileDuplicateName},
|
||||||
|
{"TestNewMDMAppleConfigProfileLabels", testNewMDMAppleConfigProfileLabels},
|
||||||
{"TestNewMDMAppleConfigProfileDuplicateIdentifier", testNewMDMAppleConfigProfileDuplicateIdentifier},
|
{"TestNewMDMAppleConfigProfileDuplicateIdentifier", testNewMDMAppleConfigProfileDuplicateIdentifier},
|
||||||
{"TestDeleteMDMAppleConfigProfile", testDeleteMDMAppleConfigProfile},
|
{"TestDeleteMDMAppleConfigProfile", testDeleteMDMAppleConfigProfile},
|
||||||
{"TestDeleteMDMAppleConfigProfileByTeamAndIdentifier", testDeleteMDMAppleConfigProfileByTeamAndIdentifier},
|
{"TestDeleteMDMAppleConfigProfileByTeamAndIdentifier", testDeleteMDMAppleConfigProfileByTeamAndIdentifier},
|
||||||
@ -138,6 +139,38 @@ func testNewMDMAppleConfigProfileDuplicateName(t *testing.T, ds *Datastore) {
|
|||||||
require.ErrorAs(t, err, &existsErr)
|
require.ErrorAs(t, err, &existsErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testNewMDMAppleConfigProfileLabels(t *testing.T, ds *Datastore) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dummyMC := mobileconfig.Mobileconfig([]byte("DummyTestMobileconfigBytes"))
|
||||||
|
cp := fleet.MDMAppleConfigProfile{
|
||||||
|
Name: "DummyTestName",
|
||||||
|
Identifier: "DummyTestIdentifier",
|
||||||
|
Mobileconfig: dummyMC,
|
||||||
|
TeamID: nil,
|
||||||
|
Labels: []fleet.ConfigurationProfileLabel{
|
||||||
|
{LabelName: "foo", LabelID: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := ds.NewMDMAppleConfigProfile(ctx, cp)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.True(t, fleet.IsForeignKey(err))
|
||||||
|
|
||||||
|
label := &fleet.Label{
|
||||||
|
Name: "my label",
|
||||||
|
Description: "a label",
|
||||||
|
Query: "select 1 from processes;",
|
||||||
|
Platform: "darwin",
|
||||||
|
}
|
||||||
|
label, err = ds.NewLabel(ctx, label)
|
||||||
|
require.NoError(t, err)
|
||||||
|
cp.Labels = []fleet.ConfigurationProfileLabel{
|
||||||
|
{LabelName: label.Name, LabelID: label.ID},
|
||||||
|
}
|
||||||
|
prof, err := ds.NewMDMAppleConfigProfile(ctx, cp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, prof.ProfileUUID)
|
||||||
|
}
|
||||||
|
|
||||||
func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore) {
|
func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
initialCP := storeDummyConfigProfileForTest(t, ds)
|
initialCP := storeDummyConfigProfileForTest(t, ds)
|
||||||
@ -163,9 +196,46 @@ func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore
|
|||||||
storedCP, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, newCP.ProfileID)
|
storedCP, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, newCP.ProfileID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkConfigProfile(t, *newCP, *storedCP)
|
checkConfigProfile(t, *newCP, *storedCP)
|
||||||
|
require.Nil(t, storedCP.Labels)
|
||||||
storedCP, err = ds.GetMDMAppleConfigProfile(ctx, newCP.ProfileUUID)
|
storedCP, err = ds.GetMDMAppleConfigProfile(ctx, newCP.ProfileUUID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkConfigProfile(t, *newCP, *storedCP)
|
checkConfigProfile(t, *newCP, *storedCP)
|
||||||
|
require.Nil(t, storedCP.Labels)
|
||||||
|
|
||||||
|
// create a label-based profile
|
||||||
|
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "lbl", Query: "select 1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelCP := fleet.MDMAppleConfigProfile{
|
||||||
|
Name: "label-based",
|
||||||
|
Identifier: "label-based",
|
||||||
|
Mobileconfig: mobileconfig.Mobileconfig([]byte("LabelTestMobileconfigBytes")),
|
||||||
|
Labels: []fleet.ConfigurationProfileLabel{
|
||||||
|
{LabelName: lbl.Name, LabelID: lbl.ID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
labelProf, err := ds.NewMDMAppleConfigProfile(ctx, labelCP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// get it back from both the deprecated ID and the uuid methods, labels are
|
||||||
|
// only included in the uuid one
|
||||||
|
prof, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, labelProf.ProfileID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, prof.Labels)
|
||||||
|
prof, err = ds.GetMDMAppleConfigProfile(ctx, labelProf.ProfileUUID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prof.Labels, 1)
|
||||||
|
require.Equal(t, lbl.Name, prof.Labels[0].LabelName)
|
||||||
|
require.False(t, prof.Labels[0].Broken)
|
||||||
|
|
||||||
|
// break the profile by deleting the label
|
||||||
|
require.NoError(t, ds.DeleteLabel(ctx, lbl.Name))
|
||||||
|
|
||||||
|
prof, err = ds.GetMDMAppleConfigProfile(ctx, labelProf.ProfileUUID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prof.Labels, 1)
|
||||||
|
require.Equal(t, lbl.Name, prof.Labels[0].LabelName)
|
||||||
|
require.True(t, prof.Labels[0].Broken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateCP(name string, identifier string, teamID uint) *fleet.MDMAppleConfigProfile {
|
func generateCP(name string, identifier string, teamID uint) *fleet.MDMAppleConfigProfile {
|
||||||
@ -1027,12 +1097,17 @@ func configProfileBytesForTest(name, identifier, uuid string) []byte {
|
|||||||
`, name, identifier, uuid))
|
`, name, identifier, uuid))
|
||||||
}
|
}
|
||||||
|
|
||||||
func configProfileForTest(t *testing.T, name, identifier, uuid string) *fleet.MDMAppleConfigProfile {
|
func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ...*fleet.Label) *fleet.MDMAppleConfigProfile {
|
||||||
prof := configProfileBytesForTest(name, identifier, uuid)
|
prof := configProfileBytesForTest(name, identifier, uuid)
|
||||||
cp, err := fleet.NewMDMAppleConfigProfile(configProfileBytesForTest(name, identifier, uuid), nil)
|
cp, err := fleet.NewMDMAppleConfigProfile(configProfileBytesForTest(name, identifier, uuid), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sum := md5.Sum(prof) // nolint:gosec // used only to hash for efficient comparisons
|
sum := md5.Sum(prof) // nolint:gosec // used only to hash for efficient comparisons
|
||||||
cp.Checksum = sum[:]
|
cp.Checksum = sum[:]
|
||||||
|
|
||||||
|
for _, lbl := range labels {
|
||||||
|
cp.Labels = append(cp.Labels, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||||
|
}
|
||||||
|
|
||||||
return cp
|
return cp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -861,27 +861,32 @@ func (ds *Datastore) SearchLabels(ctx context.Context, filter fleet.TeamFilter,
|
|||||||
return matches, nil
|
return matches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Datastore) LabelIDsByName(ctx context.Context, labels []string) ([]uint, error) {
|
func (ds *Datastore) LabelIDsByName(ctx context.Context, names []string) (map[string]uint, error) {
|
||||||
if len(labels) == 0 {
|
if len(names) == 0 {
|
||||||
return []uint{}, nil
|
return map[string]uint{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlStatement := `
|
sqlStatement := `
|
||||||
SELECT id FROM labels
|
SELECT id, name FROM labels
|
||||||
WHERE name IN (?)
|
WHERE name IN (?)
|
||||||
`
|
`
|
||||||
|
|
||||||
sql, args, err := sqlx.In(sqlStatement, labels)
|
sql, args, err := sqlx.In(sqlStatement, names)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, "building query to get label IDs")
|
return nil, ctxerr.Wrap(ctx, err, "building query to get label ids by name")
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []uint
|
var labels []fleet.Label
|
||||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labelIDs, sql, args...); err != nil {
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labels, sql, args...); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, "get label IDs")
|
return nil, ctxerr.Wrap(ctx, err, "get label ids by name")
|
||||||
}
|
}
|
||||||
|
|
||||||
return labelIDs, nil
|
result := make(map[string]uint, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
result[label.Name] = label.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncBatchInsertLabelMembership inserts into the label_membership table the
|
// AsyncBatchInsertLabelMembership inserts into the label_membership table the
|
||||||
|
@ -3,7 +3,6 @@ package mysql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -747,8 +746,7 @@ func testLabelsIDsByName(t *testing.T, ds *Datastore) {
|
|||||||
|
|
||||||
labels, err := ds.LabelIDsByName(context.Background(), []string{"foo", "bar", "bing"})
|
labels, err := ds.LabelIDsByName(context.Background(), []string{"foo", "bar", "bing"})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
sort.Slice(labels, func(i, j int) bool { return labels[i] < labels[j] })
|
assert.Equal(t, map[string]uint{"foo": 1, "bar": 2, "bing": 3}, labels)
|
||||||
assert.Equal(t, []uint{1, 2, 3}, labels)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLabelsSave(t *testing.T, db *Datastore) {
|
func testLabelsSave(t *testing.T, db *Datastore) {
|
||||||
|
@ -188,9 +188,73 @@ FROM (
|
|||||||
profs = profs[:len(profs)-1]
|
profs = profs[:len(profs)-1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load the labels associated with those profiles
|
||||||
|
var winProfUUIDs, macProfUUIDs []string
|
||||||
|
for _, prof := range profs {
|
||||||
|
if prof.Platform == "windows" {
|
||||||
|
winProfUUIDs = append(winProfUUIDs, prof.ProfileUUID)
|
||||||
|
} else {
|
||||||
|
macProfUUIDs = append(macProfUUIDs, prof.ProfileUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels, err := ds.listProfileLabelsForProfiles(ctx, winProfUUIDs, macProfUUIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// match the labels with their profiles
|
||||||
|
profMap := make(map[string]*fleet.MDMConfigProfilePayload, len(profs))
|
||||||
|
for _, prof := range profs {
|
||||||
|
profMap[prof.ProfileUUID] = prof
|
||||||
|
}
|
||||||
|
for _, label := range labels {
|
||||||
|
if prof, ok := profMap[label.ProfileUUID]; ok {
|
||||||
|
prof.Labels = append(prof.Labels, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return profs, metaData, nil
|
return profs, metaData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ds *Datastore) listProfileLabelsForProfiles(ctx context.Context, winProfUUIDs, macProfUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) {
|
||||||
|
// load the labels associated with those profiles
|
||||||
|
const labelsStmt = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid,
|
||||||
|
label_name,
|
||||||
|
COALESCE(label_id, 0) as label_id,
|
||||||
|
IF(label_id IS NULL, 1, 0) as broken
|
||||||
|
FROM
|
||||||
|
mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE
|
||||||
|
mcpl.apple_profile_uuid IN (?) OR
|
||||||
|
mcpl.windows_profile_uuid IN (?)
|
||||||
|
ORDER BY
|
||||||
|
profile_uuid, label_name
|
||||||
|
`
|
||||||
|
|
||||||
|
// ensure there's at least one (non-matching) value in the slice so the IN
|
||||||
|
// clause is valid
|
||||||
|
if len(winProfUUIDs) == 0 {
|
||||||
|
winProfUUIDs = []string{"-"}
|
||||||
|
}
|
||||||
|
if len(macProfUUIDs) == 0 {
|
||||||
|
macProfUUIDs = []string{"-"}
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "sqlx.In to list labels for profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
var labels []fleet.ConfigurationProfileLabel
|
||||||
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labels, stmt, args...); err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "select profiles labels")
|
||||||
|
}
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Note that team ID 0 is used for profiles that apply to hosts in no team
|
// Note that team ID 0 is used for profiles that apply to hosts in no team
|
||||||
// (i.e. pass 0 in that case as part of the teamIDs slice). Only one of the
|
// (i.e. pass 0 in that case as part of the teamIDs slice). Only one of the
|
||||||
// slice arguments can have values.
|
// slice arguments can have values.
|
||||||
@ -512,23 +576,58 @@ func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Conte
|
|||||||
|
|
||||||
switch host.Platform {
|
switch host.Platform {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID)
|
return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID, host.ID)
|
||||||
case "windows":
|
case "windows":
|
||||||
return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID)
|
return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID, host.ID)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported platform: %s", host.Platform)
|
return nil, fmt.Errorf("unsupported platform: %s", host.Platform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx context.Context, teamID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
|
func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx context.Context, teamID, hostID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
|
||||||
stmt := `
|
stmt := `
|
||||||
SELECT name, syncml as raw_profile, updated_at as earliest_install_date
|
SELECT
|
||||||
FROM mdm_windows_configuration_profiles mwcp
|
name,
|
||||||
WHERE mwcp.team_id = ?
|
syncml AS raw_profile,
|
||||||
|
mwcp.updated_at AS earliest_install_date,
|
||||||
|
0 AS count_profile_labels,
|
||||||
|
0 AS count_host_labels
|
||||||
|
FROM
|
||||||
|
mdm_windows_configuration_profiles mwcp
|
||||||
|
WHERE
|
||||||
|
mwcp.team_id = ?
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE
|
||||||
|
mcpl.apple_profile_uuid = mwcp.profile_uuid)
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
syncml AS raw_profile,
|
||||||
|
mwcp.updated_at AS earliest_install_date,
|
||||||
|
COUNT(*) AS count_profile_labels,
|
||||||
|
COUNT(lm.label_id) AS count_host_labels
|
||||||
|
FROM
|
||||||
|
mdm_windows_configuration_profiles mwcp
|
||||||
|
JOIN mdm_configuration_profile_labels mcpl ON mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||||
|
LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id
|
||||||
|
AND lm.host_id = ?
|
||||||
|
WHERE
|
||||||
|
mwcp.team_id = ?
|
||||||
|
GROUP BY
|
||||||
|
name
|
||||||
|
HAVING
|
||||||
|
count_profile_labels > 0
|
||||||
|
AND count_host_labels = count_profile_labels
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
var profiles []*fleet.ExpectedMDMProfile
|
var profiles []*fleet.ExpectedMDMProfile
|
||||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID)
|
// Note: teamID provided twice
|
||||||
|
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID, hostID, teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -541,10 +640,12 @@ func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx contex
|
|||||||
return byName, nil
|
return byName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Datastore) getHostMDMAppleProfilesExpectedForVerification(ctx context.Context, teamID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
|
func (ds *Datastore) getHostMDMAppleProfilesExpectedForVerification(ctx context.Context, teamID, hostID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
|
||||||
stmt := `
|
stmt := `
|
||||||
SELECT
|
SELECT
|
||||||
identifier,
|
macp.identifier AS identifier,
|
||||||
|
0 AS count_profile_labels,
|
||||||
|
0 AS count_host_labels,
|
||||||
earliest_install_date
|
earliest_install_date
|
||||||
FROM
|
FROM
|
||||||
mdm_apple_configuration_profiles macp
|
mdm_apple_configuration_profiles macp
|
||||||
@ -555,13 +656,48 @@ FROM
|
|||||||
FROM
|
FROM
|
||||||
mdm_apple_configuration_profiles
|
mdm_apple_configuration_profiles
|
||||||
GROUP BY
|
GROUP BY
|
||||||
checksum) cs
|
checksum) cs ON macp.checksum = cs.checksum
|
||||||
ON macp.checksum = cs.checksum
|
|
||||||
WHERE
|
WHERE
|
||||||
macp.team_id = ?`
|
macp.team_id = ?
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE
|
||||||
|
mcpl.apple_profile_uuid = macp.profile_uuid)
|
||||||
|
UNION
|
||||||
|
-- label-based profiles where the host is a member of all the labels
|
||||||
|
SELECT
|
||||||
|
macp.identifier AS identifier,
|
||||||
|
COUNT(*) AS count_profile_labels,
|
||||||
|
COUNT(lm.label_id) AS count_host_labels,
|
||||||
|
earliest_install_date
|
||||||
|
FROM
|
||||||
|
mdm_apple_configuration_profiles macp
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
checksum,
|
||||||
|
min(updated_at) AS earliest_install_date
|
||||||
|
FROM
|
||||||
|
mdm_apple_configuration_profiles
|
||||||
|
GROUP BY
|
||||||
|
checksum) cs ON macp.checksum = cs.checksum
|
||||||
|
JOIN mdm_configuration_profile_labels mcpl ON mcpl.apple_profile_uuid = macp.profile_uuid
|
||||||
|
LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id
|
||||||
|
AND lm.host_id = ?
|
||||||
|
WHERE
|
||||||
|
macp.team_id = ?
|
||||||
|
GROUP BY
|
||||||
|
identifier
|
||||||
|
HAVING
|
||||||
|
count_profile_labels > 0
|
||||||
|
AND count_host_labels = count_profile_labels
|
||||||
|
`
|
||||||
|
|
||||||
var rows []*fleet.ExpectedMDMProfile
|
var rows []*fleet.ExpectedMDMProfile
|
||||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID); err != nil {
|
// Note: teamID provided twice
|
||||||
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID, hostID, teamID); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
|
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,3 +785,96 @@ WHERE
|
|||||||
|
|
||||||
return dest, nil
|
return dest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func batchSetProfileLabelAssociationsDB(
|
||||||
|
ctx context.Context,
|
||||||
|
tx sqlx.ExtContext,
|
||||||
|
profileLabels []fleet.ConfigurationProfileLabel,
|
||||||
|
platform string,
|
||||||
|
) error {
|
||||||
|
if len(profileLabels) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var platformPrefix string
|
||||||
|
switch platform {
|
||||||
|
case "darwin":
|
||||||
|
// map "darwin" to "apple" to be consistent with other
|
||||||
|
// "platform-agnostic" datastore methods. We initially used "darwin"
|
||||||
|
// because that's what hosts use (as the data is reported by osquery)
|
||||||
|
// and sometimes we want to dynamically select a table based on host
|
||||||
|
// data.
|
||||||
|
platformPrefix = "apple"
|
||||||
|
case "windows":
|
||||||
|
platformPrefix = "windows"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported platform %s", platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete any profile+label tuple that is NOT in the list of provided tuples
|
||||||
|
// but are associated with the provided profiles (so we don't delete
|
||||||
|
// unrelated profile+label tuples)
|
||||||
|
deleteStmt := `
|
||||||
|
DELETE FROM mdm_configuration_profile_labels
|
||||||
|
WHERE (%s_profile_uuid, label_id) NOT IN (%s) AND
|
||||||
|
%s_profile_uuid IN (?)
|
||||||
|
`
|
||||||
|
|
||||||
|
upsertStmt := `
|
||||||
|
INSERT INTO mdm_configuration_profile_labels
|
||||||
|
(%s_profile_uuid, label_id, label_name)
|
||||||
|
VALUES
|
||||||
|
%s
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
label_id = VALUES(label_id)
|
||||||
|
`
|
||||||
|
|
||||||
|
var (
|
||||||
|
insertBuilder strings.Builder
|
||||||
|
deleteBuilder strings.Builder
|
||||||
|
insertParams []any
|
||||||
|
deleteParams []any
|
||||||
|
|
||||||
|
setProfileUUIDs = make(map[string]struct{})
|
||||||
|
)
|
||||||
|
for i, pl := range profileLabels {
|
||||||
|
if i > 0 {
|
||||||
|
insertBuilder.WriteString(",")
|
||||||
|
deleteBuilder.WriteString(",")
|
||||||
|
}
|
||||||
|
insertBuilder.WriteString("(?, ?, ?)")
|
||||||
|
deleteBuilder.WriteString("(?, ?)")
|
||||||
|
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName)
|
||||||
|
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||||
|
|
||||||
|
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := tx.ExecContext(ctx, fmt.Sprintf(upsertStmt, platformPrefix, insertBuilder.String()), insertParams...)
|
||||||
|
if err != nil {
|
||||||
|
if isChildForeignKeyError(err) {
|
||||||
|
// one of the provided labels doesn't exist
|
||||||
|
return foreignKey("mdm_configuration_profile_labels", fmt.Sprintf("(profile, label)=(%v)", insertParams))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctxerr.Wrap(ctx, err, "setting label associations for profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteStmt = fmt.Sprintf(deleteStmt, platformPrefix, deleteBuilder.String(), platformPrefix)
|
||||||
|
|
||||||
|
profUUIDs := make([]string, 0, len(setProfileUUIDs))
|
||||||
|
for k := range setProfileUUIDs {
|
||||||
|
profUUIDs = append(profUUIDs, k)
|
||||||
|
}
|
||||||
|
deleteArgs := append(deleteParams, profUUIDs)
|
||||||
|
|
||||||
|
deleteStmt, args, err := sqlx.In(deleteStmt, deleteArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "sqlx.In delete labels for profiles")
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, deleteStmt, args...); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "deleting labels for profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -701,6 +701,15 @@ WHERE
|
|||||||
return nil, ctxerr.Wrap(ctx, err, "get mdm windows config profile")
|
return nil, ctxerr.Wrap(ctx, err, "get mdm windows config profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labels, err := ds.listProfileLabelsForProfiles(ctx, []string{res.ProfileUUID}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(labels) > 0 {
|
||||||
|
// ensure we leave Labels nil if there are none
|
||||||
|
res.Labels = labels
|
||||||
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1074,8 +1083,63 @@ GROUP BY
|
|||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const windowsMDMProfilesDesiredStateQuery = `
|
||||||
|
-- non label-based profiles
|
||||||
|
SELECT
|
||||||
|
mwcp.profile_uuid,
|
||||||
|
mwcp.name,
|
||||||
|
h.uuid as host_uuid,
|
||||||
|
0 as count_profile_labels,
|
||||||
|
0 as count_host_labels
|
||||||
|
FROM
|
||||||
|
mdm_windows_configuration_profiles mwcp
|
||||||
|
JOIN hosts h
|
||||||
|
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||||
|
JOIN mdm_windows_enrollments mwe
|
||||||
|
ON mwe.host_uuid = h.uuid
|
||||||
|
WHERE
|
||||||
|
h.platform = 'windows' AND
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||||
|
) AND
|
||||||
|
( %s )
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- label-based profiles
|
||||||
|
SELECT
|
||||||
|
mwcp.profile_uuid,
|
||||||
|
mwcp.name,
|
||||||
|
h.uuid as host_uuid,
|
||||||
|
COUNT(*) as count_profile_labels,
|
||||||
|
COUNT(lm.label_id) as count_host_labels
|
||||||
|
FROM
|
||||||
|
mdm_windows_configuration_profiles mwcp
|
||||||
|
JOIN hosts h
|
||||||
|
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||||
|
JOIN mdm_windows_enrollments mwe
|
||||||
|
ON mwe.host_uuid = h.uuid
|
||||||
|
JOIN mdm_configuration_profile_labels mcpl
|
||||||
|
ON mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||||
|
LEFT OUTER JOIN label_membership lm
|
||||||
|
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||||
|
WHERE
|
||||||
|
h.platform = 'windows' AND
|
||||||
|
( %s )
|
||||||
|
GROUP BY
|
||||||
|
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||||
|
HAVING
|
||||||
|
count_profile_labels > 0 AND count_host_labels = count_profile_labels
|
||||||
|
`
|
||||||
|
|
||||||
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||||
var result []*fleet.MDMWindowsProfilePayload
|
var result []*fleet.MDMWindowsProfilePayload
|
||||||
|
// TODO(mna): why is this in a transaction/reading from the primary, but not
|
||||||
|
// Apple's implementation? I see that the called private method is sometimes
|
||||||
|
// called inside a transaction, but when called from here it could (should?)
|
||||||
|
// be without and use the reader replica?
|
||||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
var err error
|
var err error
|
||||||
result, err = listMDMWindowsProfilesToInstallDB(ctx, tx, nil)
|
result, err = listMDMWindowsProfilesToInstallDB(ctx, tx, nil)
|
||||||
@ -1091,9 +1155,10 @@ func listMDMWindowsProfilesToInstallDB(
|
|||||||
) ([]*fleet.MDMWindowsProfilePayload, error) {
|
) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||||
// The query below is a set difference between:
|
// The query below is a set difference between:
|
||||||
//
|
//
|
||||||
// - Set A (ds), the desired state, can be obtained from a JOIN between
|
// - Set A (ds), the "desired state", can be obtained from a JOIN between
|
||||||
// mdm_windows_configuration_profiles and hosts.
|
// mdm_windows_configuration_profiles and hosts.
|
||||||
// - Set B, the current state given by host_mdm_windows_profiles.
|
//
|
||||||
|
// - Set B, the "current state" given by host_mdm_windows_profiles.
|
||||||
//
|
//
|
||||||
// A - B gives us the profiles that need to be installed:
|
// A - B gives us the profiles that need to be installed:
|
||||||
//
|
//
|
||||||
@ -1105,26 +1170,26 @@ func listMDMWindowsProfilesToInstallDB(
|
|||||||
// to independent verification by Fleet (verifying), or has reached a terminal
|
// to independent verification by Fleet (verifying), or has reached a terminal
|
||||||
// state (failed or verified). If the profile's content is edited, all relevant hosts will
|
// state (failed or verified). If the profile's content is edited, all relevant hosts will
|
||||||
// be marked as status NULL so that it gets re-installed.
|
// be marked as status NULL so that it gets re-installed.
|
||||||
query := `
|
//
|
||||||
SELECT
|
// Note that for label-based profiles, only fully-satisfied profiles are
|
||||||
ds.profile_uuid,
|
// considered for installation. This means that a broken label-based profile,
|
||||||
ds.host_uuid,
|
// where one of the labels does not exist anymore, will not be considered for
|
||||||
ds.name as profile_name
|
// installation.
|
||||||
FROM (
|
|
||||||
SELECT mwcp.profile_uuid, mwcp.name, h.uuid as host_uuid
|
query := fmt.Sprintf(`
|
||||||
FROM mdm_windows_configuration_profiles mwcp
|
SELECT
|
||||||
JOIN hosts h ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
ds.profile_uuid,
|
||||||
JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid
|
ds.host_uuid,
|
||||||
WHERE h.platform = 'windows' AND (%s)
|
ds.name as profile_name
|
||||||
) as ds
|
FROM ( %s ) as ds
|
||||||
LEFT JOIN host_mdm_windows_profiles hmwp
|
LEFT JOIN host_mdm_windows_profiles hmwp
|
||||||
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.host_uuid
|
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.host_uuid
|
||||||
WHERE
|
WHERE
|
||||||
-- profiles in A but not in B
|
-- profiles in A but not in B
|
||||||
( hmwp.profile_uuid IS NULL AND hmwp.host_uuid IS NULL ) OR
|
( hmwp.profile_uuid IS NULL AND hmwp.host_uuid IS NULL ) OR
|
||||||
-- profiles in A and B with operation type "install" and NULL status
|
-- profiles in A and B with operation type "install" and NULL status
|
||||||
( hmwp.host_uuid IS NOT NULL AND hmwp.operation_type = ? AND hmwp.status IS NULL )
|
( hmwp.host_uuid IS NOT NULL AND hmwp.operation_type = ? AND hmwp.status IS NULL )
|
||||||
`
|
`, windowsMDMProfilesDesiredStateQuery)
|
||||||
|
|
||||||
hostFilter := "TRUE"
|
hostFilter := "TRUE"
|
||||||
if len(hostUUIDs) > 0 {
|
if len(hostUUIDs) > 0 {
|
||||||
@ -1133,9 +1198,9 @@ func listMDMWindowsProfilesToInstallDB(
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
args := []any{fleet.MDMOperationTypeInstall}
|
args := []any{fleet.MDMOperationTypeInstall}
|
||||||
query = fmt.Sprintf(query, hostFilter)
|
query = fmt.Sprintf(query, hostFilter, hostFilter)
|
||||||
if len(hostUUIDs) > 0 {
|
if len(hostUUIDs) > 0 {
|
||||||
query, args, err = sqlx.In(query, hostUUIDs, args)
|
query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In")
|
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In")
|
||||||
}
|
}
|
||||||
@ -1148,6 +1213,7 @@ func listMDMWindowsProfilesToInstallDB(
|
|||||||
|
|
||||||
func (ds *Datastore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
func (ds *Datastore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||||
var result []*fleet.MDMWindowsProfilePayload
|
var result []*fleet.MDMWindowsProfilePayload
|
||||||
|
// TODO(mna): same question here
|
||||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
var err error
|
var err error
|
||||||
result, err = listMDMWindowsProfilesToRemoveDB(ctx, tx, nil)
|
result, err = listMDMWindowsProfilesToRemoveDB(ctx, tx, nil)
|
||||||
@ -1171,39 +1237,51 @@ func listMDMWindowsProfilesToRemoveDB(
|
|||||||
// B - A gives us the profiles that need to be removed
|
// B - A gives us the profiles that need to be removed
|
||||||
//
|
//
|
||||||
// Any other case are profiles that are in both B and A, and as such are
|
// Any other case are profiles that are in both B and A, and as such are
|
||||||
// processed by the ListMDMWindowsProfilesToInstall method (since they are in
|
// processed by the ListMDMWindowsProfilesToInstall method (since they are
|
||||||
// both, their desired state is necessarily to be installed).
|
// in both, their desired state is necessarily to be installed).
|
||||||
query := `
|
//
|
||||||
SELECT
|
// Note that for label-based profiles, only those that are fully-sastisfied
|
||||||
hmwp.profile_uuid,
|
// by the host are considered for install (are part of the desired state used
|
||||||
hmwp.host_uuid,
|
// to compute the ones to remove). However, as a special case, a broken
|
||||||
hmwp.operation_type,
|
// label-based profile will NOT be removed from a host where it was
|
||||||
COALESCE(hmwp.detail, '') as detail,
|
// previously installed. However, if a host used to satisfy a label-based
|
||||||
hmwp.status,
|
// profile but no longer does (and that label-based profile is not "broken"),
|
||||||
hmwp.command_uuid
|
// the profile will be removed from the host.
|
||||||
FROM (
|
|
||||||
SELECT h.uuid, mwcp.profile_uuid
|
|
||||||
FROM mdm_windows_configuration_profiles mwcp
|
|
||||||
JOIN hosts h ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
|
||||||
JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid
|
|
||||||
WHERE h.platform = 'windows'
|
|
||||||
) as ds
|
|
||||||
RIGHT JOIN host_mdm_windows_profiles hmwp
|
|
||||||
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.uuid
|
|
||||||
-- profiles that are in B but not in A
|
|
||||||
WHERE ds.profile_uuid IS NULL
|
|
||||||
AND ds.uuid IS NULL
|
|
||||||
AND (%s)
|
|
||||||
`
|
|
||||||
|
|
||||||
hostFilter := "TRUE"
|
hostFilter := "TRUE"
|
||||||
if len(hostUUIDs) > 0 {
|
if len(hostUUIDs) > 0 {
|
||||||
hostFilter = "hmwp.host_uuid IN (?)"
|
hostFilter = "hmwp.host_uuid IN (?)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
hmwp.profile_uuid,
|
||||||
|
hmwp.host_uuid,
|
||||||
|
hmwp.operation_type,
|
||||||
|
COALESCE(hmwp.detail, '') as detail,
|
||||||
|
hmwp.status,
|
||||||
|
hmwp.command_uuid
|
||||||
|
FROM ( %s ) as ds
|
||||||
|
RIGHT JOIN host_mdm_windows_profiles hmwp
|
||||||
|
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.host_uuid
|
||||||
|
WHERE
|
||||||
|
-- profiles that are in B but not in A
|
||||||
|
ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND
|
||||||
|
-- TODO(mna): why don't we have the same exception for "remove" operations as for Apple
|
||||||
|
|
||||||
|
-- except "would be removed" profiles if they are a broken label-based profile
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mdm_configuration_profile_labels mcpl
|
||||||
|
WHERE
|
||||||
|
mcpl.windows_profile_uuid = hmwp.profile_uuid AND
|
||||||
|
mcpl.label_id IS NULL
|
||||||
|
) AND
|
||||||
|
(%s)
|
||||||
|
`, fmt.Sprintf(windowsMDMProfilesDesiredStateQuery, "TRUE", "TRUE"), hostFilter)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var args []any
|
var args []any
|
||||||
query = fmt.Sprintf(query, hostFilter)
|
|
||||||
if len(hostUUIDs) > 0 {
|
if len(hostUUIDs) > 0 {
|
||||||
query, args, err = sqlx.In(query, hostUUIDs)
|
query, args, err = sqlx.In(query, hostUUIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1379,7 +1457,7 @@ func (ds *Datastore) bulkDeleteMDMWindowsHostsConfigProfilesDB(
|
|||||||
|
|
||||||
func (ds *Datastore) NewMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile) (*fleet.MDMWindowsConfigProfile, error) {
|
func (ds *Datastore) NewMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile) (*fleet.MDMWindowsConfigProfile, error) {
|
||||||
profileUUID := "w" + uuid.New().String()
|
profileUUID := "w" + uuid.New().String()
|
||||||
stmt := `
|
insertProfileStmt := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml)
|
mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml)
|
||||||
(SELECT ?, ?, ?, ? FROM DUAL WHERE
|
(SELECT ?, ?, ?, ? FROM DUAL WHERE
|
||||||
@ -1393,27 +1471,41 @@ INSERT INTO
|
|||||||
teamID = *cp.TeamID
|
teamID = *cp.TeamID
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID)
|
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
if err != nil {
|
res, err := tx.ExecContext(ctx, insertProfileStmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID)
|
||||||
switch {
|
if err != nil {
|
||||||
case isDuplicate(err):
|
switch {
|
||||||
return nil, &existsError{
|
case isDuplicate(err):
|
||||||
|
return &existsError{
|
||||||
|
ResourceType: "MDMWindowsConfigProfile.Name",
|
||||||
|
Identifier: cp.Name,
|
||||||
|
TeamID: cp.TeamID,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ctxerr.Wrap(ctx, err, "creating new windows mdm config profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aff, _ := res.RowsAffected()
|
||||||
|
if aff == 0 {
|
||||||
|
return &existsError{
|
||||||
ResourceType: "MDMWindowsConfigProfile.Name",
|
ResourceType: "MDMWindowsConfigProfile.Name",
|
||||||
Identifier: cp.Name,
|
Identifier: cp.Name,
|
||||||
TeamID: cp.TeamID,
|
TeamID: cp.TeamID,
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return nil, ctxerr.Wrap(ctx, err, "creating new windows mdm config profile")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
aff, _ := res.RowsAffected()
|
for i := range cp.Labels {
|
||||||
if aff == 0 {
|
cp.Labels[i].ProfileUUID = profileUUID
|
||||||
return nil, &existsError{
|
|
||||||
ResourceType: "MDMWindowsConfigProfile.Name",
|
|
||||||
Identifier: cp.Name,
|
|
||||||
TeamID: cp.TeamID,
|
|
||||||
}
|
}
|
||||||
|
if err := batchSetProfileLabelAssociationsDB(ctx, tx, cp.Labels, "windows"); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &fleet.MDMWindowsConfigProfile{
|
return &fleet.MDMWindowsConfigProfile{
|
||||||
@ -1478,6 +1570,7 @@ func (ds *Datastore) batchSetMDMWindowsProfilesDB(
|
|||||||
const loadExistingProfiles = `
|
const loadExistingProfiles = `
|
||||||
SELECT
|
SELECT
|
||||||
name,
|
name,
|
||||||
|
profile_uuid,
|
||||||
syncml
|
syncml
|
||||||
FROM
|
FROM
|
||||||
mdm_windows_configuration_profiles
|
mdm_windows_configuration_profiles
|
||||||
@ -1579,6 +1672,41 @@ ON DUPLICATE KEY UPDATE
|
|||||||
return ctxerr.Wrapf(ctx, err, "insert new/edited profile with name %q", p.Name)
|
return ctxerr.Wrapf(ctx, err, "insert new/edited profile with name %q", p.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build a list of labels so the associations can be batch-set all at once
|
||||||
|
// TODO: with minor changes this chunk of code could be shared
|
||||||
|
// between macOS and Windows, but at the time of this
|
||||||
|
// implementation we're under tight time constraints.
|
||||||
|
incomingLabels := []fleet.ConfigurationProfileLabel{}
|
||||||
|
if len(incomingNames) > 0 {
|
||||||
|
var newlyInsertedProfs []*fleet.MDMWindowsConfigProfile
|
||||||
|
// load current profiles (again) that match the incoming profiles by name to grab their uuids
|
||||||
|
stmt, args, err := sqlx.In(loadExistingProfiles, profTeamID, incomingNames)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "build query to load newly inserted profiles")
|
||||||
|
}
|
||||||
|
if err := sqlx.SelectContext(ctx, tx, &newlyInsertedProfs, stmt, args...); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "load newly inserted profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newlyInsertedProf := range newlyInsertedProfs {
|
||||||
|
incomingProf, ok := incomingProfs[newlyInsertedProf.Name]
|
||||||
|
if !ok {
|
||||||
|
return ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range incomingProf.Labels {
|
||||||
|
label.ProfileUUID = newlyInsertedProf.ProfileUUID
|
||||||
|
incomingLabels = append(incomingLabels, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert/delete the label associations
|
||||||
|
if err := batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels, "windows"); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1781,11 +1781,61 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.ErrorAs(t, err, &existsErr)
|
require.ErrorAs(t, err, &existsErr)
|
||||||
|
|
||||||
|
// create a profile with labels that don't exist
|
||||||
|
_, err = ds.NewMDMWindowsConfigProfile(
|
||||||
|
ctx,
|
||||||
|
fleet.MDMWindowsConfigProfile{
|
||||||
|
Name: "fake-labels",
|
||||||
|
TeamID: nil,
|
||||||
|
SyncML: []byte("<Replace></Replace>"),
|
||||||
|
Labels: []fleet.ConfigurationProfileLabel{{LabelName: "foo", LabelID: 1}},
|
||||||
|
})
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.True(t, fleet.IsForeignKey(err))
|
||||||
|
|
||||||
|
label := &fleet.Label{
|
||||||
|
Name: "my label",
|
||||||
|
Description: "a label",
|
||||||
|
Query: "select 1 from processes;",
|
||||||
|
}
|
||||||
|
label, err = ds.NewLabel(ctx, label)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create a profile with a label that exists
|
||||||
|
profWithLabel, err := ds.NewMDMWindowsConfigProfile(
|
||||||
|
ctx,
|
||||||
|
fleet.MDMWindowsConfigProfile{
|
||||||
|
Name: "with-labels",
|
||||||
|
TeamID: nil,
|
||||||
|
SyncML: []byte("<Replace></Replace>"),
|
||||||
|
Labels: []fleet.ConfigurationProfileLabel{{LabelName: label.Name, LabelID: label.ID}},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, profWithLabel.ProfileUUID)
|
||||||
|
|
||||||
|
// get that profile with label
|
||||||
|
prof, err := ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prof.Labels, 1)
|
||||||
|
require.Equal(t, label.Name, prof.Labels[0].LabelName)
|
||||||
|
require.Equal(t, label.ID, prof.Labels[0].LabelID)
|
||||||
|
require.False(t, prof.Labels[0].Broken)
|
||||||
|
|
||||||
|
// break that profile by deleting the label
|
||||||
|
require.NoError(t, ds.DeleteLabel(ctx, label.Name))
|
||||||
|
|
||||||
|
prof, err = ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prof.Labels, 1)
|
||||||
|
require.Equal(t, label.Name, prof.Labels[0].LabelName)
|
||||||
|
require.Zero(t, prof.Labels[0].LabelID)
|
||||||
|
require.True(t, prof.Labels[0].Broken)
|
||||||
|
|
||||||
_, err = ds.GetMDMWindowsConfigProfile(ctx, "not-valid")
|
_, err = ds.GetMDMWindowsConfigProfile(ctx, "not-valid")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.True(t, fleet.IsNotFound(err))
|
require.True(t, fleet.IsNotFound(err))
|
||||||
|
|
||||||
prof, err := ds.GetMDMWindowsConfigProfile(ctx, profA.ProfileUUID)
|
prof, err = ds.GetMDMWindowsConfigProfile(ctx, profA.ProfileUUID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, profA.ProfileUUID, prof.ProfileUUID)
|
require.Equal(t, profA.ProfileUUID, prof.ProfileUUID)
|
||||||
require.NotNil(t, prof.TeamID)
|
require.NotNil(t, prof.TeamID)
|
||||||
@ -1794,6 +1844,7 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
|||||||
require.Equal(t, "<Replace></Replace>", string(prof.SyncML))
|
require.Equal(t, "<Replace></Replace>", string(prof.SyncML))
|
||||||
require.NotZero(t, prof.CreatedAt)
|
require.NotZero(t, prof.CreatedAt)
|
||||||
require.NotZero(t, prof.UpdatedAt)
|
require.NotZero(t, prof.UpdatedAt)
|
||||||
|
require.Nil(t, prof.Labels)
|
||||||
|
|
||||||
err = ds.DeleteMDMWindowsConfigProfile(ctx, "not-valid")
|
err = ds.DeleteMDMWindowsConfigProfile(ctx, "not-valid")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@ -1976,8 +2027,8 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||||||
applyAndExpect(nil, ptr.Uint(1), nil)
|
applyAndExpect(nil, ptr.Uint(1), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowsConfigProfileForTest(t *testing.T, name, locURI string) *fleet.MDMWindowsConfigProfile {
|
func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*fleet.Label) *fleet.MDMWindowsConfigProfile {
|
||||||
return &fleet.MDMWindowsConfigProfile{
|
prof := &fleet.MDMWindowsConfigProfile{
|
||||||
Name: name,
|
Name: name,
|
||||||
SyncML: []byte(fmt.Sprintf(`
|
SyncML: []byte(fmt.Sprintf(`
|
||||||
<Replace>
|
<Replace>
|
||||||
@ -1989,4 +2040,10 @@ func windowsConfigProfileForTest(t *testing.T, name, locURI string) *fleet.MDMWi
|
|||||||
</Replace>
|
</Replace>
|
||||||
`, locURI)),
|
`, locURI)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, lbl := range labels {
|
||||||
|
prof.Labels = append(prof.Labels, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
return prof
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
MigrationClient.AddMigration(Up_20240126020642, Down_20240126020642)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Up_20240126020642(tx *sql.Tx) error {
|
||||||
|
createStmt := `
|
||||||
|
CREATE TABLE IF NOT EXISTS mdm_configuration_profile_labels (
|
||||||
|
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|
||||||
|
-- using distinct fields for the profile uuid so that proper foreign keys
|
||||||
|
-- can be created to the apple and windows tables.
|
||||||
|
apple_profile_uuid VARCHAR(37) NULL,
|
||||||
|
windows_profile_uuid VARCHAR(37) NULL,
|
||||||
|
|
||||||
|
-- label name is stored here because we need to list the labels in the UI
|
||||||
|
-- even if it has been deleted from the labels table.
|
||||||
|
label_name VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- label id is nullable in case it gets deleted from the labels table.
|
||||||
|
-- A row in this table with label_id = null indicates the "broken" state
|
||||||
|
-- in the UI.
|
||||||
|
label_id INT(10) UNSIGNED NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- cannot have a single unique key with apple+windows+label name because
|
||||||
|
-- NULLs are not considered equal in unique keys (so "W1+null+L1" is not
|
||||||
|
-- a duplicate of itself). Using two distinct unique keys instead, and there's
|
||||||
|
-- a check constraint to ensure that only one of the apple or windows
|
||||||
|
-- profile uuid can be set.
|
||||||
|
UNIQUE KEY idx_mdm_configuration_profile_labels_apple_label_name (apple_profile_uuid, label_name),
|
||||||
|
UNIQUE KEY idx_mdm_configuration_profile_labels_windows_label_name (windows_profile_uuid, label_name),
|
||||||
|
|
||||||
|
FOREIGN KEY (apple_profile_uuid) REFERENCES mdm_apple_configuration_profiles(profile_uuid) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (windows_profile_uuid) REFERENCES mdm_windows_configuration_profiles(profile_uuid) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- TODO(mna): CHECK constraint is parsed but ignored on mysql 5.7, will have to do without.
|
||||||
|
|
||||||
|
-- exactly one of apple or windows profile uuid must be set
|
||||||
|
CONSTRAINT ck_mdm_configuration_profile_labels_apple_or_windows
|
||||||
|
CHECK (ISNULL(apple_profile_uuid) <> ISNULL(windows_profile_uuid))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`
|
||||||
|
if _, err := tx.Exec(createStmt); err != nil {
|
||||||
|
return errors.Wrap(err, "create mdm_configuration_profile_labels table")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Down_20240126020642(tx *sql.Tx) error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUp_20240126020642(t *testing.T) {
|
||||||
|
db := applyUpToPrev(t)
|
||||||
|
|
||||||
|
// Apply current migration.
|
||||||
|
applyNext(t, db)
|
||||||
|
|
||||||
|
// create some Windows profiles
|
||||||
|
idwA, idwB, idwC := "w"+uuid.New().String(), "w"+uuid.New().String(), "w"+uuid.New().String()
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml) VALUES (?, 0, 'A', '<Replace>A</Replace>')`, idwA)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml) VALUES (?, 1, 'B', '<Replace>B</Replace>')`, idwB)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml) VALUES (?, 0, 'C', '<Replace>C</Replace>')`, idwC)
|
||||||
|
nonExistingWID := "w" + uuid.New().String()
|
||||||
|
|
||||||
|
// create some Apple profiles
|
||||||
|
idaA, idaB, idaC := "a"+uuid.New().String(), "a"+uuid.New().String(), "a"+uuid.New().String()
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum) VALUES (?, 0, 'IA', 'NA', '<plist></plist>', '')`, idaA)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum) VALUES (?, 1, 'IB', 'NB', '<plist></plist>', '')`, idaB)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum) VALUES (?, 0, 'IC', 'NC', '<plist></plist>', '')`, idaC)
|
||||||
|
nonExistingAID := "a" + uuid.New().String()
|
||||||
|
|
||||||
|
// create some labels
|
||||||
|
idlA := execNoErrLastID(t, db, `INSERT INTO labels (name, query) VALUES ('LA', 'select 1')`)
|
||||||
|
idlB := execNoErrLastID(t, db, `INSERT INTO labels (name, query) VALUES ('LB', 'select 1')`)
|
||||||
|
idlC := execNoErrLastID(t, db, `INSERT INTO labels (name, query) VALUES ('LC', 'select 1')`)
|
||||||
|
nonExistingLID := idlC + 1
|
||||||
|
|
||||||
|
// apply labels A and B to Windows profile A
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idwA, "LA", idlA)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idwA, "LB", idlB)
|
||||||
|
|
||||||
|
// apply labels B and C to Windows profile B (team 1)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idwB, "LB", idlB)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idwB, "LC", idlC)
|
||||||
|
|
||||||
|
// apply labels A and C to Apple profile A
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idaA, "LA", idlA)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idaA, "LC", idlC)
|
||||||
|
|
||||||
|
// apply label B to Apple profile B (team 1)
|
||||||
|
execNoErr(t, db, `INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idaB, "LB", idlB)
|
||||||
|
|
||||||
|
// apply label A to non-existing Windows profile
|
||||||
|
_, err := db.Exec(`INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, nonExistingWID, "LA", idlA)
|
||||||
|
require.ErrorContains(t, err, "foreign key constraint fails")
|
||||||
|
|
||||||
|
// apply label A to non-existing Apple profile
|
||||||
|
_, err = db.Exec(`INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, nonExistingAID, "LA", idlA)
|
||||||
|
require.ErrorContains(t, err, "foreign key constraint fails")
|
||||||
|
|
||||||
|
// apply non-existing label to Windows profile A
|
||||||
|
_, err = db.Exec(`INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idwA, "Lnone", nonExistingLID)
|
||||||
|
require.ErrorContains(t, err, "foreign key constraint fails")
|
||||||
|
|
||||||
|
// apply non-existing label to Apple profile A
|
||||||
|
_, err = db.Exec(`INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idaA, "Lnone", nonExistingLID)
|
||||||
|
require.ErrorContains(t, err, "foreign key constraint fails")
|
||||||
|
|
||||||
|
// apply duplicate (label A to Windows profile A)
|
||||||
|
_, err = db.Exec(`INSERT INTO mdm_configuration_profile_labels (windows_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idwA, "LA", idlA)
|
||||||
|
require.ErrorContains(t, err, "Duplicate entry")
|
||||||
|
|
||||||
|
// apply duplicate (label A to Apple profile A)
|
||||||
|
_, err = db.Exec(`INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`, idaA, "LA", idlA)
|
||||||
|
require.ErrorContains(t, err, "Duplicate entry")
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -590,7 +590,7 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
|||||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||||
},
|
},
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.SetSlice([]string{"foo", "bar"}),
|
CustomSettings: optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -613,7 +613,7 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
|
|||||||
MacOSSetupAssistant: optjson.SetString("assistant"),
|
MacOSSetupAssistant: optjson.SetString("assistant"),
|
||||||
},
|
},
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.SetSlice([]string{"foo", "bar"}),
|
CustomSettings: optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}),
|
||||||
},
|
},
|
||||||
}, mdm)
|
}, mdm)
|
||||||
})
|
})
|
||||||
|
@ -298,8 +298,8 @@ type MacOSSettings struct {
|
|||||||
//
|
//
|
||||||
// NOTE: These are only present here for informational purposes.
|
// NOTE: These are only present here for informational purposes.
|
||||||
// (The source of truth for profiles is in MySQL.)
|
// (The source of truth for profiles is in MySQL.)
|
||||||
CustomSettings []string `json:"custom_settings"`
|
CustomSettings []MDMProfileSpec `json:"custom_settings"`
|
||||||
DeprecatedEnableDiskEncryption *bool `json:"enable_disk_encryption,omitempty"`
|
DeprecatedEnableDiskEncryption *bool `json:"enable_disk_encryption,omitempty"`
|
||||||
|
|
||||||
// NOTE: make sure to update the ToMap/FromMap methods when adding/updating fields.
|
// NOTE: make sure to update the ToMap/FromMap methods when adding/updating fields.
|
||||||
}
|
}
|
||||||
@ -324,20 +324,37 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro
|
|||||||
|
|
||||||
vals, ok := v.([]interface{})
|
vals, ok := v.([]interface{})
|
||||||
if v == nil || ok {
|
if v == nil || ok {
|
||||||
strs := make([]string, 0, len(vals))
|
csSpecs := make([]MDMProfileSpec, 0, len(vals))
|
||||||
for _, v := range vals {
|
for _, v := range vals {
|
||||||
str, ok := v.(string)
|
if m, ok := v.(map[string]interface{}); ok {
|
||||||
if !ok {
|
var spec MDMProfileSpec
|
||||||
// error, must be a []string
|
// extract the Path field
|
||||||
|
if path, ok := m["path"].(string); ok {
|
||||||
|
spec.Path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the Labels field (if they are not provided, labels are
|
||||||
|
// cleared for that profile)
|
||||||
|
if labels, ok := m["labels"].([]interface{}); ok {
|
||||||
|
for _, label := range labels {
|
||||||
|
if strLabel, ok := label.(string); ok {
|
||||||
|
spec.Labels = append(spec.Labels, strLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
csSpecs = append(csSpecs, spec)
|
||||||
|
} else if m, ok := v.(string); ok { // for backwards compatibility with the old way to define profiles
|
||||||
|
csSpecs = append(csSpecs, MDMProfileSpec{Path: m})
|
||||||
|
} else {
|
||||||
return nil, &json.UnmarshalTypeError{
|
return nil, &json.UnmarshalTypeError{
|
||||||
Value: fmt.Sprintf("%T", v),
|
Value: fmt.Sprintf("%T", v),
|
||||||
Type: reflect.TypeOf(s.CustomSettings),
|
Type: reflect.TypeOf(s.CustomSettings),
|
||||||
Field: "macos_settings.custom_settings",
|
Field: "macos_settings.custom_settings",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
strs = append(strs, str)
|
|
||||||
}
|
}
|
||||||
s.CustomSettings = strs
|
s.CustomSettings = csSpecs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -553,8 +570,14 @@ func (c *AppConfig) Copy() *AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.MDM.MacOSSettings.CustomSettings != nil {
|
if c.MDM.MacOSSettings.CustomSettings != nil {
|
||||||
clone.MDM.MacOSSettings.CustomSettings = make([]string, len(c.MDM.MacOSSettings.CustomSettings))
|
clone.MDM.MacOSSettings.CustomSettings = make([]MDMProfileSpec, len(c.MDM.MacOSSettings.CustomSettings))
|
||||||
copy(clone.MDM.MacOSSettings.CustomSettings, c.MDM.MacOSSettings.CustomSettings)
|
for i, mps := range c.MDM.MacOSSettings.CustomSettings {
|
||||||
|
clone.MDM.MacOSSettings.CustomSettings[i] = *mps.Copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
|
||||||
|
b := *c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption
|
||||||
|
clone.MDM.MacOSSettings.DeprecatedEnableDiskEncryption = &b
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Scripts.Set {
|
if c.Scripts.Set {
|
||||||
@ -564,8 +587,10 @@ func (c *AppConfig) Copy() *AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.MDM.WindowsSettings.CustomSettings.Set {
|
if c.MDM.WindowsSettings.CustomSettings.Set {
|
||||||
windowsSettings := make([]string, len(c.MDM.WindowsSettings.CustomSettings.Value))
|
windowsSettings := make([]MDMProfileSpec, len(c.MDM.WindowsSettings.CustomSettings.Value))
|
||||||
copy(windowsSettings, c.MDM.WindowsSettings.CustomSettings.Value)
|
for i, mps := range c.MDM.WindowsSettings.CustomSettings.Value {
|
||||||
|
windowsSettings[i] = *mps.Copy()
|
||||||
|
}
|
||||||
clone.MDM.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
|
clone.MDM.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1205,5 +1230,5 @@ func (v *Version) AuthzType() string {
|
|||||||
type WindowsSettings struct {
|
type WindowsSettings struct {
|
||||||
// NOTE: These are only present here for informational purposes.
|
// NOTE: These are only present here for informational purposes.
|
||||||
// (The source of truth for profiles is in MySQL.)
|
// (The source of truth for profiles is in MySQL.)
|
||||||
CustomSettings optjson.Slice[string] `json:"custom_settings"`
|
CustomSettings optjson.Slice[MDMProfileSpec] `json:"custom_settings"`
|
||||||
}
|
}
|
||||||
|
@ -195,9 +195,23 @@ type MDMAppleConfigProfile struct {
|
|||||||
// representation of the configuration profile. It must be XML or PKCS7 parseable.
|
// representation of the configuration profile. It must be XML or PKCS7 parseable.
|
||||||
Mobileconfig mobileconfig.Mobileconfig `db:"mobileconfig" json:"-"`
|
Mobileconfig mobileconfig.Mobileconfig `db:"mobileconfig" json:"-"`
|
||||||
// Checksum is an MD5 hash of the Mobileconfig bytes
|
// Checksum is an MD5 hash of the Mobileconfig bytes
|
||||||
Checksum []byte `db:"checksum" json:"checksum,omitempty"`
|
Checksum []byte `db:"checksum" json:"checksum,omitempty"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
// Labels are the associated labels for this profile
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
Labels []ConfigurationProfileLabel `db:"labels" json:"labels,omitempty"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigurationProfileLabel represents the many-to-many relationship between
|
||||||
|
// profiles and labels.
|
||||||
|
//
|
||||||
|
// NOTE: json representation of the fields is a bit awkward to match the
|
||||||
|
// required API response, as this struct is returned within profile responses.
|
||||||
|
type ConfigurationProfileLabel struct {
|
||||||
|
ProfileUUID string `db:"profile_uuid" json:"-"`
|
||||||
|
LabelName string `db:"label_name" json:"name"`
|
||||||
|
LabelID uint `db:"label_id" json:"id,omitempty"` // omitted if 0 (which is impossible if the label is not broken)
|
||||||
|
Broken bool `db:"broken" json:"broken,omitempty"` // omitted (not rendered to JSON) if false
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMDMAppleConfigProfile(raw []byte, teamID *uint) (*MDMAppleConfigProfile, error) {
|
func NewMDMAppleConfigProfile(raw []byte, teamID *uint) (*MDMAppleConfigProfile, error) {
|
||||||
|
@ -189,8 +189,8 @@ type Datastore interface {
|
|||||||
|
|
||||||
SearchLabels(ctx context.Context, filter TeamFilter, query string, omit ...uint) ([]*Label, error)
|
SearchLabels(ctx context.Context, filter TeamFilter, query string, omit ...uint) ([]*Label, error)
|
||||||
|
|
||||||
// LabelIDsByName Retrieve the IDs associated with the given labels
|
// LabelIDsByName retrieves the IDs associated with the given label names
|
||||||
LabelIDsByName(ctx context.Context, labels []string) ([]uint, error)
|
LabelIDsByName(ctx context.Context, labels []string) (map[string]uint, error)
|
||||||
|
|
||||||
// Methods used for async processing of host label query results.
|
// Methods used for async processing of host label query results.
|
||||||
AsyncBatchInsertLabelMembership(ctx context.Context, batch [][2]uint) error
|
AsyncBatchInsertLabelMembership(ctx context.Context, batch [][2]uint) error
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package fleet
|
package fleet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
@ -117,6 +119,10 @@ type ExpectedMDMProfile struct {
|
|||||||
EarliestInstallDate time.Time `db:"earliest_install_date"`
|
EarliestInstallDate time.Time `db:"earliest_install_date"`
|
||||||
// RawProfile contains the raw profile contents
|
// RawProfile contains the raw profile contents
|
||||||
RawProfile []byte `db:"raw_profile"`
|
RawProfile []byte `db:"raw_profile"`
|
||||||
|
// CountProfileLabels is used to enable queries that filter based on profile <-> label mappings.
|
||||||
|
CountProfileLabels uint `db:"count_profile_labels"`
|
||||||
|
// CountHostLabels is used to enable queries that filter based on profile <-> label mappings.
|
||||||
|
CountHostLabels uint `db:"count_host_labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWithinGracePeriod returns true if the host is within the grace period for the profile.
|
// IsWithinGracePeriod returns true if the host is within the grace period for the profile.
|
||||||
@ -351,14 +357,23 @@ func (m MDMConfigProfileAuthz) AuthzType() string {
|
|||||||
// MDMConfigProfilePayload is the platform-agnostic struct returned by
|
// MDMConfigProfilePayload is the platform-agnostic struct returned by
|
||||||
// endpoints that return MDM configuration profiles (get/list profiles).
|
// endpoints that return MDM configuration profiles (get/list profiles).
|
||||||
type MDMConfigProfilePayload struct {
|
type MDMConfigProfilePayload struct {
|
||||||
ProfileUUID string `json:"profile_uuid" db:"profile_uuid"`
|
ProfileUUID string `json:"profile_uuid" db:"profile_uuid"`
|
||||||
TeamID *uint `json:"team_id" db:"team_id"` // null for no-team
|
TeamID *uint `json:"team_id" db:"team_id"` // null for no-team
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
Platform string `json:"platform" db:"platform"` // "windows" or "darwin"
|
Platform string `json:"platform" db:"platform"` // "windows" or "darwin"
|
||||||
Identifier string `json:"identifier,omitempty" db:"identifier"` // only set for macOS
|
Identifier string `json:"identifier,omitempty" db:"identifier"` // only set for macOS
|
||||||
Checksum []byte `json:"checksum,omitempty" db:"checksum"` // only set for macOS
|
Checksum []byte `json:"checksum,omitempty" db:"checksum"` // only set for macOS
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
Labels []ConfigurationProfileLabel `json:"labels,omitempty" db:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MDMProfileBatchPayload represents the payload to batch-set the profiles for
|
||||||
|
// a team or no-team.
|
||||||
|
type MDMProfileBatchPayload struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Contents []byte `json:"contents,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConfigProfilePayload {
|
func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConfigProfilePayload {
|
||||||
@ -373,6 +388,7 @@ func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConf
|
|||||||
Platform: "windows",
|
Platform: "windows",
|
||||||
CreatedAt: cp.CreatedAt,
|
CreatedAt: cp.CreatedAt,
|
||||||
UpdatedAt: cp.UpdatedAt,
|
UpdatedAt: cp.UpdatedAt,
|
||||||
|
Labels: cp.Labels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,5 +406,108 @@ func NewMDMConfigProfilePayloadFromApple(cp *MDMAppleConfigProfile) *MDMConfigPr
|
|||||||
Checksum: cp.Checksum,
|
Checksum: cp.Checksum,
|
||||||
CreatedAt: cp.CreatedAt,
|
CreatedAt: cp.CreatedAt,
|
||||||
UpdatedAt: cp.UpdatedAt,
|
UpdatedAt: cp.UpdatedAt,
|
||||||
|
Labels: cp.Labels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MDMProfileSpec represents the spec used to define configuration
|
||||||
|
// profiles via yaml files.
|
||||||
|
type MDMProfileSpec struct {
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface to add backwards
|
||||||
|
// compatibility to previous ways to define profile specs.
|
||||||
|
func (p *MDMProfileSpec) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if lookAhead := bytes.TrimSpace(data); len(lookAhead) > 0 && lookAhead[0] == '"' {
|
||||||
|
var backwardsCompat string
|
||||||
|
if err := json.Unmarshal(data, &backwardsCompat); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal profile spec. Error using old format: %w", err)
|
||||||
|
}
|
||||||
|
p.Path = backwardsCompat
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use an alias type to avoid recursively calling this function forever.
|
||||||
|
type Alias MDMProfileSpec
|
||||||
|
aliasData := struct {
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
Alias: (*Alias)(p),
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &aliasData); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal profile spec. Error using new format: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MDMProfileSpec) Clone() (Cloner, error) {
|
||||||
|
return p.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MDMProfileSpec) Copy() *MDMProfileSpec {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var clone MDMProfileSpec
|
||||||
|
clone = *p
|
||||||
|
|
||||||
|
if len(p.Labels) > 0 {
|
||||||
|
clone.Labels = make([]string, len(p.Labels))
|
||||||
|
copy(clone.Labels, p.Labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelCountMap(labels []string) map[string]int {
|
||||||
|
counts := make(map[string]int)
|
||||||
|
for _, label := range labels {
|
||||||
|
counts[label]++
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
|
// MDMProfileSpecsMatch match checks if two slices contain the same spec
|
||||||
|
// elements, regardless of order.
|
||||||
|
func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pathLabelCounts := make(map[string]map[string]int)
|
||||||
|
for _, v := range a {
|
||||||
|
pathLabelCounts[v.Path] = labelCountMap(v.Labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range b {
|
||||||
|
labels, ok := pathLabelCounts[v.Path]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bLabelCounts := labelCountMap(v.Labels)
|
||||||
|
for label, count := range bLabelCounts {
|
||||||
|
if labels[label] != count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
labels[label] -= count
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, count := range labels {
|
||||||
|
if count != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(pathLabelCounts, v.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(pathLabelCounts) == 0
|
||||||
|
}
|
||||||
|
@ -179,3 +179,148 @@ func TestMDMAppleBootstrapPackage(t *testing.T) {
|
|||||||
require.Empty(t, url)
|
require.Empty(t, url)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMDMProfileSpecUnmarshalJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
expectPath string
|
||||||
|
expectLabels []string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: []byte(""),
|
||||||
|
expectPath: "",
|
||||||
|
expectLabels: nil,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new format",
|
||||||
|
input: []byte(`{"path": "testpath", "labels": ["label1", "label2"]}`),
|
||||||
|
expectPath: "testpath",
|
||||||
|
expectLabels: []string{"label1", "label2"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old format",
|
||||||
|
input: []byte(`"oldpath"`),
|
||||||
|
expectPath: "oldpath",
|
||||||
|
expectLabels: nil,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON",
|
||||||
|
input: []byte(`{invalid json}`),
|
||||||
|
expectPath: "",
|
||||||
|
expectLabels: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid JSON with extra fields",
|
||||||
|
input: []byte(`{"path": "testpath", "labels": ["label1"], "extra": "field"}`),
|
||||||
|
expectPath: "testpath",
|
||||||
|
expectLabels: []string{"label1"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var p fleet.MDMProfileSpec
|
||||||
|
err := p.UnmarshalJSON(tc.input)
|
||||||
|
if tc.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectPath, p.Path)
|
||||||
|
require.Equal(t, tc.expectLabels, p.Labels)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("complex scenario", func(t *testing.T) {
|
||||||
|
var p fleet.MDMProfileSpec
|
||||||
|
// test new format
|
||||||
|
data := []byte(`{"path": "newpath", "labels": ["label1", "label2"]}`)
|
||||||
|
err := p.UnmarshalJSON(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "newpath", p.Path)
|
||||||
|
require.Equal(t, []string{"label1", "label2"}, p.Labels)
|
||||||
|
|
||||||
|
// test old format
|
||||||
|
p = fleet.MDMProfileSpec{}
|
||||||
|
data = []byte(`"oldpath"`)
|
||||||
|
err = p.UnmarshalJSON(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "oldpath", p.Path)
|
||||||
|
require.Empty(t, p.Labels)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMDMProfileSpecsMatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
a []fleet.MDMProfileSpec
|
||||||
|
b []fleet.MDMProfileSpec
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty Slices",
|
||||||
|
a: []fleet.MDMProfileSpec{},
|
||||||
|
b: []fleet.MDMProfileSpec{},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single Element Match",
|
||||||
|
a: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path1", Labels: []string{"label1"}},
|
||||||
|
},
|
||||||
|
b: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path1", Labels: []string{"label1"}},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single Element Mismatch",
|
||||||
|
a: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path1", Labels: []string{"label1"}},
|
||||||
|
},
|
||||||
|
b: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path2", Labels: []string{"label1"}},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple Elements Match",
|
||||||
|
a: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path1", Labels: []string{"label1", "label2"}},
|
||||||
|
{Path: "path2", Labels: []string{"label3"}},
|
||||||
|
},
|
||||||
|
b: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path2", Labels: []string{"label3"}},
|
||||||
|
{Path: "path1", Labels: []string{"label1", "label2"}},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple Elements Mismatch",
|
||||||
|
a: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path1", Labels: []string{"label1"}},
|
||||||
|
{Path: "path2", Labels: []string{"label3"}},
|
||||||
|
},
|
||||||
|
b: []fleet.MDMProfileSpec{
|
||||||
|
{Path: "path1", Labels: []string{"label2"}},
|
||||||
|
{Path: "path2", Labels: []string{"label3"}},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := fleet.MDMProfileSpecsMatch(tc.a, tc.b)
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -618,7 +618,7 @@ type Service interface {
|
|||||||
GetHostDEPAssignment(ctx context.Context, host *Host) (*HostDEPAssignment, error)
|
GetHostDEPAssignment(ctx context.Context, host *Host) (*HostDEPAssignment, error)
|
||||||
|
|
||||||
// NewMDMAppleConfigProfile creates a new configuration profile for the specified team.
|
// NewMDMAppleConfigProfile creates a new configuration profile for the specified team.
|
||||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader) (*MDMAppleConfigProfile, error)
|
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string) (*MDMAppleConfigProfile, error)
|
||||||
// GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple
|
// GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple
|
||||||
// configuration profile via its numeric ID. This method is deprecated and
|
// configuration profile via its numeric ID. This method is deprecated and
|
||||||
// should not be used for new endpoints.
|
// should not be used for new endpoints.
|
||||||
@ -856,7 +856,7 @@ type Service interface {
|
|||||||
|
|
||||||
// NewMDMWindowsConfigProfile creates a new Windows configuration profile for
|
// NewMDMWindowsConfigProfile creates a new Windows configuration profile for
|
||||||
// the specified team.
|
// the specified team.
|
||||||
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader) (*MDMWindowsConfigProfile, error)
|
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string) (*MDMWindowsConfigProfile, error)
|
||||||
|
|
||||||
// NewMDMUnsupportedConfigProfile is called when a profile with an
|
// NewMDMUnsupportedConfigProfile is called when a profile with an
|
||||||
// unsupported extension is uploaded.
|
// unsupported extension is uploaded.
|
||||||
@ -867,7 +867,7 @@ type Service interface {
|
|||||||
|
|
||||||
// BatchSetMDMProfiles replaces the custom Windows/macOS profiles for a specified
|
// BatchSetMDMProfiles replaces the custom Windows/macOS profiles for a specified
|
||||||
// team or for hosts with no team.
|
// team or for hosts with no team.
|
||||||
BatchSetMDMProfiles(ctx context.Context, teamID *uint, teamName *string, profiles map[string][]byte, dryRun bool, skipBulkPending bool) error
|
BatchSetMDMProfiles(ctx context.Context, teamID *uint, teamName *string, profiles []MDMProfileBatchPayload, dryRun bool, skipBulkPending bool) error
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Common MDM
|
// Common MDM
|
||||||
|
@ -186,15 +186,19 @@ func (t *TeamMDM) Copy() *TeamMDM {
|
|||||||
// pointers/slices/maps).
|
// pointers/slices/maps).
|
||||||
|
|
||||||
if t.MacOSSettings.CustomSettings != nil {
|
if t.MacOSSettings.CustomSettings != nil {
|
||||||
clone.MacOSSettings.CustomSettings = make([]string, len(t.MacOSSettings.CustomSettings))
|
clone.MacOSSettings.CustomSettings = make([]MDMProfileSpec, len(t.MacOSSettings.CustomSettings))
|
||||||
copy(clone.MacOSSettings.CustomSettings, t.MacOSSettings.CustomSettings)
|
for i, mps := range t.MacOSSettings.CustomSettings {
|
||||||
|
clone.MacOSSettings.CustomSettings[i] = *mps.Copy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if t.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
|
if t.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
|
||||||
clone.MacOSSettings.DeprecatedEnableDiskEncryption = ptr.Bool(*t.MacOSSettings.DeprecatedEnableDiskEncryption)
|
clone.MacOSSettings.DeprecatedEnableDiskEncryption = ptr.Bool(*t.MacOSSettings.DeprecatedEnableDiskEncryption)
|
||||||
}
|
}
|
||||||
if t.WindowsSettings.CustomSettings.Set {
|
if t.WindowsSettings.CustomSettings.Set {
|
||||||
windowsSettings := make([]string, len(t.WindowsSettings.CustomSettings.Value))
|
windowsSettings := make([]MDMProfileSpec, len(t.WindowsSettings.CustomSettings.Value))
|
||||||
copy(windowsSettings, t.WindowsSettings.CustomSettings.Value)
|
for i, mps := range t.WindowsSettings.CustomSettings.Value {
|
||||||
|
windowsSettings[i] = *mps.Copy()
|
||||||
|
}
|
||||||
clone.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
|
clone.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
|
||||||
}
|
}
|
||||||
return &clone
|
return &clone
|
||||||
|
@ -288,7 +288,7 @@ func TestTeamMDMCopy(t *testing.T) {
|
|||||||
t.Run("copy MacOSSettings", func(t *testing.T) {
|
t.Run("copy MacOSSettings", func(t *testing.T) {
|
||||||
tm := &TeamMDM{
|
tm := &TeamMDM{
|
||||||
MacOSSettings: MacOSSettings{
|
MacOSSettings: MacOSSettings{
|
||||||
CustomSettings: []string{"a", "b"},
|
CustomSettings: []MDMProfileSpec{{Path: "a"}, {Path: "b"}},
|
||||||
DeprecatedEnableDiskEncryption: ptr.Bool(false),
|
DeprecatedEnableDiskEncryption: ptr.Bool(false),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,13 @@ type MDMWindowsBitLockerSummary struct {
|
|||||||
type MDMWindowsConfigProfile struct {
|
type MDMWindowsConfigProfile struct {
|
||||||
// ProfileUUID is the unique identifier of the configuration profile in
|
// ProfileUUID is the unique identifier of the configuration profile in
|
||||||
// Fleet. For Windows profiles, it is the letter "w" followed by a uuid.
|
// Fleet. For Windows profiles, it is the letter "w" followed by a uuid.
|
||||||
ProfileUUID string `db:"profile_uuid" json:"profile_uuid"`
|
ProfileUUID string `db:"profile_uuid" json:"profile_uuid"`
|
||||||
TeamID *uint `db:"team_id" json:"team_id"`
|
TeamID *uint `db:"team_id" json:"team_id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
SyncML []byte `db:"syncml" json:"-"`
|
SyncML []byte `db:"syncml" json:"-"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
Labels []ConfigurationProfileLabel `db:"labels" json:"labels,omitempty"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateUserProvided ensures that the SyncML content in the profile is valid
|
// ValidateUserProvided ensures that the SyncML content in the profile is valid
|
||||||
|
@ -148,7 +148,7 @@ type ListUniqueHostsInLabelsFunc func(ctx context.Context, filter fleet.TeamFilt
|
|||||||
|
|
||||||
type SearchLabelsFunc func(ctx context.Context, filter fleet.TeamFilter, query string, omit ...uint) ([]*fleet.Label, error)
|
type SearchLabelsFunc func(ctx context.Context, filter fleet.TeamFilter, query string, omit ...uint) ([]*fleet.Label, error)
|
||||||
|
|
||||||
type LabelIDsByNameFunc func(ctx context.Context, labels []string) ([]uint, error)
|
type LabelIDsByNameFunc func(ctx context.Context, labels []string) (map[string]uint, error)
|
||||||
|
|
||||||
type AsyncBatchInsertLabelMembershipFunc func(ctx context.Context, batch [][2]uint) error
|
type AsyncBatchInsertLabelMembershipFunc func(ctx context.Context, batch [][2]uint) error
|
||||||
|
|
||||||
@ -2422,7 +2422,7 @@ func (s *DataStore) SearchLabels(ctx context.Context, filter fleet.TeamFilter, q
|
|||||||
return s.SearchLabelsFunc(ctx, filter, query, omit...)
|
return s.SearchLabelsFunc(ctx, filter, query, omit...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DataStore) LabelIDsByName(ctx context.Context, labels []string) ([]uint, error) {
|
func (s *DataStore) LabelIDsByName(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.LabelIDsByNameFuncInvoked = true
|
s.LabelIDsByNameFuncInvoked = true
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
@ -15,7 +15,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/pkg/rawjson"
|
"github.com/fleetdm/fleet/v4/pkg/rawjson"
|
||||||
"github.com/fleetdm/fleet/v4/server"
|
|
||||||
"github.com/fleetdm/fleet/v4/server/authz"
|
"github.com/fleetdm/fleet/v4/server/authz"
|
||||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
||||||
@ -676,7 +675,7 @@ func (svc *Service) validateMDM(
|
|||||||
// we want to use `oldMdm` here as this boolean is set by the fleet
|
// we want to use `oldMdm` here as this boolean is set by the fleet
|
||||||
// server at startup and can't be modified by the user
|
// server at startup and can't be modified by the user
|
||||||
if !oldMdm.EnabledAndConfigured {
|
if !oldMdm.EnabledAndConfigured {
|
||||||
if len(mdm.MacOSSettings.CustomSettings) > 0 && !server.SliceStringsMatch(mdm.MacOSSettings.CustomSettings, oldMdm.MacOSSettings.CustomSettings) {
|
if len(mdm.MacOSSettings.CustomSettings) > 0 && !fleet.MDMProfileSpecsMatch(mdm.MacOSSettings.CustomSettings, oldMdm.MacOSSettings.CustomSettings) {
|
||||||
invalid.Append("macos_settings.custom_settings",
|
invalid.Append("macos_settings.custom_settings",
|
||||||
`Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
`Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||||
}
|
}
|
||||||
@ -699,7 +698,7 @@ func (svc *Service) validateMDM(
|
|||||||
if !mdm.WindowsEnabledAndConfigured {
|
if !mdm.WindowsEnabledAndConfigured {
|
||||||
if mdm.WindowsSettings.CustomSettings.Set &&
|
if mdm.WindowsSettings.CustomSettings.Set &&
|
||||||
len(mdm.WindowsSettings.CustomSettings.Value) > 0 &&
|
len(mdm.WindowsSettings.CustomSettings.Value) > 0 &&
|
||||||
!server.SliceStringsMatch(mdm.WindowsSettings.CustomSettings.Value, oldMdm.WindowsSettings.CustomSettings.Value) {
|
!fleet.MDMProfileSpecsMatch(mdm.WindowsSettings.CustomSettings.Value, oldMdm.WindowsSettings.CustomSettings.Value) {
|
||||||
invalid.Append("windows_settings.custom_settings",
|
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.`)
|
`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.`)
|
||||||
}
|
}
|
||||||
|
@ -814,7 +814,9 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "newDefaultTeamNoLicense",
|
name: "newDefaultTeamNoLicense",
|
||||||
@ -843,7 +845,9 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "foundEdit",
|
name: "foundEdit",
|
||||||
@ -857,7 +861,9 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "ssoFree",
|
name: "ssoFree",
|
||||||
@ -877,7 +883,9 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "ssoAllFields",
|
name: "ssoAllFields",
|
||||||
@ -900,7 +908,9 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||||||
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
|
||||||
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
|
||||||
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
EnableDiskEncryption: optjson.Bool{Set: true, Valid: false},
|
||||||
WindowsSettings: fleet.WindowsSettings{CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}}},
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "ssoShortEntityID",
|
name: "ssoShortEntityID",
|
||||||
|
@ -298,7 +298,8 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
|||||||
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
||||||
}
|
}
|
||||||
defer ff.Close()
|
defer ff.Close()
|
||||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff)
|
// providing an empty set of labels since this endpoint is only maintained for backwards compat
|
||||||
|
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
||||||
}
|
}
|
||||||
@ -307,7 +308,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader) (*fleet.MDMAppleConfigProfile, error) {
|
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string) (*fleet.MDMAppleConfigProfile, error) {
|
||||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err)
|
return nil, ctxerr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
@ -347,6 +348,12 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r
|
|||||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: err.Error()})
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labelMap, err := svc.validateProfileLabels(ctx, labels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||||
|
}
|
||||||
|
cp.Labels = labelMap
|
||||||
|
|
||||||
newCP, err := svc.ds.NewMDMAppleConfigProfile(ctx, *cp)
|
newCP, err := svc.ds.NewMDMAppleConfigProfile(ctx, *cp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var existsErr existsErrorInterface
|
var existsErr existsErrorInterface
|
||||||
|
@ -593,11 +593,11 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// test authz create new profile (no team)
|
// test authz create new profile (no team)
|
||||||
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes))
|
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), nil)
|
||||||
checkShouldFail(err, tt.shouldFailGlobal)
|
checkShouldFail(err, tt.shouldFailGlobal)
|
||||||
|
|
||||||
// test authz create new profile (team 1)
|
// test authz create new profile (team 1)
|
||||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes))
|
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), nil)
|
||||||
checkShouldFail(err, tt.shouldFailTeam)
|
checkShouldFail(err, tt.shouldFailTeam)
|
||||||
|
|
||||||
// test authz list profiles (no team)
|
// test authz list profiles (no team)
|
||||||
@ -659,7 +659,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r)
|
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "Foo", cp.Name)
|
require.Equal(t, "Foo", cp.Name)
|
||||||
require.Equal(t, "Bar", cp.Identifier)
|
require.Equal(t, "Bar", cp.Identifier)
|
||||||
|
@ -206,11 +206,16 @@ func (svc *Service) NewDistributedQueryCampaignByNames(ctx context.Context, quer
|
|||||||
return nil, ctxerr.Wrap(ctx, err, "finding host IDs")
|
return nil, ctxerr.Wrap(ctx, err, "finding host IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
labelIDs, err := svc.ds.LabelIDsByName(ctx, labels)
|
labelMap, err := svc.ds.LabelIDsByName(ctx, labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err, "finding label IDs")
|
return nil, ctxerr.Wrap(ctx, err, "finding label IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var labelIDs []uint
|
||||||
|
for _, labelID := range labelMap {
|
||||||
|
labelIDs = append(labelIDs, labelID)
|
||||||
|
}
|
||||||
|
|
||||||
targets := fleet.HostTargets{HostIDs: hostIDs, LabelIDs: labelIDs}
|
targets := fleet.HostTargets{HostIDs: hostIDs, LabelIDs: labelIDs}
|
||||||
return svc.NewDistributedQueryCampaign(ctx, queryString, queryID, targets)
|
return svc.NewDistributedQueryCampaign(ctx, queryString, queryID, targets)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ func TestLiveQueryAuth(t *testing.T) {
|
|||||||
ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, names []string) ([]uint, error) {
|
ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, names []string) ([]uint, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string) (map[string]uint, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ds.CountHostsInTargetsFunc = func(ctx context.Context, filters fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
ds.CountHostsInTargetsFunc = func(ctx context.Context, filters fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
||||||
|
@ -279,34 +279,40 @@ func (c *Client) runAppConfigChecks(fn func(ac *fleet.EnrichedAppConfig) error)
|
|||||||
return fn(appCfg)
|
return fn(appCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getProfilesContents takes file paths and creates a map of profile contents
|
// getProfilesContents takes file paths and creates a slice of profile payloads
|
||||||
// keyed by the name of the profile (the file name on Windows,
|
// ready to batch-apply.
|
||||||
// PayloadDisplayName on macOS)
|
func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fleet.MDMProfileBatchPayload, error) {
|
||||||
func getProfilesContents(baseDir string, paths []string) (map[string][]byte, error) {
|
fileNameMap := make(map[string]struct{}, len(profiles))
|
||||||
files := resolveApplyRelativePaths(baseDir, paths)
|
result := make([]fleet.MDMProfileBatchPayload, 0, len(profiles))
|
||||||
fileContents := make(map[string][]byte, len(files))
|
|
||||||
for _, f := range files {
|
for _, profile := range profiles {
|
||||||
b, err := os.ReadFile(f)
|
filePath := resolveApplyRelativePath(baseDir, profile.Path)
|
||||||
|
fileContents, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("applying fleet config: %w", err)
|
return nil, fmt.Errorf("applying fleet config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// by default, use the file name. macOS profiles use their PayloadDisplayName
|
// by default, use the file name. macOS profiles use their PayloadDisplayName
|
||||||
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
|
name := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
||||||
if mdm.GetRawProfilePlatform(b) == "darwin" {
|
if mdm.GetRawProfilePlatform(fileContents) == "darwin" {
|
||||||
mc, err := fleet.NewMDMAppleConfigProfile(b, nil)
|
mc, err := fleet.NewMDMAppleConfigProfile(fileContents, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("applying fleet config: %w", err)
|
return nil, fmt.Errorf("applying fleet config: %w", err)
|
||||||
}
|
}
|
||||||
name = strings.TrimSpace(mc.Name)
|
name = strings.TrimSpace(mc.Name)
|
||||||
}
|
}
|
||||||
if _, isDuplicate := fileContents[name]; isDuplicate {
|
if _, isDuplicate := fileNameMap[name]; isDuplicate {
|
||||||
return nil, errors.New("Couldn't edit windows_settings.custom_settings. More than one configuration profile have the same name (Windows .xml file name or macOS PayloadDisplayName).")
|
return nil, errors.New("Couldn't edit windows_settings.custom_settings. More than one configuration profile have the same name (Windows .xml file name or macOS PayloadDisplayName).")
|
||||||
}
|
}
|
||||||
fileContents[name] = b
|
fileNameMap[name] = struct{}{}
|
||||||
}
|
result = append(result, fleet.MDMProfileBatchPayload{
|
||||||
|
Name: name,
|
||||||
|
Contents: fileContents,
|
||||||
|
Labels: profile.Labels,
|
||||||
|
})
|
||||||
|
|
||||||
return fileContents, nil
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyGroup applies the given spec group to Fleet.
|
// ApplyGroup applies the given spec group to Fleet.
|
||||||
@ -384,7 +390,14 @@ func (c *Client) ApplyGroup(
|
|||||||
macosCustomSettings := extractAppCfgMacOSCustomSettings(specs.AppConfig)
|
macosCustomSettings := extractAppCfgMacOSCustomSettings(specs.AppConfig)
|
||||||
allCustomSettings := append(macosCustomSettings, windowsCustomSettings...)
|
allCustomSettings := append(macosCustomSettings, windowsCustomSettings...)
|
||||||
|
|
||||||
if len(allCustomSettings) > 0 {
|
// if there is no custom setting but the windows and mac settings are
|
||||||
|
// non-nil, this means that we want to clear the existing custom settings,
|
||||||
|
// so we still go on with calling the batch-apply endpoint.
|
||||||
|
//
|
||||||
|
// TODO(mna): shouldn't that be an || instead of && ? I.e. if there are no
|
||||||
|
// custom settings but windows is present and empty (but mac is absent),
|
||||||
|
// shouldn't that clear the windows ones?
|
||||||
|
if (windowsCustomSettings != nil && macosCustomSettings != nil) || len(allCustomSettings) > 0 {
|
||||||
fileContents, err := getProfilesContents(baseDir, allCustomSettings)
|
fileContents, err := getProfilesContents(baseDir, allCustomSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -461,7 +474,7 @@ func (c *Client) ApplyGroup(
|
|||||||
// that any non-existing file error is found before applying the specs.
|
// that any non-existing file error is found before applying the specs.
|
||||||
tmMDMSettings := extractTmSpecsMDMCustomSettings(specs.Teams)
|
tmMDMSettings := extractTmSpecsMDMCustomSettings(specs.Teams)
|
||||||
|
|
||||||
tmFileContents := make(map[string]map[string][]byte, len(tmMDMSettings))
|
tmFileContents := make(map[string][]fleet.MDMProfileBatchPayload, len(tmMDMSettings))
|
||||||
for k, paths := range tmMDMSettings {
|
for k, paths := range tmMDMSettings {
|
||||||
fileContents, err := getProfilesContents(baseDir, paths)
|
fileContents, err := getProfilesContents(baseDir, paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -606,7 +619,7 @@ func resolveApplyRelativePaths(baseDir string, paths []string) []string {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractAppCfgMacOSCustomSettings(appCfg interface{}) []string {
|
func extractAppCfgCustomSettings(appCfg interface{}, platformKey string) []fleet.MDMProfileSpec {
|
||||||
asMap, ok := appCfg.(map[string]interface{})
|
asMap, ok := appCfg.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@ -615,7 +628,7 @@ func extractAppCfgMacOSCustomSettings(appCfg interface{}) []string {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
mos, ok := mmdm["macos_settings"].(map[string]interface{})
|
mos, ok := mmdm[platformKey].(map[string]interface{})
|
||||||
if !ok || mos == nil {
|
if !ok || mos == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -630,54 +643,46 @@ func extractAppCfgMacOSCustomSettings(appCfg interface{}) []string {
|
|||||||
if !ok || csAny == nil {
|
if !ok || csAny == nil {
|
||||||
// return a non-nil, empty slice instead, so the caller knows that the
|
// return a non-nil, empty slice instead, so the caller knows that the
|
||||||
// custom_settings key was actually provided.
|
// custom_settings key was actually provided.
|
||||||
return []string{}
|
return []fleet.MDMProfileSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
csStrings := make([]string, 0, len(csAny))
|
csSpecs := make([]fleet.MDMProfileSpec, 0, len(csAny))
|
||||||
for _, v := range csAny {
|
for _, v := range csAny {
|
||||||
s, _ := v.(string)
|
if m, ok := v.(map[string]interface{}); ok {
|
||||||
if s != "" {
|
var profSpec fleet.MDMProfileSpec
|
||||||
csStrings = append(csStrings, s)
|
|
||||||
|
// extract the Path field
|
||||||
|
if path, ok := m["path"].(string); ok {
|
||||||
|
profSpec.Path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the Labels field, labels are cleared if not provided
|
||||||
|
if labels, ok := m["labels"].([]interface{}); ok {
|
||||||
|
for _, label := range labels {
|
||||||
|
if strLabel, ok := label.(string); ok {
|
||||||
|
profSpec.Labels = append(profSpec.Labels, strLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if profSpec.Path != "" {
|
||||||
|
csSpecs = append(csSpecs, profSpec)
|
||||||
|
}
|
||||||
|
} else if m, ok := v.(string); ok { // for backwards compatibility with the old way to define profiles
|
||||||
|
if m != "" {
|
||||||
|
csSpecs = append(csSpecs, fleet.MDMProfileSpec{Path: m})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return csStrings
|
return csSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractAppCfgWindowsCustomSettings(appCfg interface{}) []string {
|
func extractAppCfgMacOSCustomSettings(appCfg interface{}) []fleet.MDMProfileSpec {
|
||||||
asMap, ok := appCfg.(map[string]interface{})
|
return extractAppCfgCustomSettings(appCfg, "macos_settings")
|
||||||
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"]
|
func extractAppCfgWindowsCustomSettings(appCfg interface{}) []fleet.MDMProfileSpec {
|
||||||
if !ok {
|
return extractAppCfgCustomSettings(appCfg, "windows_settings")
|
||||||
// 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 {
|
func extractAppCfgScripts(appCfg interface{}) []string {
|
||||||
@ -710,8 +715,8 @@ func extractAppCfgScripts(appCfg interface{}) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the custom macOS and Windows settings keyed by team name.
|
// returns the custom macOS and Windows settings keyed by team name.
|
||||||
func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]string {
|
func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]fleet.MDMProfileSpec {
|
||||||
var m map[string][]string
|
var m map[string][]fleet.MDMProfileSpec
|
||||||
for _, tm := range tmSpecs {
|
for _, tm := range tmSpecs {
|
||||||
var spec struct {
|
var spec struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -729,15 +734,15 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]str
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if spec.Name != "" {
|
if spec.Name != "" {
|
||||||
var macOSSettings []string
|
var macOSSettings []fleet.MDMProfileSpec
|
||||||
var windowsSettings []string
|
var windowsSettings []fleet.MDMProfileSpec
|
||||||
|
|
||||||
// to keep existing bahavior, if any of the custom
|
// to keep existing bahavior, if any of the custom
|
||||||
// settings is provided, make the map a non-nil map
|
// settings is provided, make the map a non-nil map
|
||||||
if len(spec.MDM.MacOSSettings.CustomSettings) > 0 ||
|
if len(spec.MDM.MacOSSettings.CustomSettings) > 0 ||
|
||||||
len(spec.MDM.WindowsSettings.CustomSettings) > 0 {
|
len(spec.MDM.WindowsSettings.CustomSettings) > 0 {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
m = make(map[string][]string)
|
m = make(map[string][]fleet.MDMProfileSpec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,7 +754,7 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]str
|
|||||||
if macOSSettings == nil {
|
if macOSSettings == nil {
|
||||||
// to be consistent with the AppConfig custom settings, set it to an
|
// to be consistent with the AppConfig custom settings, set it to an
|
||||||
// empty slice if the provided custom settings are present but empty.
|
// empty slice if the provided custom settings are present but empty.
|
||||||
macOSSettings = []string{}
|
macOSSettings = []fleet.MDMProfileSpec{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(spec.MDM.WindowsSettings.CustomSettings) > 0 {
|
if len(spec.MDM.WindowsSettings.CustomSettings) > 0 {
|
||||||
@ -760,10 +765,11 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]str
|
|||||||
if windowsSettings == nil {
|
if windowsSettings == nil {
|
||||||
// to be consistent with the AppConfig custom settings, set it to an
|
// to be consistent with the AppConfig custom settings, set it to an
|
||||||
// empty slice if the provided custom settings are present but empty.
|
// empty slice if the provided custom settings are present but empty.
|
||||||
windowsSettings = []string{}
|
windowsSettings = []fleet.MDMProfileSpec{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: validate equal names here and API?
|
||||||
if macOSSettings != nil || windowsSettings != nil {
|
if macOSSettings != nil || windowsSettings != nil {
|
||||||
m[spec.Name] = append(macOSSettings, windowsSettings...)
|
m[spec.Name] = append(macOSSettings, windowsSettings...)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ func (c *Client) ApplyAppConfig(payload interface{}, opts fleet.ApplySpecOptions
|
|||||||
|
|
||||||
// ApplyNoTeamProfiles sends the list of profiles to be applied for the hosts
|
// ApplyNoTeamProfiles sends the list of profiles to be applied for the hosts
|
||||||
// in no team.
|
// in no team.
|
||||||
func (c *Client) ApplyNoTeamProfiles(profiles map[string][]byte, opts fleet.ApplySpecOptions) error {
|
func (c *Client) ApplyNoTeamProfiles(profiles []fleet.MDMProfileBatchPayload, opts fleet.ApplySpecOptions) error {
|
||||||
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
|
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
|
||||||
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profiles}, verb, path, nil, opts.RawQuery())
|
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profiles}, verb, path, nil, opts.RawQuery())
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplySpecOptions
|
|||||||
|
|
||||||
// ApplyTeamProfiles sends the list of profiles to be applied for the specified
|
// ApplyTeamProfiles sends the list of profiles to be applied for the specified
|
||||||
// team.
|
// team.
|
||||||
func (c *Client) ApplyTeamProfiles(tmName string, profiles map[string][]byte, opts fleet.ApplySpecOptions) error {
|
func (c *Client) ApplyTeamProfiles(tmName string, profiles []fleet.MDMProfileBatchPayload, opts fleet.ApplySpecOptions) error {
|
||||||
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
|
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
|
||||||
query, err := url.ParseQuery(opts.RawQuery())
|
query, err := url.ParseQuery(opts.RawQuery())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/pkg/spec"
|
"github.com/fleetdm/fleet/v4/pkg/spec"
|
||||||
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ func TestExtractAppConfigMacOSCustomSettings(t *testing.T) {
|
|||||||
cases := []struct {
|
cases := []struct {
|
||||||
desc string
|
desc string
|
||||||
yaml string
|
yaml string
|
||||||
want []string
|
want []fleet.MDMProfileSpec
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"no settings",
|
"no settings",
|
||||||
@ -50,7 +50,7 @@ spec:
|
|||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
`,
|
`,
|
||||||
[]string{},
|
[]fleet.MDMProfileSpec{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"custom settings specified",
|
"custom settings specified",
|
||||||
@ -63,16 +63,61 @@ spec:
|
|||||||
mdm:
|
mdm:
|
||||||
macos_settings:
|
macos_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
- "a"
|
- path: "a"
|
||||||
- "b"
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- bar
|
||||||
|
- path: "b"
|
||||||
`,
|
`,
|
||||||
[]string{"a", "b"},
|
[]fleet.MDMProfileSpec{{Path: "a", Labels: []string{"foo", "bar"}}, {Path: "b"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty and invalid custom settings",
|
"empty and invalid custom settings",
|
||||||
`
|
`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: config
|
kind: config
|
||||||
|
spec:
|
||||||
|
org_info:
|
||||||
|
org_name: "Fleet"
|
||||||
|
mdm:
|
||||||
|
macos_settings:
|
||||||
|
custom_settings:
|
||||||
|
- path: "a"
|
||||||
|
labels:
|
||||||
|
- path: ""
|
||||||
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- path: 4
|
||||||
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- "bar"
|
||||||
|
- path: "c"
|
||||||
|
labels:
|
||||||
|
- baz
|
||||||
|
`,
|
||||||
|
[]fleet.MDMProfileSpec{{Path: "a"}, {Path: "c", Labels: []string{"baz"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"old custom settings specified",
|
||||||
|
`
|
||||||
|
apiVersion: v1
|
||||||
|
kind: config
|
||||||
|
spec:
|
||||||
|
org_info:
|
||||||
|
org_name: "Fleet"
|
||||||
|
mdm:
|
||||||
|
macos_settings:
|
||||||
|
custom_settings:
|
||||||
|
- "a"
|
||||||
|
- "b"
|
||||||
|
`,
|
||||||
|
[]fleet.MDMProfileSpec{{Path: "a"}, {Path: "b"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"old empty and invalid custom settings",
|
||||||
|
`
|
||||||
|
apiVersion: v1
|
||||||
|
kind: config
|
||||||
spec:
|
spec:
|
||||||
org_info:
|
org_info:
|
||||||
org_name: "Fleet"
|
org_name: "Fleet"
|
||||||
@ -84,7 +129,7 @@ spec:
|
|||||||
- 4
|
- 4
|
||||||
- "c"
|
- "c"
|
||||||
`,
|
`,
|
||||||
[]string{"a", "c"},
|
[]fleet.MDMProfileSpec{{Path: "a"}, {Path: "c"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@ -103,7 +148,7 @@ func TestExtractAppConfigWindowsCustomSettings(t *testing.T) {
|
|||||||
cases := []struct {
|
cases := []struct {
|
||||||
desc string
|
desc string
|
||||||
yaml string
|
yaml string
|
||||||
want []string
|
want []fleet.MDMProfileSpec
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"no settings",
|
"no settings",
|
||||||
@ -139,7 +184,7 @@ spec:
|
|||||||
windows_settings:
|
windows_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
`,
|
`,
|
||||||
[]string{},
|
[]fleet.MDMProfileSpec{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"custom settings specified",
|
"custom settings specified",
|
||||||
@ -152,16 +197,61 @@ spec:
|
|||||||
mdm:
|
mdm:
|
||||||
windows_settings:
|
windows_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
- "a"
|
- path: "a"
|
||||||
- "b"
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- bar
|
||||||
|
- path: "b"
|
||||||
`,
|
`,
|
||||||
[]string{"a", "b"},
|
[]fleet.MDMProfileSpec{{Path: "a", Labels: []string{"foo", "bar"}}, {Path: "b"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty and invalid custom settings",
|
"empty and invalid custom settings",
|
||||||
`
|
`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: config
|
kind: config
|
||||||
|
spec:
|
||||||
|
org_info:
|
||||||
|
org_name: "Fleet"
|
||||||
|
mdm:
|
||||||
|
windows_settings:
|
||||||
|
custom_settings:
|
||||||
|
- path: "a"
|
||||||
|
labels:
|
||||||
|
- path: ""
|
||||||
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- path: 4
|
||||||
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- "bar"
|
||||||
|
- path: "c"
|
||||||
|
labels:
|
||||||
|
- baz
|
||||||
|
`,
|
||||||
|
[]fleet.MDMProfileSpec{{Path: "a"}, {Path: "c", Labels: []string{"baz"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"old custom settings specified",
|
||||||
|
`
|
||||||
|
apiVersion: v1
|
||||||
|
kind: config
|
||||||
|
spec:
|
||||||
|
org_info:
|
||||||
|
org_name: "Fleet"
|
||||||
|
mdm:
|
||||||
|
windows_settings:
|
||||||
|
custom_settings:
|
||||||
|
- "a"
|
||||||
|
- "b"
|
||||||
|
`,
|
||||||
|
[]fleet.MDMProfileSpec{{Path: "a"}, {Path: "b"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"old empty and invalid custom settings",
|
||||||
|
`
|
||||||
|
apiVersion: v1
|
||||||
|
kind: config
|
||||||
spec:
|
spec:
|
||||||
org_info:
|
org_info:
|
||||||
org_name: "Fleet"
|
org_name: "Fleet"
|
||||||
@ -173,7 +263,7 @@ spec:
|
|||||||
- 4
|
- 4
|
||||||
- "c"
|
- "c"
|
||||||
`,
|
`,
|
||||||
[]string{"a", "c"},
|
[]fleet.MDMProfileSpec{{Path: "a"}, {Path: "c"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@ -192,7 +282,7 @@ func TestExtractTeamSpecsMDMCustomSettings(t *testing.T) {
|
|||||||
cases := []struct {
|
cases := []struct {
|
||||||
desc string
|
desc string
|
||||||
yaml string
|
yaml string
|
||||||
want map[string][]string
|
want map[string][]fleet.MDMProfileSpec
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"no settings",
|
"no settings",
|
||||||
@ -252,13 +342,44 @@ spec:
|
|||||||
windows_settings:
|
windows_settings:
|
||||||
custom_settings:
|
custom_settings:
|
||||||
`,
|
`,
|
||||||
map[string][]string{"Fleet": {}, "Fleet2": {}},
|
map[string][]fleet.MDMProfileSpec{"Fleet": {}, "Fleet2": {}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"custom settings specified",
|
"custom settings specified",
|
||||||
`
|
`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: team
|
kind: team
|
||||||
|
spec:
|
||||||
|
team:
|
||||||
|
name: "Fleet"
|
||||||
|
mdm:
|
||||||
|
macos_settings:
|
||||||
|
custom_settings:
|
||||||
|
- path: "a"
|
||||||
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- bar
|
||||||
|
- path: "b"
|
||||||
|
windows_settings:
|
||||||
|
custom_settings:
|
||||||
|
- path: "c"
|
||||||
|
- path: "d"
|
||||||
|
labels:
|
||||||
|
- "foo"
|
||||||
|
- baz
|
||||||
|
`,
|
||||||
|
map[string][]fleet.MDMProfileSpec{"Fleet": {
|
||||||
|
{Path: "a", Labels: []string{"foo", "bar"}},
|
||||||
|
{Path: "b"},
|
||||||
|
{Path: "c"},
|
||||||
|
{Path: "d", Labels: []string{"foo", "baz"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"old custom settings specified",
|
||||||
|
`
|
||||||
|
apiVersion: v1
|
||||||
|
kind: team
|
||||||
spec:
|
spec:
|
||||||
team:
|
team:
|
||||||
name: "Fleet"
|
name: "Fleet"
|
||||||
@ -272,13 +393,43 @@ spec:
|
|||||||
- "c"
|
- "c"
|
||||||
- "d"
|
- "d"
|
||||||
`,
|
`,
|
||||||
map[string][]string{"Fleet": {"a", "b", "c", "d"}},
|
map[string][]fleet.MDMProfileSpec{"Fleet": {{Path: "a"}, {Path: "b"}, {Path: "c"}, {Path: "d"}}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"invalid custom settings",
|
"invalid custom settings",
|
||||||
`
|
`
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: team
|
kind: team
|
||||||
|
spec:
|
||||||
|
team:
|
||||||
|
name: "Fleet"
|
||||||
|
mdm:
|
||||||
|
macos_settings:
|
||||||
|
custom_settings:
|
||||||
|
- path: "a"
|
||||||
|
labels:
|
||||||
|
- "y"
|
||||||
|
- path: ""
|
||||||
|
- path: 42
|
||||||
|
labels:
|
||||||
|
- "x"
|
||||||
|
- path: "c"
|
||||||
|
windows_settings:
|
||||||
|
custom_settings:
|
||||||
|
- path: "x"
|
||||||
|
- path: ""
|
||||||
|
labels:
|
||||||
|
- "x"
|
||||||
|
- path: 24
|
||||||
|
- path: "y"
|
||||||
|
`,
|
||||||
|
map[string][]fleet.MDMProfileSpec{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"old invalid custom settings",
|
||||||
|
`
|
||||||
|
apiVersion: v1
|
||||||
|
kind: team
|
||||||
spec:
|
spec:
|
||||||
team:
|
team:
|
||||||
name: "Fleet"
|
name: "Fleet"
|
||||||
@ -296,7 +447,7 @@ spec:
|
|||||||
- 24
|
- 24
|
||||||
- "y"
|
- "y"
|
||||||
`,
|
`,
|
||||||
map[string][]string{},
|
map[string][]fleet.MDMProfileSpec{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@ -336,13 +487,16 @@ func TestExtractFilenameFromPath(t *testing.T) {
|
|||||||
|
|
||||||
func TestGetProfilesContents(t *testing.T) {
|
func TestGetProfilesContents(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
darwinProfile := mobileconfigForTest("bar", "I")
|
||||||
|
windowsProfile := syncMLForTest("./some/path")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
baseDir string
|
baseDir string
|
||||||
setupFiles [][2]string
|
setupFiles [][2]string
|
||||||
expectError bool
|
labels []string
|
||||||
expectedKeys []string
|
expectError bool
|
||||||
|
want []fleet.MDMProfileBatchPayload
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "invalid darwin xml",
|
name: "invalid darwin xml",
|
||||||
@ -350,34 +504,54 @@ func TestGetProfilesContents(t *testing.T) {
|
|||||||
setupFiles: [][2]string{
|
setupFiles: [][2]string{
|
||||||
{"foo.mobileconfig", `<?xml version="1.0" encoding="UTF-8"?>`},
|
{"foo.mobileconfig", `<?xml version="1.0" encoding="UTF-8"?>`},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectedKeys: []string{"foo"},
|
want: []fleet.MDMProfileBatchPayload{{Name: "foo"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "windows and darwin files",
|
name: "windows and darwin files",
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
setupFiles: [][2]string{
|
setupFiles: [][2]string{
|
||||||
{"foo.xml", string(syncMLForTest("./some/path"))},
|
{"foo.xml", string(windowsProfile)},
|
||||||
{"bar.mobileconfig", string(mobileconfigForTest("bar", "I"))},
|
{"bar.mobileconfig", string(darwinProfile)},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
want: []fleet.MDMProfileBatchPayload{
|
||||||
|
{Name: "foo", Contents: windowsProfile},
|
||||||
|
{Name: "bar", Contents: darwinProfile},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "windows and darwin files with labels",
|
||||||
|
baseDir: tempDir,
|
||||||
|
setupFiles: [][2]string{
|
||||||
|
{"foo.xml", string(windowsProfile)},
|
||||||
|
{"bar.mobileconfig", string(darwinProfile)},
|
||||||
|
},
|
||||||
|
labels: []string{"foo", "bar"},
|
||||||
|
expectError: false,
|
||||||
|
want: []fleet.MDMProfileBatchPayload{
|
||||||
|
{Name: "foo", Contents: windowsProfile, Labels: []string{"foo", "bar"}},
|
||||||
|
{Name: "bar", Contents: darwinProfile, Labels: []string{"foo", "bar"}},
|
||||||
},
|
},
|
||||||
expectError: false,
|
|
||||||
expectedKeys: []string{"foo", "bar"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "darwin files with file name != PayloadDisplayName",
|
name: "darwin files with file name != PayloadDisplayName",
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
setupFiles: [][2]string{
|
setupFiles: [][2]string{
|
||||||
{"foo.xml", string(syncMLForTest("./some/path"))},
|
{"foo.xml", string(windowsProfile)},
|
||||||
{"bar.mobileconfig", string(mobileconfigForTest("fizz", "I"))},
|
{"bar.mobileconfig", string(darwinProfile)},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
want: []fleet.MDMProfileBatchPayload{
|
||||||
|
{Name: "foo", Contents: windowsProfile},
|
||||||
|
{Name: "bar", Contents: darwinProfile},
|
||||||
},
|
},
|
||||||
expectError: false,
|
|
||||||
expectedKeys: []string{"foo", "fizz"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "duplicate names across windows and darwin",
|
name: "duplicate names across windows and darwin",
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
setupFiles: [][2]string{
|
setupFiles: [][2]string{
|
||||||
{"baz.xml", string(syncMLForTest("./some/path"))},
|
{"baz.xml", string(windowsProfile)},
|
||||||
{"bar.mobileconfig", string(mobileconfigForTest("baz", "I"))},
|
{"bar.mobileconfig", string(mobileconfigForTest("baz", "I"))},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
@ -386,8 +560,8 @@ func TestGetProfilesContents(t *testing.T) {
|
|||||||
name: "duplicate file names",
|
name: "duplicate file names",
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
setupFiles: [][2]string{
|
setupFiles: [][2]string{
|
||||||
{"baz.xml", string(syncMLForTest("./some/path"))},
|
{"baz.xml", string(windowsProfile)},
|
||||||
{"baz.xml", string(syncMLForTest("./some/path"))},
|
{"baz.xml", string(windowsProfile)},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
@ -395,11 +569,11 @@ func TestGetProfilesContents(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
paths := []string{}
|
paths := []fleet.MDMProfileSpec{}
|
||||||
for _, fileSpec := range tt.setupFiles {
|
for _, fileSpec := range tt.setupFiles {
|
||||||
filePath := filepath.Join(tempDir, fileSpec[0])
|
filePath := filepath.Join(tempDir, fileSpec[0])
|
||||||
require.NoError(t, os.WriteFile(filePath, []byte(fileSpec[1]), 0644))
|
require.NoError(t, os.WriteFile(filePath, []byte(fileSpec[1]), 0644))
|
||||||
paths = append(paths, filePath)
|
paths = append(paths, fleet.MDMProfileSpec{Path: filePath, Labels: tt.labels})
|
||||||
}
|
}
|
||||||
|
|
||||||
profileContents, err := getProfilesContents(tt.baseDir, paths)
|
profileContents, err := getProfilesContents(tt.baseDir, paths)
|
||||||
@ -409,11 +583,8 @@ func TestGetProfilesContents(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, profileContents)
|
require.NotNil(t, profileContents)
|
||||||
require.Len(t, profileContents, len(tt.expectedKeys))
|
require.Len(t, profileContents, len(tt.want))
|
||||||
for _, key := range tt.expectedKeys {
|
require.ElementsMatch(t, tt.want, profileContents)
|
||||||
_, exists := profileContents[key]
|
|
||||||
require.True(t, exists, fmt.Sprintf("Expected key %s not found", key))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3713,7 +3713,7 @@ func (s *integrationTestSuite) TestListHostsByLabel() {
|
|||||||
lblIDs, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
|
lblIDs, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, lblIDs, 1)
|
require.Len(t, lblIDs, 1)
|
||||||
labelID := lblIDs[0]
|
labelID := lblIDs["All Hosts"]
|
||||||
|
|
||||||
hosts := s.createHosts(t, "darwin")
|
hosts := s.createHosts(t, "darwin")
|
||||||
host := hosts[0]
|
host := hosts[0]
|
||||||
@ -5344,8 +5344,8 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
|
|||||||
s.DoJSON("GET", "/api/latest/fleet/scripts/results/test-id", nil, http.StatusPaymentRequired, &scriptResultResp)
|
s.DoJSON("GET", "/api/latest/fleet/scripts/results/test-id", nil, http.StatusPaymentRequired, &scriptResultResp)
|
||||||
|
|
||||||
// create a saved script
|
// create a saved script
|
||||||
body, headers := generateNewScriptMultipartRequest(t, nil,
|
body, headers := generateNewScriptMultipartRequest(t,
|
||||||
"myscript.sh", []byte(`echo "hello"`), s.token)
|
"myscript.sh", []byte(`echo "hello"`), s.token, nil)
|
||||||
s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusPaymentRequired, headers)
|
s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusPaymentRequired, headers)
|
||||||
|
|
||||||
// delete a saved script
|
// delete a saved script
|
||||||
@ -6160,9 +6160,9 @@ func (s *integrationTestSuite) TestSearchTargets() {
|
|||||||
|
|
||||||
hosts := s.createHosts(t)
|
hosts := s.createHosts(t)
|
||||||
|
|
||||||
lblIDs, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
|
lblMap, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, lblIDs, 1)
|
require.Len(t, lblMap, 1)
|
||||||
|
|
||||||
// no search criteria
|
// no search criteria
|
||||||
var searchResp searchTargetsResponse
|
var searchResp searchTargetsResponse
|
||||||
@ -6172,6 +6172,11 @@ func (s *integrationTestSuite) TestSearchTargets() {
|
|||||||
require.Len(t, searchResp.Targets.Labels, 1)
|
require.Len(t, searchResp.Targets.Labels, 1)
|
||||||
require.Len(t, searchResp.Targets.Teams, 0)
|
require.Len(t, searchResp.Targets.Teams, 0)
|
||||||
|
|
||||||
|
var lblIDs []uint
|
||||||
|
for _, labelID := range lblMap {
|
||||||
|
lblIDs = append(lblIDs, labelID)
|
||||||
|
}
|
||||||
|
|
||||||
searchResp = searchTargetsResponse{}
|
searchResp = searchTargetsResponse{}
|
||||||
s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{LabelIDs: lblIDs}}, http.StatusOK, &searchResp)
|
s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{LabelIDs: lblIDs}}, http.StatusOK, &searchResp)
|
||||||
require.Equal(t, uint(0), searchResp.TargetsCount)
|
require.Equal(t, uint(0), searchResp.TargetsCount)
|
||||||
@ -6272,12 +6277,12 @@ func (s *integrationTestSuite) TestCountTargets() {
|
|||||||
|
|
||||||
hosts := s.createHosts(t)
|
hosts := s.createHosts(t)
|
||||||
|
|
||||||
lblIDs, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
|
lblMap, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, lblIDs, 1)
|
require.Len(t, lblMap, 1)
|
||||||
|
|
||||||
for i := range hosts {
|
for i := range hosts {
|
||||||
err = s.ds.RecordLabelQueryExecutions(context.Background(), hosts[i], map[uint]*bool{lblIDs[0]: ptr.Bool(true)}, time.Now(), false)
|
err = s.ds.RecordLabelQueryExecutions(context.Background(), hosts[i], map[uint]*bool{lblMap["All Hosts"]: ptr.Bool(true)}, time.Now(), false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6299,6 +6304,10 @@ func (s *integrationTestSuite) TestCountTargets() {
|
|||||||
require.Equal(t, uint(0), countResp.TargetsOnline)
|
require.Equal(t, uint(0), countResp.TargetsOnline)
|
||||||
require.Equal(t, uint(0), countResp.TargetsOffline)
|
require.Equal(t, uint(0), countResp.TargetsOffline)
|
||||||
|
|
||||||
|
var lblIDs []uint
|
||||||
|
for _, labelID := range lblMap {
|
||||||
|
lblIDs = append(lblIDs, labelID)
|
||||||
|
}
|
||||||
// all hosts label selected
|
// all hosts label selected
|
||||||
countResp = countTargetsResponse{}
|
countResp = countTargetsResponse{}
|
||||||
s.DoJSON("POST", "/api/latest/fleet/targets/count", countTargetsRequest{Selected: fleet.HostTargets{LabelIDs: lblIDs}}, http.StatusOK, &countResp)
|
s.DoJSON("POST", "/api/latest/fleet/targets/count", countTargetsRequest{Selected: fleet.HostTargets{LabelIDs: lblIDs}}, http.StatusOK, &countResp)
|
||||||
@ -6933,7 +6942,7 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
|
|||||||
lids, err := s.ds.LabelIDsByName(context.Background(), []string{t.Name()})
|
lids, err := s.ds.LabelIDsByName(context.Background(), []string{t.Name()})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, lids, 1)
|
require.Len(t, lids, 1)
|
||||||
customLabelID := lids[0]
|
customLabelID := lids[t.Name()]
|
||||||
|
|
||||||
// create a policy and make host[1] fail that policy
|
// create a policy and make host[1] fail that policy
|
||||||
pol, err := s.ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{Name: t.Name(), Query: "SELECT 1"})
|
pol, err := s.ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{Name: t.Name(), Query: "SELECT 1"})
|
||||||
|
@ -148,7 +148,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||||||
// it did get marshalled, and then when unmarshalled it was set (but
|
// it did get marshalled, and then when unmarshalled it was set (but
|
||||||
// empty).
|
// empty).
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
},
|
},
|
||||||
}, team.Config.MDM)
|
}, team.Config.MDM)
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||||||
BootstrapPackage: optjson.String{Set: true},
|
BootstrapPackage: optjson.String{Set: true},
|
||||||
},
|
},
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
},
|
},
|
||||||
}, team.Config.MDM)
|
}, team.Config.MDM)
|
||||||
|
|
||||||
@ -227,7 +227,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||||||
BootstrapPackage: optjson.String{Set: true},
|
BootstrapPackage: optjson.String{Set: true},
|
||||||
},
|
},
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
},
|
},
|
||||||
}, getTmResp.Team.Config.MDM)
|
}, getTmResp.Team.Config.MDM)
|
||||||
|
|
||||||
@ -250,7 +250,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
|
|||||||
BootstrapPackage: optjson.String{Set: true},
|
BootstrapPackage: optjson.String{Set: true},
|
||||||
},
|
},
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
},
|
},
|
||||||
}, listTmResp.Teams[0].Config.MDM)
|
}, listTmResp.Teams[0].Config.MDM)
|
||||||
|
|
||||||
@ -1911,7 +1911,7 @@ func (s *integrationEnterpriseTestSuite) TestWindowsUpdatesTeamConfig() {
|
|||||||
BootstrapPackage: optjson.String{Set: true},
|
BootstrapPackage: optjson.String{Set: true},
|
||||||
},
|
},
|
||||||
WindowsSettings: fleet.WindowsSettings{
|
WindowsSettings: fleet.WindowsSettings{
|
||||||
CustomSettings: optjson.Slice[string]{Set: true, Value: []string{}},
|
CustomSettings: optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}},
|
||||||
},
|
},
|
||||||
}, getTmResp.Team.Config.MDM)
|
}, getTmResp.Team.Config.MDM)
|
||||||
|
|
||||||
@ -4998,8 +4998,8 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
|
|||||||
|
|
||||||
// create a saved script for no team
|
// create a saved script for no team
|
||||||
var newScriptResp createScriptResponse
|
var newScriptResp createScriptResponse
|
||||||
body, headers := generateNewScriptMultipartRequest(t, nil,
|
body, headers := generateNewScriptMultipartRequest(t,
|
||||||
"script1.sh", []byte(`echo "hello"`), s.token)
|
"script1.sh", []byte(`echo "hello"`), s.token, nil)
|
||||||
res := s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
res := s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
||||||
err := json.NewDecoder(res.Body).Decode(&newScriptResp)
|
err := json.NewDecoder(res.Body).Decode(&newScriptResp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -5031,50 +5031,50 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
|
|||||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/scripts/%d", noTeamScriptID+999), nil, http.StatusNotFound, &getScriptResp, "alt", "media")
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/scripts/%d", noTeamScriptID+999), nil, http.StatusNotFound, &getScriptResp, "alt", "media")
|
||||||
|
|
||||||
// file name is empty
|
// file name is empty
|
||||||
body, headers = generateNewScriptMultipartRequest(t, nil,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"", []byte(`echo "hello"`), s.token)
|
"", []byte(`echo "hello"`), s.token, nil)
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusBadRequest, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusBadRequest, headers)
|
||||||
errMsg := extractServerErrorText(res.Body)
|
errMsg := extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "no file headers for script")
|
require.Contains(t, errMsg, "no file headers for script")
|
||||||
|
|
||||||
// file name is not .sh
|
// file name is not .sh
|
||||||
body, headers = generateNewScriptMultipartRequest(t, nil,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"not_sh.txt", []byte(`echo "hello"`), s.token)
|
"not_sh.txt", []byte(`echo "hello"`), s.token, nil)
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "Validation Failed: File type not supported. Only .sh and .ps1 file type is allowed.")
|
require.Contains(t, errMsg, "Validation Failed: File type not supported. Only .sh and .ps1 file type is allowed.")
|
||||||
|
|
||||||
// file content is empty
|
// file content is empty
|
||||||
body, headers = generateNewScriptMultipartRequest(t, nil,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script2.sh", []byte(``), s.token)
|
"script2.sh", []byte(``), s.token, nil)
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "Script contents must not be empty")
|
require.Contains(t, errMsg, "Script contents must not be empty")
|
||||||
|
|
||||||
// file content is too large
|
// file content is too large
|
||||||
body, headers = generateNewScriptMultipartRequest(t, nil,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script2.sh", []byte(strings.Repeat("a", 10001)), s.token)
|
"script2.sh", []byte(strings.Repeat("a", 10001)), s.token, nil)
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "Script is too large. It's limited to 10,000 characters")
|
require.Contains(t, errMsg, "Script is too large. It's limited to 10,000 characters")
|
||||||
|
|
||||||
// invalid hashbang
|
// invalid hashbang
|
||||||
body, headers = generateNewScriptMultipartRequest(t, nil,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script2.sh", []byte(`#!/bin/python`), s.token)
|
"script2.sh", []byte(`#!/bin/python`), s.token, nil)
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "Interpreter not supported.")
|
require.Contains(t, errMsg, "Interpreter not supported.")
|
||||||
|
|
||||||
// script already exists with this name for this no-team
|
// script already exists with this name for this no-team
|
||||||
body, headers = generateNewScriptMultipartRequest(t, nil,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script1.sh", []byte(`echo "hello"`), s.token)
|
"script1.sh", []byte(`echo "hello"`), s.token, nil)
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusConflict, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusConflict, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "A script with this name already exists")
|
require.Contains(t, errMsg, "A script with this name already exists")
|
||||||
|
|
||||||
// team id does not exist
|
// team id does not exist
|
||||||
body, headers = generateNewScriptMultipartRequest(t, ptr.Uint(123),
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script1.sh", []byte(`echo "hello"`), s.token)
|
"script1.sh", []byte(`echo "hello"`), s.token, map[string][]string{"team_id": {"123"}})
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusNotFound, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusNotFound, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "The team does not exist.")
|
require.Contains(t, errMsg, "The team does not exist.")
|
||||||
@ -5084,8 +5084,8 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// create with existing name for this time for a team
|
// create with existing name for this time for a team
|
||||||
body, headers = generateNewScriptMultipartRequest(t, &tm.ID,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script1.sh", []byte(`echo "team"`), s.token)
|
"script1.sh", []byte(`echo "team"`), s.token, map[string][]string{"team_id": {fmt.Sprintf("%d", tm.ID)}})
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
||||||
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
|
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -5095,8 +5095,9 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
|
|||||||
s.lastActivityMatches("added_script", fmt.Sprintf(`{"script_name": %q, "team_name": %q, "team_id": %d}`, "script1.sh", tm.Name, tm.ID), 0)
|
s.lastActivityMatches("added_script", fmt.Sprintf(`{"script_name": %q, "team_name": %q, "team_id": %d}`, "script1.sh", tm.Name, tm.ID), 0)
|
||||||
|
|
||||||
// create a windows script
|
// create a windows script
|
||||||
body, headers = generateNewScriptMultipartRequest(t, &tm.ID,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script2.ps1", []byte(`Write-Host "Hello, World!"`), s.token)
|
"script2.ps1", []byte(`Write-Host "Hello, World!"`), s.token, map[string][]string{"team_id": {fmt.Sprintf("%d", tm.ID)}})
|
||||||
|
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
||||||
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
|
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -5124,15 +5125,17 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
|
|||||||
require.Equal(t, fmt.Sprintf("attachment;filename=\"%s %s\"", time.Now().Format(time.DateOnly), "script1.sh"), res.Header.Get("Content-Disposition"))
|
require.Equal(t, fmt.Sprintf("attachment;filename=\"%s %s\"", time.Now().Format(time.DateOnly), "script1.sh"), res.Header.Get("Content-Disposition"))
|
||||||
|
|
||||||
// script already exists with this name for this team
|
// script already exists with this name for this team
|
||||||
body, headers = generateNewScriptMultipartRequest(t, &tm.ID,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script1.sh", []byte(`echo "hello"`), s.token)
|
"script1.sh", []byte(`echo "hello"`), s.token, map[string][]string{"team_id": {fmt.Sprintf("%d", tm.ID)}})
|
||||||
|
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusConflict, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusConflict, headers)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
errMsg = extractServerErrorText(res.Body)
|
||||||
require.Contains(t, errMsg, "A script with this name already exists")
|
require.Contains(t, errMsg, "A script with this name already exists")
|
||||||
|
|
||||||
// create with a different name for this team
|
// create with a different name for this team
|
||||||
body, headers = generateNewScriptMultipartRequest(t, &tm.ID,
|
body, headers = generateNewScriptMultipartRequest(t,
|
||||||
"script2.sh", []byte(`echo "hello"`), s.token)
|
"script2.sh", []byte(`echo "hello"`), s.token, map[string][]string{"team_id": {fmt.Sprintf("%d", tm.ID)}})
|
||||||
|
|
||||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
|
||||||
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
|
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -5657,10 +5660,10 @@ VALUES
|
|||||||
|
|
||||||
// generates the body and headers part of a multipart request ready to be
|
// generates the body and headers part of a multipart request ready to be
|
||||||
// used via s.DoRawWithHeaders to POST /api/_version_/fleet/scripts.
|
// used via s.DoRawWithHeaders to POST /api/_version_/fleet/scripts.
|
||||||
func generateNewScriptMultipartRequest(t *testing.T, tmID *uint,
|
func generateNewScriptMultipartRequest(t *testing.T,
|
||||||
fileName string, fileContent []byte, token string,
|
fileName string, fileContent []byte, token string, extraFields map[string][]string,
|
||||||
) (*bytes.Buffer, map[string]string) {
|
) (*bytes.Buffer, map[string]string) {
|
||||||
return generateMultipartRequest(t, tmID, "script", fileName, fileContent, token)
|
return generateMultipartRequest(t, "script", fileName, fileContent, token, extraFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *integrationEnterpriseTestSuite) TestAppConfigScripts() {
|
func (s *integrationEnterpriseTestSuite) TestAppConfigScripts() {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,9 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -1152,10 +1154,7 @@ func (svc *Service) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
|
|||||||
// returns the numeric Apple profile ID and true if it is an Apple identifier,
|
// returns the numeric Apple profile ID and true if it is an Apple identifier,
|
||||||
// or 0 and false otherwise.
|
// or 0 and false otherwise.
|
||||||
func isAppleProfileUUID(profileUUID string) bool {
|
func isAppleProfileUUID(profileUUID string) bool {
|
||||||
if strings.HasPrefix(profileUUID, "a") {
|
return strings.HasPrefix(profileUUID, "a")
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -1165,6 +1164,7 @@ func isAppleProfileUUID(profileUUID string) bool {
|
|||||||
type newMDMConfigProfileRequest struct {
|
type newMDMConfigProfileRequest struct {
|
||||||
TeamID uint
|
TeamID uint
|
||||||
Profile *multipart.FileHeader
|
Profile *multipart.FileHeader
|
||||||
|
Labels []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||||
@ -1178,6 +1178,7 @@ func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add team_id
|
||||||
val, ok := r.MultipartForm.Value["team_id"]
|
val, ok := r.MultipartForm.Value["team_id"]
|
||||||
if !ok || len(val) < 1 {
|
if !ok || len(val) < 1 {
|
||||||
// default is no team
|
// default is no team
|
||||||
@ -1190,12 +1191,16 @@ func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Req
|
|||||||
decoded.TeamID = uint(teamID)
|
decoded.TeamID = uint(teamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add profile
|
||||||
fhs, ok := r.MultipartForm.File["profile"]
|
fhs, ok := r.MultipartForm.File["profile"]
|
||||||
if !ok || len(fhs) < 1 {
|
if !ok || len(fhs) < 1 {
|
||||||
return nil, &fleet.BadRequestError{Message: "no file headers for profile"}
|
return nil, &fleet.BadRequestError{Message: "no file headers for profile"}
|
||||||
}
|
}
|
||||||
decoded.Profile = fhs[0]
|
decoded.Profile = fhs[0]
|
||||||
|
|
||||||
|
// add labels
|
||||||
|
decoded.Labels = r.MultipartForm.Value["labels"]
|
||||||
|
|
||||||
return &decoded, nil
|
return &decoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1217,7 +1222,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||||||
|
|
||||||
fileExt := filepath.Ext(req.Profile.Filename)
|
fileExt := filepath.Ext(req.Profile.Filename)
|
||||||
if isApple := strings.EqualFold(fileExt, ".mobileconfig"); isApple {
|
if isApple := strings.EqualFold(fileExt, ".mobileconfig"); isApple {
|
||||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff)
|
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, req.Labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||||
}
|
}
|
||||||
@ -1228,7 +1233,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
|||||||
|
|
||||||
if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows {
|
if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows {
|
||||||
profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt)
|
profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt)
|
||||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff)
|
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, req.Labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||||
}
|
}
|
||||||
@ -1252,7 +1257,7 @@ func (svc *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID u
|
|||||||
return &fleet.BadRequestError{Message: "Couldn't upload. The file should be a .mobileconfig or .xml file."}
|
return &fleet.BadRequestError{Message: "Couldn't upload. The file should be a .mobileconfig or .xml file."}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader) (*fleet.MDMWindowsConfigProfile, error) {
|
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string) (*fleet.MDMWindowsConfigProfile, error) {
|
||||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx, err)
|
return nil, ctxerr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
@ -1297,6 +1302,12 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
|||||||
return nil, ctxerr.Wrap(ctx, err, "validate profile")
|
return nil, ctxerr.Wrap(ctx, err, "validate profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labelMap, err := svc.validateProfileLabels(ctx, labels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||||
|
}
|
||||||
|
cp.Labels = labelMap
|
||||||
|
|
||||||
newCP, err := svc.ds.NewMDMWindowsConfigProfile(ctx, cp)
|
newCP, err := svc.ds.NewMDMWindowsConfigProfile(ctx, cp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var existsErr existsErrorInterface
|
var existsErr existsErrorInterface
|
||||||
@ -1330,15 +1341,92 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
|||||||
return newCP, nil
|
return newCP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *Service) batchValidateProfileLabels(ctx context.Context, labelNames []string) (map[string]fleet.ConfigurationProfileLabel, error) {
|
||||||
|
if len(labelNames) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, err := svc.ds.LabelIDsByName(ctx, labelNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "getting label IDs by name")
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueNames := make(map[string]bool)
|
||||||
|
for _, entry := range labelNames {
|
||||||
|
if _, value := uniqueNames[entry]; !value {
|
||||||
|
uniqueNames[entry] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(labels) != len(uniqueNames) {
|
||||||
|
return nil, &fleet.BadRequestError{
|
||||||
|
Message: "some or all the labels provided don't exist",
|
||||||
|
InternalErr: fmt.Errorf("names provided: %v", labelNames),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profLabels := make(map[string]fleet.ConfigurationProfileLabel)
|
||||||
|
for labelName, labelID := range labels {
|
||||||
|
profLabels[labelName] = fleet.ConfigurationProfileLabel{
|
||||||
|
LabelName: labelName,
|
||||||
|
LabelID: labelID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profLabels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) validateProfileLabels(ctx context.Context, labelNames []string) ([]fleet.ConfigurationProfileLabel, error) {
|
||||||
|
labelMap, err := svc.batchValidateProfileLabels(ctx, labelNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "validating profile labels")
|
||||||
|
}
|
||||||
|
|
||||||
|
var profLabels []fleet.ConfigurationProfileLabel
|
||||||
|
for _, label := range labelMap {
|
||||||
|
profLabels = append(profLabels, label)
|
||||||
|
}
|
||||||
|
return profLabels, nil
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Batch Replace MDM Profiles
|
// Batch Replace MDM Profiles
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
type batchSetMDMProfilesRequest struct {
|
type batchSetMDMProfilesRequest struct {
|
||||||
TeamID *uint `json:"-" query:"team_id,optional"`
|
TeamID *uint `json:"-" query:"team_id,optional"`
|
||||||
TeamName *string `json:"-" query:"team_name,optional"`
|
TeamName *string `json:"-" query:"team_name,optional"`
|
||||||
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
|
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
|
||||||
Profiles map[string][]byte `json:"profiles"`
|
Profiles backwardsCompatProfilesParam `json:"profiles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type backwardsCompatProfilesParam []fleet.MDMProfileBatchPayload
|
||||||
|
|
||||||
|
func (bcp *backwardsCompatProfilesParam) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if lookAhead := bytes.TrimSpace(data); len(lookAhead) > 0 && lookAhead[0] == '[' {
|
||||||
|
// use []fleet.MDMProfileBatchPayload to prevent infinite recursion if we
|
||||||
|
// use `backwardsCompatProfileSlice`
|
||||||
|
var profs []fleet.MDMProfileBatchPayload
|
||||||
|
if err := json.Unmarshal(data, &profs); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal profile spec. Error using new format: %w", err)
|
||||||
|
}
|
||||||
|
*bcp = profs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var backwardsCompat map[string][]byte
|
||||||
|
if err := json.Unmarshal(data, &backwardsCompat); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal profile spec. Error using old format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*bcp = make(backwardsCompatProfilesParam, 0, len(backwardsCompat))
|
||||||
|
for name, contents := range backwardsCompat {
|
||||||
|
*bcp = append(*bcp, fleet.MDMProfileBatchPayload{Name: name, Contents: contents})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type batchSetMDMProfilesResponse struct {
|
type batchSetMDMProfilesResponse struct {
|
||||||
@ -1357,7 +1445,7 @@ func batchSetMDMProfilesEndpoint(ctx context.Context, request interface{}, svc f
|
|||||||
return batchSetMDMProfilesResponse{}, nil
|
return batchSetMDMProfilesResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) BatchSetMDMProfiles(ctx context.Context, tmID *uint, tmName *string, profiles map[string][]byte, dryRun, skipBulkPending bool) error {
|
func (svc *Service) BatchSetMDMProfiles(ctx context.Context, tmID *uint, tmName *string, profiles []fleet.MDMProfileBatchPayload, dryRun, skipBulkPending bool) error {
|
||||||
var err error
|
var err error
|
||||||
if tmID, tmName, err = svc.authorizeBatchProfiles(ctx, tmID, tmName); err != nil {
|
if tmID, tmName, err = svc.authorizeBatchProfiles(ctx, tmID, tmName); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1372,12 +1460,21 @@ func (svc *Service) BatchSetMDMProfiles(ctx context.Context, tmID *uint, tmName
|
|||||||
return ctxerr.Wrap(ctx, err, "validating profiles")
|
return ctxerr.Wrap(ctx, err, "validating profiles")
|
||||||
}
|
}
|
||||||
|
|
||||||
appleProfiles, err := getAppleProfiles(ctx, tmID, appCfg, profiles)
|
labels := []string{}
|
||||||
|
for _, prof := range profiles {
|
||||||
|
labels = append(labels, prof.Labels...)
|
||||||
|
}
|
||||||
|
labelMap, err := svc.batchValidateProfileLabels(ctx, labels)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "validating labels")
|
||||||
|
}
|
||||||
|
|
||||||
|
appleProfiles, err := getAppleProfiles(ctx, tmID, appCfg, profiles, labelMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "validating macOS profiles")
|
return ctxerr.Wrap(ctx, err, "validating macOS profiles")
|
||||||
}
|
}
|
||||||
|
|
||||||
windowsProfiles, err := getWindowsProfiles(ctx, tmID, appCfg, profiles)
|
windowsProfiles, err := getWindowsProfiles(ctx, tmID, appCfg, profiles, labelMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctxerr.Wrap(ctx, err, "validating Windows profiles")
|
return ctxerr.Wrap(ctx, err, "validating Windows profiles")
|
||||||
}
|
}
|
||||||
@ -1466,42 +1563,54 @@ func (svc *Service) authorizeBatchProfiles(ctx context.Context, tmID *uint, tmNa
|
|||||||
return tmID, tmName, nil
|
return tmID, tmName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAppleProfiles(ctx context.Context, tmID *uint, appCfg *fleet.AppConfig, profiles map[string][]byte) ([]*fleet.MDMAppleConfigProfile, error) {
|
func getAppleProfiles(
|
||||||
|
ctx context.Context,
|
||||||
|
tmID *uint,
|
||||||
|
appCfg *fleet.AppConfig,
|
||||||
|
profiles []fleet.MDMProfileBatchPayload,
|
||||||
|
labelMap map[string]fleet.ConfigurationProfileLabel,
|
||||||
|
) ([]*fleet.MDMAppleConfigProfile, error) {
|
||||||
// any duplicate identifier or name in the provided set results in an error
|
// any duplicate identifier or name in the provided set results in an error
|
||||||
profs := make([]*fleet.MDMAppleConfigProfile, 0, len(profiles))
|
profs := make([]*fleet.MDMAppleConfigProfile, 0, len(profiles))
|
||||||
byName, byIdent := make(map[string]bool, len(profiles)), make(map[string]bool, len(profiles))
|
byName, byIdent := make(map[string]bool, len(profiles)), make(map[string]bool, len(profiles))
|
||||||
for i, prof := range profiles {
|
for _, prof := range profiles {
|
||||||
if mdm.GetRawProfilePlatform(prof) != "darwin" {
|
if mdm.GetRawProfilePlatform(prof.Contents) != "darwin" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mdmProf, err := fleet.NewMDMAppleConfigProfile(prof, tmID)
|
mdmProf, err := fleet.NewMDMAppleConfigProfile(prof.Contents, tmID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx,
|
return nil, ctxerr.Wrap(ctx,
|
||||||
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", i), err.Error()),
|
fleet.NewInvalidArgumentError(prof.Name, err.Error()),
|
||||||
"invalid mobileconfig profile")
|
"invalid mobileconfig profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, labelName := range prof.Labels {
|
||||||
|
if lbl, ok := labelMap[labelName]; ok {
|
||||||
|
mdmProf.Labels = append(mdmProf.Labels, lbl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := mdmProf.ValidateUserProvided(); err != nil {
|
if err := mdmProf.ValidateUserProvided(); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx,
|
return nil, ctxerr.Wrap(ctx,
|
||||||
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", i), err.Error()))
|
fleet.NewInvalidArgumentError(prof.Name, err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if mdmProf.Name != i {
|
if mdmProf.Name != prof.Name {
|
||||||
return nil, ctxerr.Wrap(ctx,
|
return nil, ctxerr.Wrap(ctx,
|
||||||
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", i), fmt.Sprintf("Couldn’t edit custom_settings. The name provided for the profile must match the profile PayloadDisplayName: %q", mdmProf.Name)),
|
fleet.NewInvalidArgumentError(prof.Name, fmt.Sprintf("Couldn’t edit custom_settings. The name provided for the profile must match the profile PayloadDisplayName: %q", mdmProf.Name)),
|
||||||
"duplicate mobileconfig profile by name")
|
"duplicate mobileconfig profile by name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if byName[mdmProf.Name] {
|
if byName[mdmProf.Name] {
|
||||||
return nil, ctxerr.Wrap(ctx,
|
return nil, ctxerr.Wrap(ctx,
|
||||||
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", i), fmt.Sprintf("Couldn’t edit custom_settings. More than one configuration profile have the same name (PayloadDisplayName): %q", mdmProf.Name)),
|
fleet.NewInvalidArgumentError(prof.Name, fmt.Sprintf("Couldn’t edit custom_settings. More than one configuration profile have the same name (PayloadDisplayName): %q", mdmProf.Name)),
|
||||||
"duplicate mobileconfig profile by name")
|
"duplicate mobileconfig profile by name")
|
||||||
}
|
}
|
||||||
byName[mdmProf.Name] = true
|
byName[mdmProf.Name] = true
|
||||||
|
|
||||||
if byIdent[mdmProf.Identifier] {
|
if byIdent[mdmProf.Identifier] {
|
||||||
return nil, ctxerr.Wrap(ctx,
|
return nil, ctxerr.Wrap(ctx,
|
||||||
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", i), fmt.Sprintf("Couldn’t edit custom_settings. More than one configuration profile have the same identifier (PayloadIdentifier): %q", mdmProf.Identifier)),
|
fleet.NewInvalidArgumentError(prof.Name, fmt.Sprintf("Couldn’t edit custom_settings. More than one configuration profile have the same identifier (PayloadIdentifier): %q", mdmProf.Identifier)),
|
||||||
"duplicate mobileconfig profile by identifier")
|
"duplicate mobileconfig profile by identifier")
|
||||||
}
|
}
|
||||||
byIdent[mdmProf.Identifier] = true
|
byIdent[mdmProf.Identifier] = true
|
||||||
@ -1525,23 +1634,34 @@ func getAppleProfiles(ctx context.Context, tmID *uint, appCfg *fleet.AppConfig,
|
|||||||
return profs, nil
|
return profs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWindowsProfiles(ctx context.Context, tmID *uint, appCfg *fleet.AppConfig, profiles map[string][]byte) ([]*fleet.MDMWindowsConfigProfile, error) {
|
func getWindowsProfiles(
|
||||||
|
ctx context.Context,
|
||||||
|
tmID *uint,
|
||||||
|
appCfg *fleet.AppConfig,
|
||||||
|
profiles []fleet.MDMProfileBatchPayload,
|
||||||
|
labelMap map[string]fleet.ConfigurationProfileLabel,
|
||||||
|
) ([]*fleet.MDMWindowsConfigProfile, error) {
|
||||||
profs := make([]*fleet.MDMWindowsConfigProfile, 0, len(profiles))
|
profs := make([]*fleet.MDMWindowsConfigProfile, 0, len(profiles))
|
||||||
|
|
||||||
for name, syncML := range profiles {
|
for _, profile := range profiles {
|
||||||
if mdm.GetRawProfilePlatform(syncML) != "windows" {
|
if mdm.GetRawProfilePlatform(profile.Contents) != "windows" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mdmProf := &fleet.MDMWindowsConfigProfile{
|
mdmProf := &fleet.MDMWindowsConfigProfile{
|
||||||
TeamID: tmID,
|
TeamID: tmID,
|
||||||
Name: name,
|
Name: profile.Name,
|
||||||
SyncML: syncML,
|
SyncML: profile.Contents,
|
||||||
|
}
|
||||||
|
for _, labelName := range profile.Labels {
|
||||||
|
if lbl, ok := labelMap[labelName]; ok {
|
||||||
|
mdmProf.Labels = append(mdmProf.Labels, lbl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mdmProf.ValidateUserProvided(); err != nil {
|
if err := mdmProf.ValidateUserProvided(); err != nil {
|
||||||
return nil, ctxerr.Wrap(ctx,
|
return nil, ctxerr.Wrap(ctx,
|
||||||
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", name), err.Error()))
|
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", profile.Name), err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
profs = append(profs, mdmProf)
|
profs = append(profs, mdmProf)
|
||||||
@ -1563,9 +1683,9 @@ func getWindowsProfiles(ctx context.Context, tmID *uint, appCfg *fleet.AppConfig
|
|||||||
return profs, nil
|
return profs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateProfiles(profiles map[string][]byte) error {
|
func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error {
|
||||||
for _, rawBytes := range profiles {
|
for _, profile := range profiles {
|
||||||
platform := mdm.GetRawProfilePlatform(rawBytes)
|
platform := mdm.GetRawProfilePlatform(profile.Contents)
|
||||||
if platform != "darwin" && platform != "windows" {
|
if platform != "darwin" && platform != "windows" {
|
||||||
// TODO(roberto): there's ongoing feedback with Marko about improving this message, as it's too windows specific
|
// TODO(roberto): there's ongoing feedback with Marko about improving this message, as it's too windows specific
|
||||||
return fleet.NewInvalidArgumentError("mdm", "Only <Replace> supported as a top level element. Make sure you don’t have other top level elements.")
|
return fleet.NewInvalidArgumentError("mdm", "Only <Replace> supported as a top level element. Make sure you don’t have other top level elements.")
|
||||||
|
@ -971,11 +971,11 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
|
|||||||
checkShouldFail(t, err, tt.shouldFailTeamRead)
|
checkShouldFail(t, err, tt.shouldFailTeamRead)
|
||||||
|
|
||||||
// test authz create new profile (no team)
|
// test authz create new profile (no team)
|
||||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent))
|
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", strings.NewReader(winProfContent), nil)
|
||||||
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
|
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
|
||||||
|
|
||||||
// test authz create new profile (team 1)
|
// test authz create new profile (team 1)
|
||||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent))
|
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", strings.NewReader(winProfContent), nil)
|
||||||
checkShouldFail(t, err, tt.shouldFailTeamWrite)
|
checkShouldFail(t, err, tt.shouldFailTeamWrite)
|
||||||
|
|
||||||
// test authz delete config profile (no team)
|
// test authz delete config profile (no team)
|
||||||
@ -1057,7 +1057,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile))
|
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", strings.NewReader(c.profile), nil)
|
||||||
if c.wantErr != "" {
|
if c.wantErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.ErrorContains(t, err, c.wantErr)
|
require.ErrorContains(t, err, c.wantErr)
|
||||||
@ -1109,7 +1109,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
premium bool
|
premium bool
|
||||||
teamID *uint
|
teamID *uint
|
||||||
teamName *string
|
teamName *string
|
||||||
profiles map[string][]byte
|
profiles []fleet.MDMProfileBatchPayload
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -1271,9 +1271,9 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
ptr.Uint(1),
|
ptr.Uint(1),
|
||||||
nil,
|
nil,
|
||||||
map[string][]byte{
|
[]fleet.MDMProfileBatchPayload{
|
||||||
"N1": mobileconfigForTest("N1", "I1"),
|
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
|
||||||
"N2": mobileconfigForTest("N1", "I2"),
|
{Name: "N2", Contents: mobileconfigForTest("N1", "I2")},
|
||||||
},
|
},
|
||||||
`The name provided for the profile must match the profile PayloadDisplayName: "N1"`,
|
`The name provided for the profile must match the profile PayloadDisplayName: "N1"`,
|
||||||
},
|
},
|
||||||
@ -1283,10 +1283,10 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
ptr.Uint(1),
|
ptr.Uint(1),
|
||||||
nil,
|
nil,
|
||||||
map[string][]byte{
|
[]fleet.MDMProfileBatchPayload{
|
||||||
"N1": mobileconfigForTest("N1", "I1"),
|
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
|
||||||
"N2": mobileconfigForTest("N2", "I2"),
|
{Name: "N2", Contents: mobileconfigForTest("N2", "I2")},
|
||||||
"N3": mobileconfigForTest("N3", "I1"),
|
{Name: "N3", Contents: mobileconfigForTest("N3", "I1")},
|
||||||
},
|
},
|
||||||
`More than one configuration profile have the same identifier (PayloadIdentifier): "I1"`,
|
`More than one configuration profile have the same identifier (PayloadIdentifier): "I1"`,
|
||||||
},
|
},
|
||||||
@ -1296,10 +1296,10 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string][]byte{
|
[]fleet.MDMProfileBatchPayload{
|
||||||
"N1": mobileconfigForTest("N1", "I1"),
|
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
|
||||||
"N2": mobileconfigForTest("N2", "I2"),
|
{Name: "N2", Contents: mobileconfigForTest("N2", "I2")},
|
||||||
"N3": mobileconfigForTest("N3", "I3"),
|
{Name: "N3", Contents: mobileconfigForTest("N3", "I3")},
|
||||||
},
|
},
|
||||||
``,
|
``,
|
||||||
},
|
},
|
||||||
@ -1309,13 +1309,13 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string][]byte{
|
[]fleet.MDMProfileBatchPayload{
|
||||||
"N1": syncMLForTest("./foo/bar"),
|
{Name: "N1", Contents: syncMLForTest("./foo/bar")},
|
||||||
"N2": syncMLForTest("./baz"),
|
{Name: "N2", Contents: syncMLForTest("./baz")},
|
||||||
"N3": syncMLForTest("./zab"),
|
{Name: "N3", Contents: syncMLForTest("./zab")},
|
||||||
"N4": mobileconfigForTest("N4", "I1"),
|
{Name: "N4", Contents: mobileconfigForTest("N4", "I1")},
|
||||||
"N5": mobileconfigForTest("N5", "I2"),
|
{Name: "N5", Contents: mobileconfigForTest("N5", "I2")},
|
||||||
"N6": mobileconfigForTest("N6", "I3"),
|
{Name: "N6", Contents: mobileconfigForTest("N6", "I3")},
|
||||||
},
|
},
|
||||||
``,
|
``,
|
||||||
},
|
},
|
||||||
@ -1325,10 +1325,10 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string][]byte{
|
[]fleet.MDMProfileBatchPayload{
|
||||||
"N1": syncMLForTest("./foo/bar"),
|
{Name: "N1", Contents: syncMLForTest("./foo/bar")},
|
||||||
"N2": syncMLForTest("./baz"),
|
{Name: "N2", Contents: syncMLForTest("./baz")},
|
||||||
"N3": syncMLForTest("./zab"),
|
{Name: "N3", Contents: syncMLForTest("./zab")},
|
||||||
},
|
},
|
||||||
``,
|
``,
|
||||||
},
|
},
|
||||||
@ -1338,8 +1338,8 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string][]byte{
|
[]fleet.MDMProfileBatchPayload{
|
||||||
"foo": []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
{Name: "foo", Contents: []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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
@ -1372,7 +1372,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>`),
|
</plist>`),
|
||||||
},
|
}},
|
||||||
"unsupported PayloadType(s)",
|
"unsupported PayloadType(s)",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1405,42 +1405,42 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||||||
func TestValidateProfiles(t *testing.T) {
|
func TestValidateProfiles(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
profiles map[string][]byte
|
profiles []fleet.MDMProfileBatchPayload
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid Darwin Profile",
|
name: "Valid Darwin Profile",
|
||||||
profiles: map[string][]byte{
|
profiles: []fleet.MDMProfileBatchPayload{
|
||||||
"darwinProfile": []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"),
|
{Name: "darwinProfile", Contents: []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid Windows Profile",
|
name: "Valid Windows Profile",
|
||||||
profiles: map[string][]byte{
|
profiles: []fleet.MDMProfileBatchPayload{
|
||||||
"windowsProfile": []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>"),
|
{Name: "windowsProfile", Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid Profile",
|
name: "Invalid Profile",
|
||||||
profiles: map[string][]byte{
|
profiles: []fleet.MDMProfileBatchPayload{
|
||||||
"invalidProfile": []byte("invalid data"),
|
{Name: "invalidProfile", Contents: []byte("invalid data")},
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Mixed Valid and Invalid Profiles",
|
name: "Mixed Valid and Invalid Profiles",
|
||||||
profiles: map[string][]byte{
|
profiles: []fleet.MDMProfileBatchPayload{
|
||||||
"validProfile": []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"),
|
{Name: "validProfile", Contents: []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")},
|
||||||
"invalidProfile": []byte("invalid data"),
|
{Name: "invalidProfile", Contents: []byte("invalid data")},
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty Profile",
|
name: "Empty Profile",
|
||||||
profiles: map[string][]byte{
|
profiles: []fleet.MDMProfileBatchPayload{
|
||||||
"emptyProfile": []byte(""),
|
{Name: "emptyProfile", Contents: []byte("")},
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@ -1457,3 +1457,65 @@ func TestValidateProfiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackwardsCompatProfilesParamUnmarshalJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
expect backwardsCompatProfilesParam
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: []byte(""),
|
||||||
|
expect: nil,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new format",
|
||||||
|
input: []byte(`[{"name": "profile1", "contents": "Zm9vCg=="}, {"name": "profile2", "contents": "YmFyCg=="}]`),
|
||||||
|
expect: backwardsCompatProfilesParam{
|
||||||
|
{Name: "profile1", Contents: []byte("foo\n")},
|
||||||
|
{Name: "profile2", Contents: []byte("bar\n")},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new format with labels",
|
||||||
|
input: []byte(`[{"name": "profile1", "contents": "Zm9vCg==", "labels": ["foo", "bar"]}, {"name": "profile2", "contents": "YmFyCg=="}]`),
|
||||||
|
expect: backwardsCompatProfilesParam{
|
||||||
|
{Name: "profile1", Contents: []byte("foo\n"), Labels: []string{"foo", "bar"}},
|
||||||
|
{Name: "profile2", Contents: []byte("bar\n")},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old format",
|
||||||
|
input: []byte(`{"profile1": "Zm9vCg==", "profile2": "YmFyCg=="}`),
|
||||||
|
expect: backwardsCompatProfilesParam{
|
||||||
|
{Name: "profile1", Contents: []byte("foo\n")},
|
||||||
|
{Name: "profile2", Contents: []byte("bar\n")},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
input: []byte(`{invalid json}`),
|
||||||
|
expect: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var bcp backwardsCompatProfilesParam
|
||||||
|
err := bcp.UnmarshalJSON(tc.input)
|
||||||
|
if tc.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, tc.expect, bcp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -41,7 +41,7 @@ func translateLabelToID(ctx context.Context, ds fleet.Datastore, identifier stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return labelIDs[0], nil
|
return labelIDs[identifier], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateTeamToID(ctx context.Context, ds fleet.Datastore, identifier string) (uint, error) {
|
func translateTeamToID(ctx context.Context, ds fleet.Datastore, identifier string) (uint, error) {
|
||||||
|
@ -160,29 +160,3 @@ func RemoveDuplicatesFromSlice[T comparable](slice []T) []T {
|
|||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
// SliceStringsMatch checks if two slices contain the same string elements,
|
|
||||||
// regardless of order.
|
|
||||||
func SliceStringsMatch(a, b []string) bool {
|
|
||||||
if len(a) != len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a map to count occurrences of elements in a
|
|
||||||
elementCount := make(map[string]int, len(a))
|
|
||||||
for _, item := range a {
|
|
||||||
elementCount[item]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrease the count for each element in b
|
|
||||||
for _, item := range b {
|
|
||||||
elementCount[item]--
|
|
||||||
if elementCount[item] < 0 {
|
|
||||||
// if the count goes below zero, b has an element not
|
|
||||||
// in a or more occurrences of it than a
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
@ -150,28 +150,3 @@ func TestRemoveDuplicatesFromSlice(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSliceStringsMatch(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
a, b []string
|
|
||||||
want bool
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{[]string{"foo", "bar"}, []string{"bar", "foo"}, true, "same elements in different order"},
|
|
||||||
{[]string{"foo", "bar"}, []string{"foo", "bar"}, true, "same elements in same order"},
|
|
||||||
{[]string{"foo", "bar"}, []string{"bar", "bar"}, false, "different number of same elements"},
|
|
||||||
{[]string{"foo", "foo", "bar"}, []string{"bar", "foo", "foo"}, true, "both have duplicates"},
|
|
||||||
{[]string{"foo", "bar", "bar"}, []string{"bar", "foo", "foo"}, false, "both have duplicates but elements don't match"},
|
|
||||||
{[]string{"foo", "bar"}, []string{"foo"}, false, "different lengths"},
|
|
||||||
{[]string{}, []string{}, true, "both slices empty"},
|
|
||||||
{[]string{"foo"}, []string{}, false, "one slice empty"},
|
|
||||||
{[]string{"unique"}, []string{"unique", "unique"}, false, "duplicate in one slice"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
got := SliceStringsMatch(tc.a, tc.b)
|
|
||||||
require.Equal(t, tc.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -102,7 +102,9 @@ github.com/fleetdm/fleet/v4/pkg/optjson/Int Valid bool
|
|||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Int Value int
|
github.com/fleetdm/fleet/v4/pkg/optjson/Int Value int
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.Int
|
github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.Int
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSettings fleet.MacOSSettings
|
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSettings fleet.MacOSSettings
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []string
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||||
|
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||||
|
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSetup fleet.MacOSSetup
|
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSetup fleet.MacOSSetup
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
||||||
@ -120,10 +122,13 @@ github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
|
|||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
|
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
|
github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MDM WindowsSettings fleet.WindowsSettings
|
github.com/fleetdm/fleet/v4/server/fleet/MDM WindowsSettings fleet.WindowsSettings
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[string]
|
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec]
|
||||||
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Set bool
|
||||||
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Valid bool
|
||||||
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Value []fleet.MDMProfileSpec
|
||||||
|
github.com/fleetdm/fleet/v4/server/fleet/AppConfig Scripts optjson.Slice[string]
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Set bool
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Set bool
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Valid bool
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Valid bool
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Value []string
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Value []string
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig Scripts optjson.Slice[string]
|
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig strictDecoding bool
|
github.com/fleetdm/fleet/v4/server/fleet/AppConfig strictDecoding bool
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig didUnmarshalLegacySettings []string
|
github.com/fleetdm/fleet/v4/server/fleet/AppConfig didUnmarshalLegacySettings []string
|
||||||
|
2
tools/cloner-check/generated_files/mdmprofilespec.txt
Normal file
2
tools/cloner-check/generated_files/mdmprofilespec.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||||
|
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
@ -12,14 +12,16 @@ github.com/fleetdm/fleet/v4/pkg/optjson/Int Valid bool
|
|||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Int Value int
|
github.com/fleetdm/fleet/v4/pkg/optjson/Int Value int
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.Int
|
github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.Int
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSettings fleet.MacOSSettings
|
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSettings fleet.MacOSSettings
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []string
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||||
|
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||||
|
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings DeprecatedEnableDiskEncryption *bool
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSetup fleet.MacOSSetup
|
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSetup fleet.MacOSSetup
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.String
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableEndUserAuthentication bool
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableEndUserAuthentication bool
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup MacOSSetupAssistant optjson.String
|
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup MacOSSetupAssistant optjson.String
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM WindowsSettings fleet.WindowsSettings
|
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM WindowsSettings fleet.WindowsSettings
|
||||||
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[string]
|
github.com/fleetdm/fleet/v4/server/fleet/WindowsSettings CustomSettings optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec]
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Set bool
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Set bool
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Valid bool
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Valid bool
|
||||||
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Value []string
|
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMProfileSpec] Value []fleet.MDMProfileSpec
|
||||||
|
@ -47,6 +47,7 @@ var cacheableItems = []fleet.Cloner{
|
|||||||
&fleet.Features{},
|
&fleet.Features{},
|
||||||
&fleet.TeamMDM{},
|
&fleet.TeamMDM{},
|
||||||
&fleet.Query{},
|
&fleet.Query{},
|
||||||
|
&fleet.MDMProfileSpec{},
|
||||||
// TeamAgentOptions is not in the list because it is a json.RawMessage, no fields can change.
|
// TeamAgentOptions is not in the list because it is a json.RawMessage, no fields can change.
|
||||||
// Same for ResultCountForQuery, it's just an int.
|
// Same for ResultCountForQuery, it's just an int.
|
||||||
}
|
}
|
||||||
|
@ -240,8 +240,296 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var profiles = map[string][]byte{
|
var profiles = []fleet.MDMProfileBatchPayload{
|
||||||
"Disable Bluetooth sharing": []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
{
|
||||||
|
Name: "Ensure Install Security Responses and System Files Is Enabled",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.SoftwareUpdate</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.6.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>0D8F676A-A705-4F57-8FF8-3118360EFDEB</string>
|
||||||
|
<key>ConfigDataInstall</key>
|
||||||
|
<true/>
|
||||||
|
<key>CriticalUpdateInstall</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Install Security Responses and System Files Is Enabled</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.6</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>EBEE9B81-9D33-477F-AFBE-9691360B7A74</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ensure Software Update Deferment Is Less Than or Equal to 30 Days",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.applicationaccess</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.7.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>123FD592-D1C3-41FD-BC41-F91F3E1E2CF4</string>
|
||||||
|
<key>enforcedSoftwareUpdateDelay</key>
|
||||||
|
<integer>29</integer>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Software Update Deferment Is Less Than or Equal to 30 Days</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.zwass.cis-1.7</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>385A0C13-2472-41B3-851C-1311FA12EB49</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ensure Auto Update Is Enabled",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.SoftwareUpdate</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.2.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>4DC539B5-837E-4DC3-B60B-43A8C556A8F0</string>
|
||||||
|
<key>AutomaticCheckEnabled</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Auto Update Is Enabled</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.2</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>03E69A02-02CE-4CA0-8F17-3BAAD5D3852F</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ensure Download New Updates When Available Is Enabled",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.SoftwareUpdate</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.3.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>5FDE6D58-79CD-447A-AFB0-BA32D889C396</string>
|
||||||
|
<key>AutomaticDownload</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Download New Updates When Available Is Enabled</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.3</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>0A1C2F97-D6FA-4CDB-ABB6-47DF2B151F4F</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ensure Install of macOS Updates Is Enabled",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.SoftwareUpdate</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.4.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>15BF7634-276A-411B-8C4E-52D89B4ED82C</string>
|
||||||
|
<key>AutomaticallyInstallMacOSUpdates</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Install of macOS Updates Is Enabled</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-1.4</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>7DB8733E-BD11-4E88-9AE0-273EF2D0974B</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ensure Firewall Logging Is Enabled and Configured",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.security.firewall</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-3.6.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>604D8218-D7B6-43B1-95E6-DFCA4C25D73D</string>
|
||||||
|
<key>EnableFirewall</key>
|
||||||
|
<true/>
|
||||||
|
<key>EnableLogging</key>
|
||||||
|
<true/>
|
||||||
|
<key>LoggingOption</key>
|
||||||
|
<string>detail</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Firewall Logging Is Enabled and Configured</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-3.6</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>5E27501E-50DF-4804-9DEC-0E63C34E8831</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ensure Bonjour Advertising Services Is Disabled",
|
||||||
|
Contents: []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>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.mDNSResponder</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-4.1.check</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>08FEA43B-CE9B-4098-804C-11459D109992</string>
|
||||||
|
<key>NoMulticastAdvertisements</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>test</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>Ensure Bonjour Advertising Services Is Disabled</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.fleetdm.cis-4.1</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>25BD1312-2B79-40C7-99FA-E60B49A1883E</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Disable Bluetooth sharing",
|
||||||
|
Contents: []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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
@ -301,118 +589,10 @@ var profiles = map[string][]byte{
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>`),
|
</plist>`),
|
||||||
"Ensure Auto Update Is Enabled": []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">
|
Name: "Ensure Install Application Updates from the App Store Is Enabled",
|
||||||
<dict>
|
Contents: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.SoftwareUpdate</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.2.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>4DC539B5-837E-4DC3-B60B-43A8C556A8F0</string>
|
|
||||||
<key>AutomaticCheckEnabled</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Auto Update Is Enabled</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.2</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>03E69A02-02CE-4CA0-8F17-3BAAD5D3852F</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
"Ensure Download New Updates When Available Is Enabled": []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>
|
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.SoftwareUpdate</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.3.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>5FDE6D58-79CD-447A-AFB0-BA32D889C396</string>
|
|
||||||
<key>AutomaticDownload</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Download New Updates When Available Is Enabled</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.3</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>0A1C2F97-D6FA-4CDB-ABB6-47DF2B151F4F</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
"Ensure Install of macOS Updates Is Enabled": []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>
|
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.SoftwareUpdate</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.4.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>15BF7634-276A-411B-8C4E-52D89B4ED82C</string>
|
|
||||||
<key>AutomaticallyInstallMacOSUpdates</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Install of macOS Updates Is Enabled</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.4</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>7DB8733E-BD11-4E88-9AE0-273EF2D0974B</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
"Ensure Install Application Updates from the App Store Is Enabled": []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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
@ -449,83 +629,10 @@ var profiles = map[string][]byte{
|
|||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>`),
|
</plist>`),
|
||||||
"Ensure Install Security Responses and System Files Is Enabled": []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">
|
Name: "Disable iCloud Drive storage solution usage",
|
||||||
<dict>
|
Contents: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.SoftwareUpdate</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.6.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>0D8F676A-A705-4F57-8FF8-3118360EFDEB</string>
|
|
||||||
<key>ConfigDataInstall</key>
|
|
||||||
<true/>
|
|
||||||
<key>CriticalUpdateInstall</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Install Security Responses and System Files Is Enabled</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.6</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>EBEE9B81-9D33-477F-AFBE-9691360B7A74</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
"Ensure Software Update Deferment Is Less Than or Equal to 30 Days": []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>
|
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.applicationaccess</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-1.7.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>123FD592-D1C3-41FD-BC41-F91F3E1E2CF4</string>
|
|
||||||
<key>enforcedSoftwareUpdateDelay</key>
|
|
||||||
<integer>29</integer>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Software Update Deferment Is Less Than or Equal to 30 Days</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.zwass.cis-1.7</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>385A0C13-2472-41B3-851C-1311FA12EB49</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
"Disable iCloud Drive storage solution usage": []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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
@ -562,84 +669,7 @@ var profiles = map[string][]byte{
|
|||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>`),
|
</plist>`),
|
||||||
"Ensure Firewall Logging Is Enabled and Configured": []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>
|
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.security.firewall</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-3.6.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>604D8218-D7B6-43B1-95E6-DFCA4C25D73D</string>
|
|
||||||
<key>EnableFirewall</key>
|
|
||||||
<true/>
|
|
||||||
<key>EnableLogging</key>
|
|
||||||
<true/>
|
|
||||||
<key>LoggingOption</key>
|
|
||||||
<string>detail</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Firewall Logging Is Enabled and Configured</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-3.6</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>5E27501E-50DF-4804-9DEC-0E63C34E8831</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
"Ensure Bonjour Advertising Services Is Disabled": []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>
|
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.mDNSResponder</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-4.1.check</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>08FEA43B-CE9B-4098-804C-11459D109992</string>
|
|
||||||
<key>NoMulticastAdvertisements</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>test</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Ensure Bonjour Advertising Services Is Disabled</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.fleetdm.cis-4.1</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadScope</key>
|
|
||||||
<string>System</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>25BD1312-2B79-40C7-99FA-E60B49A1883E</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>`),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newProfile = []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
var newProfile = []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
Loading…
Reference in New Issue
Block a user