diff --git a/CHANGELOG.md b/CHANGELOG.md index c7115220bd..521746d6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Fleet 4.47.3 (Mar 26, 2024) + +### Bug fixes + +* Fixed a bug where valid Windows MDM enrollments would show up as unmanaged (EnrollmentState 3). + ## Fleet 4.47.2 (Mar 22, 2024) ### Bug fixes diff --git a/changes/17288-fix-sort-of-sql-results b/changes/17288-fix-sort-of-sql-results new file mode 100644 index 0000000000..ededd089b4 --- /dev/null +++ b/changes/17288-fix-sort-of-sql-results @@ -0,0 +1 @@ +* UI fix of sql result sort for both string and numerical columns on live query results, live policy results, and query report \ No newline at end of file diff --git a/changes/17404-mdm-custom-settings b/changes/17404-mdm-custom-settings new file mode 100644 index 0000000000..78b0506bd0 --- /dev/null +++ b/changes/17404-mdm-custom-settings @@ -0,0 +1 @@ +- Adds API functionality for creating DDM declarations, both individually and as a batch. \ No newline at end of file diff --git a/changes/issue-17409-add-ddm-activities-to-ui b/changes/issue-17409-add-ddm-activities-to-ui new file mode 100644 index 0000000000..0c0c267a32 --- /dev/null +++ b/changes/issue-17409-add-ddm-activities-to-ui @@ -0,0 +1 @@ +- add ddm activities to the fleet UI diff --git a/changes/issue-17416-update-ui-to-support-ddm b/changes/issue-17416-update-ui-to-support-ddm new file mode 100644 index 0000000000..3bbe4eaaa9 --- /dev/null +++ b/changes/issue-17416-update-ui-to-support-ddm @@ -0,0 +1 @@ +- update UI to support macos DDM profiles. diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index 92a097ab51..f0558d302e 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,7 +8,7 @@ version: v6.0.2 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.47.2 +appVersion: v4.47.3 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 5c9e45f593..af4099168f 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -2,7 +2,7 @@ # All settings related to how Fleet is deployed in Kubernetes hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy -imageTag: v4.47.2 # Version of Fleet to deploy +imageTag: v4.47.3 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index ad20a3278d..42fca14d17 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -1033,6 +1033,9 @@ func newMDMProfileManager( schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error { return service.ReconcileAppleProfiles(ctx, ds, commander, logger) }), + schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error { + return service.ReconcileAppleDeclarations(ctx, ds, commander, logger) + }), schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error { return service.ReconcileWindowsProfiles(ctx, ds, logger) }), diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 0971c3e39a..d0ca94a81c 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -462,6 +462,7 @@ the way that the Fleet server works. mdmStorage *mysql.NanoMDMStorage mdmPushService push.Pusher mdmCheckinAndCommandService *service.MDMAppleCheckinAndCommandService + ddmService *service.MDMAppleDDMService mdmPushCertTopic string ) @@ -546,6 +547,7 @@ the way that the Fleet server works. } commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) mdmCheckinAndCommandService = service.NewMDMAppleCheckinAndCommandService(ds, commander, logger) + ddmService = service.NewMDMAppleDDMService(ds, logger) appCfg.MDM.EnabledAndConfigured = true } @@ -882,6 +884,7 @@ the way that the Fleet server works. scepStorage, logger, mdmCheckinAndCommandService, + ddmService, ); err != nil { initFatal(err, "setup mdm apple services") } diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index e7a11fd46c..e2bbcb13b8 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -165,7 +165,7 @@ func TestApplyTeamSpecs(t *testing.T) { return nil } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error { + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { return nil } @@ -1110,7 +1110,7 @@ func TestApplyAsGitOps(t *testing.T) { teamEnrollSecrets = secrets return nil } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error { + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { return nil } ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 07bb5b3e1b..b770565a8e 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -2207,7 +2207,7 @@ func TestGetTeamsYAMLAndApply(t *testing.T) { } return nil, fmt.Errorf("team not found: %s", name) } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error { + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { return nil } ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error { diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 6a8fab6482..0c4947ca29 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -25,6 +25,7 @@ func TestBasicGlobalGitOps(t *testing.T) { ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, + macDecls []*fleet.MDMAppleDeclaration, ) error { return nil } @@ -133,7 +134,7 @@ func TestBasicTeamGitOps(t *testing.T) { ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } ds.BatchSetMDMProfilesFunc = func( - ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, + ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, ) error { return nil } @@ -246,7 +247,7 @@ func TestFullGlobalGitOps(t *testing.T) { var appliedMacProfiles []*fleet.MDMAppleConfigProfile var appliedWinProfiles []*fleet.MDMWindowsConfigProfile ds.BatchSetMDMProfilesFunc = func( - ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, + ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, ) error { appliedMacProfiles = macProfiles appliedWinProfiles = winProfiles @@ -408,7 +409,7 @@ func TestFullTeamGitOps(t *testing.T) { var appliedMacProfiles []*fleet.MDMAppleConfigProfile var appliedWinProfiles []*fleet.MDMWindowsConfigProfile ds.BatchSetMDMProfilesFunc = func( - ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, + ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, ) error { appliedMacProfiles = macProfiles appliedWinProfiles = winProfiles diff --git a/ee/fleetd-chrome/package-lock.json b/ee/fleetd-chrome/package-lock.json index f10cd0199c..1eef8fb7d6 100644 --- a/ee/fleetd-chrome/package-lock.json +++ b/ee/fleetd-chrome/package-lock.json @@ -1,12 +1,12 @@ { "name": "fleetd-for-chrome", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fleetd-for-chrome", - "version": "1.2.0", + "version": "1.2.1", "dependencies": { "dotenv": "^16.0.3", "wa-sqlite": "github:rhashimoto/wa-sqlite#v0.9.11" diff --git a/ee/fleetd-chrome/package.json b/ee/fleetd-chrome/package.json index ad2d9a08f3..eba4f66075 100644 --- a/ee/fleetd-chrome/package.json +++ b/ee/fleetd-chrome/package.json @@ -1,7 +1,7 @@ { "name": "fleetd-for-chrome", "description": "Extension for Fleetd on ChromeOS", - "version": "1.2.0", + "version": "1.2.1", "dependencies": { "dotenv": "^16.0.3", "wa-sqlite": "github:rhashimoto/wa-sqlite#v0.9.11" diff --git a/ee/fleetd-chrome/updates-beta.xml b/ee/fleetd-chrome/updates-beta.xml index 03bddca04f..fdb6c2e6fd 100644 --- a/ee/fleetd-chrome/updates-beta.xml +++ b/ee/fleetd-chrome/updates-beta.xml @@ -1,6 +1,6 @@ - + - \ No newline at end of file + diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts index ebd2bb37eb..5912ef451c 100644 --- a/frontend/__mocks__/hostMock.ts +++ b/frontend/__mocks__/hostMock.ts @@ -10,7 +10,7 @@ const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = { detail: "This is verified", }; -export const createMockHostMacMdmProfile = ( +export const createMockHostMdmProfile = ( overrides?: Partial ): IHostMdmProfile => { return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides }; diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 143efeaf0a..dd871d7be6 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -67,6 +67,9 @@ export enum ActivityType { LockedHost = "locked_host", UnlockedHost = "unlocked_host", WipedHost = "wiped_host", + CreatedDeclarationProfile = "created_declaration_profile", + DeletedDeclarationProfile = "deleted_declaration_profile", + EditedDeclarationProfile = "edited_declaration_profile", } // This is a subset of ActivityType that are shown only for the host past activities diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index c7b4071fac..10d87ed75f 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -81,6 +81,11 @@ export interface IMdmProfile { } export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed"; +export type MdmDDMProfileStatus = + | "success" + | "pending" + | "failed" + | "acknowledged"; export type ProfileOperationType = "remove" | "install"; @@ -89,7 +94,7 @@ export interface IHostMdmProfile { name: string; operation_type: ProfileOperationType | null; platform: ProfilePlatform; - status: MdmProfileStatus; + status: MdmProfileStatus | MdmDDMProfileStatus; detail: string; } diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index 7c13965bdb..a3941ac7af 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -763,6 +763,55 @@ const TAGGED_TEMPLATES = { ); }, + createdDeclarationProfile: (activity: IActivity, isPremiumTier: boolean) => { + return ( + <> + {" "} + added declaration (DDM) profile + {activity.details?.profile_name} + {" "} + to{" "} + {getProfileMessageSuffix( + isPremiumTier, + "darwin", + activity.details?.team_name + )} + . + + ); + }, + deletedDeclarationProfile: (activity: IActivity, isPremiumTier: boolean) => { + return ( + <> + {" "} + removed declaration (DDM) profile{" "} + {activity.details?.profile_name} from{" "} + {getProfileMessageSuffix( + isPremiumTier, + "darwin", + activity.details?.team_name + )} + . + + ); + }, + editedDeclarationProfile: (activity: IActivity, isPremiumTier: boolean) => { + return ( + <> + {" "} + edited declaration (DDM) profile + {activity.details?.profile_name} + {" "} + for{" "} + {getProfileMessageSuffix( + isPremiumTier, + "darwin", + activity.details?.team_name + )}{" "} + via fleetctl. + + ); + }, }; const getDetail = ( @@ -918,6 +967,22 @@ const getDetail = ( case ActivityType.WipedHost: { return TAGGED_TEMPLATES.wipedHost(activity); } + case ActivityType.CreatedDeclarationProfile: { + return TAGGED_TEMPLATES.createdDeclarationProfile( + activity, + isPremiumTier + ); + } + case ActivityType.DeletedDeclarationProfile: { + return TAGGED_TEMPLATES.deletedDeclarationProfile( + activity, + isPremiumTier + ); + } + case ActivityType.EditedDeclarationProfile: { + return TAGGED_TEMPLATES.editedDeclarationProfile(activity, isPremiumTier); + } + default: { return TAGGED_TEMPLATES.defaultActivityTemplate(activity); } diff --git a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts index 8dbe94abf8..c2a917fb59 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts +++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts @@ -14,7 +14,8 @@ const AGGREGATE_STATUS_DISPLAY_OPTIONS: IAggregateDisplayOption[] = [ text: "Verified", iconName: "success", tooltipText: - "These hosts applied all OS settings. Fleet verified with osquery.", + "These hosts applied all OS settings. Fleet verified with osquery. " + + "Declaration profiles are verified with DDM.", }, { value: "verifying", diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss index 3c869da345..9f6c3eef81 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/_styles.scss @@ -186,7 +186,7 @@ flex-direction: column; border-radius: $border-radius; border: 1px solid $ui-fleet-black-10; - overflow-y: scroll; + overflow-y: auto; .loading-spinner { margin: 69.5px auto; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx index 15b7676701..b61ea7c2f0 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx @@ -4,7 +4,7 @@ import FileSaver from "file-saver"; import classnames from "classnames"; import { IMdmProfile } from "interfaces/mdm"; -import mdmAPI from "services/entities/mdm"; +import mdmAPI, { isDDMProfile } from "services/entities/mdm"; import Button from "components/buttons/Button"; import Graphic from "components/Graphic"; @@ -29,11 +29,17 @@ const LabelCount = ({ interface IProfileDetailsProps { platform: string; createdAt: string; + isDDM?: boolean; } -const ProfileDetails = ({ platform, createdAt }: IProfileDetailsProps) => { +const ProfileDetails = ({ + platform, + createdAt, + isDDM, +}: IProfileDetailsProps) => { const getPlatformName = () => { - return platform === "darwin" ? "macOS" : "Windows"; + if (platform === "windows") return "Windows"; + return isDDM ? "macOS (declaration)" : "macOS"; }; return ( @@ -47,6 +53,21 @@ const ProfileDetails = ({ platform, createdAt }: IProfileDetailsProps) => { ); }; +const createProfileExtension = (profile: IMdmProfile) => { + if (isDDMProfile(profile)) { + return "json"; + } + return profile.platform === "darwin" ? "mobileconfig" : "xml"; +}; + +const createFileContent = async (profile: IMdmProfile) => { + const content = await mdmAPI.downloadProfile(profile.profile_uuid); + if (isDDMProfile(profile)) { + return JSON.stringify(content, null, 2); + } + return content; +}; + interface IProfileListItemProps { isPremium: boolean; profile: IMdmProfile; @@ -62,13 +83,13 @@ const ProfileListItem = ({ onDelete, setProfileLabelsModalData, }: IProfileListItemProps) => { - const { created_at, labels, name, platform, profile_uuid } = profile; + const { created_at, labels, name, platform } = profile; const subClass = "list-item"; const onClickDownload = async () => { - const fileContent = await mdmAPI.downloadProfile(profile_uuid); + const fileContent = await createFileContent(profile); const formatDate = format(new Date(), "yyyy-MM-dd"); - const extension = platform === "darwin" ? "mobileconfig" : "xml"; + const extension = createProfileExtension(profile); const filename = `${formatDate}_${name}.${extension}`; const file = new File([fileContent], filename); FileSaver.saveAs(file); @@ -81,7 +102,11 @@ const ProfileListItem = ({
{name}
- +
diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx index c0460e83dd..c75193dcb6 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx @@ -3,7 +3,7 @@ import React from "react"; import Graphic from "components/Graphic"; const ALLOWED_FILE_TYPES_MESSAGE = - "Configuration profile (.mobileconfig for macOS or .xml for Windows)"; + "Configuration profile (.mobileconfig and .json for macOS or .xml for Windows)"; const ProfileGraphic = ({ baseClass, diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx index 883479185a..403ef83c16 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx @@ -52,7 +52,7 @@ const FileChooser = ({ { diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/helpers.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/helpers.tsx index 3f12067abf..bef5542e71 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/helpers.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/helpers.tsx @@ -78,6 +78,9 @@ export const parseFile = async (file: File): Promise<[string, string]> => { // } return [name, "macOS"]; } + case "json": { + return [name, "macOS"]; + } default: { throw new Error(`Invalid file type: ${ext}`); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index a117974fa2..4f03cb495a 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -52,6 +52,7 @@ import { HOST_ABOUT_DATA, HOST_OSQUERY_DATA, } from "utilities/constants"; +import { createMockHostMdmProfile } from "__mocks__/hostMock"; import Spinner from "components/Spinner"; import TabsWrapper from "components/TabsWrapper"; diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts index da989801ef..8f5dd764ee 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts @@ -22,28 +22,35 @@ type OperationTypeOption = Record< type ProfileDisplayConfig = Record; +const MAC_PROFILE_VERIFIED_DISPLAY_CONFIG: ProfileDisplayOption = { + statusText: "Verified", + iconName: "success", + tooltip: (innerProps) => + innerProps.isDiskEncryptionProfile + ? "The host turned disk encryption on and sent the key to Fleet. " + + "Fleet verified with osquery." + : "The host applied the setting. Fleet verified with osquery. " + + "Declaration profiles are verified with DDM.", +} as const; + +const MAC_PROFILE_VERIFYING_DISPLAY_CONFIG: ProfileDisplayOption = { + statusText: "Verifying", + iconName: "success-outline", + tooltip: (innerProps) => + innerProps.isDiskEncryptionProfile + ? "The host acknowledged the MDM command to turn on disk encryption. " + + "Fleet is verifying with osquery and retrieving the disk encryption key. " + + "This may take up to one hour." + : "The host acknowledged the MDM command to apply the setting. Fleet is " + + "verifying with osquery.", +} as const; + export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = { install: { - verified: { - statusText: "Verified", - iconName: "success", - tooltip: (innerProps) => - innerProps.isDiskEncryptionProfile - ? "The host turned disk encryption on and sent the key to Fleet. " + - "Fleet verified with osquery." - : "The host applied the setting. Fleet verified with osquery.", - }, - verifying: { - statusText: "Verifying", - iconName: "success-outline", - tooltip: (innerProps) => - innerProps.isDiskEncryptionProfile - ? "The host acknowledged the MDM command to turn on disk encryption. " + - "Fleet is verifying with osquery and retrieving the disk encryption key. " + - "This may take up to one hour." - : "The host acknowledged the MDM command to apply the setting. Fleet is " + - "verifying with osquery.", - }, + verified: MAC_PROFILE_VERIFIED_DISPLAY_CONFIG, + success: MAC_PROFILE_VERIFIED_DISPLAY_CONFIG, + verifying: MAC_PROFILE_VERIFYING_DISPLAY_CONFIG, + acknowledged: MAC_PROFILE_VERIFYING_DISPLAY_CONFIG, pending: { statusText: "Enforcing (pending)", iconName: "pending-outline", @@ -79,6 +86,8 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = { action_required: null, // should not be reached verified: null, // should not be reached verifying: null, // should not be reached + success: null, // should not be reached + acknowledged: null, // should not be reached failed: { statusText: "Failed", iconName: "error", @@ -89,7 +98,8 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = { type WindowsDiskEncryptionDisplayConfig = Omit< OperationTypeOption, - "action_required" + // windows disk encryption does not have these states + "action_required" | "success" | "acknowledged" >; export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = { diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx index d96606e357..7aea4d34ad 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx @@ -1,12 +1,14 @@ import React from "react"; import TableContainer from "components/TableContainer"; -import tableHeaders, { ITableRowOsSettings } from "./OSSettingsTableConfig"; +import tableHeaders, { + IHostMdmProfileWithAddedStatus, +} from "./OSSettingsTableConfig"; const baseClass = "os-settings-table"; interface IOSSettingsTableProps { - tableData?: ITableRowOsSettings[]; + tableData?: IHostMdmProfileWithAddedStatus[]; } const OSSettingsTable = ({ tableData }: IOSSettingsTableProps) => { diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx index 005a8e7e72..5c94da95ab 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx @@ -1,58 +1,44 @@ -import TextCell from "components/TableContainer/DataTable/TextCell"; import React from "react"; +import { Column } from "react-table"; +import { IStringCellProps } from "interfaces/datatable_config"; import { IHostMdmData } from "interfaces/host"; import { FLEET_FILEVAULT_PROFILE_DISPLAY_NAME, // FLEET_FILEVAULT_PROFILE_IDENTIFIER, IHostMdmProfile, + MdmDDMProfileStatus, MdmProfileStatus, ProfilePlatform, isWindowsDiskEncryptionStatus, } from "interfaces/mdm"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +import TextCell from "components/TableContainer/DataTable/TextCell"; import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell"; + import OSSettingStatusCell from "./OSSettingStatusCell"; import { generateWinDiskEncryptionProfile } from "../../helpers"; -export interface ITableRowOsSettings extends Omit { - status: OsSettingsTableStatusValue; -} - -export type OsSettingsTableStatusValue = MdmProfileStatus | "action_required"; - export const isMdmProfileStatus = ( status: string ): status is MdmProfileStatus => { return status !== "action_required"; }; -interface IHeaderProps { - column: { - title: string; - isSortedDesc: boolean; - }; +export interface IHostMdmProfileWithAddedStatus + extends Omit { + status: OsSettingsTableStatusValue; } -interface ICellProps { - cell: { - value: string; - }; - row: { - original: ITableRowOsSettings; - }; -} +type ITableColumnConfig = Column; +type ITableStringCellProps = IStringCellProps; -interface IDataColumn { - Header: ((props: IHeaderProps) => JSX.Element) | string; - Cell: (props: ICellProps) => JSX.Element; - id?: string; - title?: string; - accessor?: string; - disableHidden?: boolean; - disableSortBy?: boolean; - sortType?: string; -} +export type INonDDMProfileStatus = MdmProfileStatus | "action_required"; + +export type OsSettingsTableStatusValue = + | MdmDDMProfileStatus + | INonDDMProfileStatus; /** * generates the formatted tooltip for the error column. @@ -107,22 +93,20 @@ const generateErrorTooltip = ( return generateFormattedTooltip(detail); }; -const tableHeaders: IDataColumn[] = [ +const tableHeaders: ITableColumnConfig[] = [ { - title: "Name", Header: "Name", disableSortBy: true, accessor: "name", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), + Cell: (cellProps: ITableStringCellProps) => { + return ; + }, }, { - title: "Status", Header: "Status", disableSortBy: true, - accessor: "statusText", - Cell: (cellProps: ICellProps) => { + accessor: "status", + Cell: (cellProps: ITableStringCellProps) => { return ( { + Cell: (cellProps: ITableStringCellProps): JSX.Element => { const profile = cellProps.row.original; const value = @@ -165,7 +148,7 @@ const tableHeaders: IDataColumn[] = [ ]; const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => { - const rows: ITableRowOsSettings[] = []; + const rows: IHostMdmProfileWithAddedStatus[] = []; if (profiles) { rows.push(...profiles); @@ -190,15 +173,12 @@ const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => { return rows; }; -const makeDarwinRows = ({ - profiles, - macos_settings, -}: IHostMdmData): ITableRowOsSettings[] | null => { +const makeDarwinRows = ({ profiles, macos_settings }: IHostMdmData) => { if (!profiles) { return null; } - let rows: ITableRowOsSettings[] = profiles; + let rows: IHostMdmProfileWithAddedStatus[] = profiles; if (macos_settings?.disk_encryption === "action_required") { rows = profiles.map((p) => { // TODO: this is a brittle check for the filevault profile diff --git a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx index 6f90c372ab..68043e52a7 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx @@ -32,7 +32,8 @@ const STATUS_DISPLAY_OPTIONS: StatusDisplayOptions = { Verified: { iconName: "success", tooltipText: - "The host applied all OS settings. Fleet verified with osquery.", + "The host applied all OS settings. Fleet verified with osquery. " + + "Declaration profiles are verified with DDM.", }, Verifying: { iconName: "success-outline", diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx index 2c78ef99b4..7ccd7fb166 100644 --- a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx +++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx @@ -8,6 +8,7 @@ import { generateCSVFilename, generateCSVQueryResults, } from "utilities/generate_csv"; +import { getTableColumnsFromSql } from "utilities/helpers"; import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report"; import Button from "components/buttons/Button"; @@ -52,9 +53,13 @@ const QueryReport = ({ useEffect(() => { if (queryReport && queryReport.results && queryReport.results.length > 0) { + const tableColumns = getTableColumnsFromSql(lastEditedQueryBody); + const newColumnConfigs = generateReportColumnConfigsFromResults( - flattenResults(queryReport.results) + flattenResults(queryReport.results), + tableColumns ); + // Update tableHeaders if new headers are found if (newColumnConfigs !== columnConfigs) { setColumnConfigs(newColumnConfigs); diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx index 95ac94c664..b46383ecf6 100644 --- a/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx +++ b/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx @@ -9,10 +9,12 @@ import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColu import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import { + getSortTypeFromColumnType, getUniqueColumnNamesFromRows, humanHostLastSeen, internallyTruncateText, } from "utilities/helpers"; +import { IQueryTableColumn } from "interfaces/osquery_table"; import { IHeaderProps, IWebSocketData } from "interfaces/datatable_config"; type IQueryReportTableColumnConfig = Column; @@ -40,7 +42,8 @@ const _unshiftHostname = (headers: IQueryReportTableColumnConfig[]) => { }; const generateReportColumnConfigsFromResults = ( - results: IWebSocketData[] + results: IWebSocketData[], + tableColumns?: IQueryTableColumn[] | [] ): IQueryReportTableColumnConfig[] => { /* Results include an array of objects, each representing a table row Each key value pair in an object represents a column name and value @@ -79,7 +82,7 @@ const generateReportColumnConfigsFromResults = ( Filter: DefaultColumnFilter, // Component hides filter for last_fetched filterType: "text", disableSortBy: false, - sortType: "caseInsensitive", + sortType: getSortTypeFromColumnType(key, tableColumns), }; } ); diff --git a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx index 786563c05b..c27ab54685 100644 --- a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx +++ b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx @@ -5,16 +5,14 @@ import classnames from "classnames"; import FileSaver from "file-saver"; import { QueryContext } from "context/query"; import { useDebouncedCallback } from "use-debounce"; -import { find } from "lodash"; import { generateCSVFilename, generateCSVQueryResults, } from "utilities/generate_csv"; -import { osqueryTables } from "utilities/osquery_tables"; +import { getTableColumnsFromSql } from "utilities/helpers"; import { ICampaign, ICampaignError } from "interfaces/campaign"; import { ITarget } from "interfaces/target"; -import { IQueryTableColumn } from "interfaces/osquery_table"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; @@ -25,7 +23,6 @@ import QueryResultsHeading from "components/queries/queryResults/QueryResultsHea import AwaitingResults from "components/queries/queryResults/AwaitingResults"; import InfoBanner from "components/InfoBanner"; import CustomLink from "components/CustomLink"; -import { checkTable } from "utilities/sql_tools"; import generateColumnConfigsFromRows from "./QueryResultsTableConfig"; @@ -77,9 +74,6 @@ const QueryResults = ({ const [queryResultsForTableRender, setQueryResultsForTableRender] = useState( queryResults ); - const [osqueryTableColumns, setOsqueryTableColumns] = useState< - IQueryTableColumn[] | [] - >([]); // immediately reset results const onRunAgain = useCallback(() => { @@ -98,32 +92,20 @@ const QueryResults = ({ debounceQueryResults(queryResults); }, [queryResults, debounceQueryResults]); - // Set table/s columns from SQL - useEffect(() => { - const tableNames = - (lastEditedQueryBody && checkTable(lastEditedQueryBody).tables) || []; - - let columns: IQueryTableColumn[] | [] = []; - tableNames.forEach((tableName: string) => { - const tableColumns = - find(osqueryTables, { name: tableName })?.columns || []; - columns = [...columns, ...tableColumns]; - }); - setOsqueryTableColumns(columns); - }, [lastEditedQueryBody]); - useEffect(() => { if (queryResults && queryResults.length > 0) { + const tableColumns = getTableColumnsFromSql(lastEditedQueryBody); + const newResultsColumnConfigs = generateColumnConfigsFromRows( queryResults, - osqueryTableColumns + tableColumns ); // Update tableHeaders if new headers are found if (newResultsColumnConfigs !== resultsColumnConfigs) { setResultsColumnConfigs(newResultsColumnConfigs); } } - }, [queryResults]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders + }, [queryResults, lastEditedQueryBody]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders useEffect(() => { if (errorColumnConfigs?.length === 0 && !!errors?.length) { diff --git a/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx index 75327cea21..fd29edceb4 100644 --- a/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx +++ b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx @@ -4,11 +4,11 @@ import React from "react"; import { CellProps, Column, HeaderProps } from "react-table"; -import { find } from "lodash"; import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import { + getSortTypeFromColumnType, getUniqueColumnNamesFromRows, internallyTruncateText, } from "utilities/helpers"; @@ -34,28 +34,11 @@ const _unshiftHostname = (columns: Column[]) => { return newHeaders; }; -// Sorts numerical columns correctly while perserving case insensitive sort for text columns -const sortType = ( - colName: string | number | symbol, - osqueryTableColumns?: IQueryTableColumn[] | [] -) => { - if (typeof colName === "string" && !!osqueryTableColumns) { - const numberTypes = ["integer", "bigint", "unsigned_bigint", "double"]; - - const type = find(osqueryTableColumns, { name: colName })?.type; - - if (type && numberTypes.includes(type)) { - return "alphanumeric"; - } - } - return "caseInsensitive"; -}; - const generateColumnConfigsFromRows = >( // TODO - narrow typing down this entire chain of logic // typed as any[] to accomodate loose typing of websocket API results: T[], // {col:val, ...} for each row of query results - osqueryTableColumns?: IQueryTableColumn[] | [] + tableColumns?: IQueryTableColumn[] | [] ): Column[] => { const uniqueColumnNames = getUniqueColumnNamesFromRows(results); const columnsConfigs = uniqueColumnNames.map>((colName) => { @@ -76,7 +59,7 @@ const generateColumnConfigsFromRows = >( }, Filter: DefaultColumnFilter, disableSortBy: false, - sortType: sortType(colName, osqueryTableColumns), + sortType: getSortTypeFromColumnType(colName, tableColumns), }; }); return _unshiftHostname(columnsConfigs); diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index 90101a1135..eb580d06f6 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { DiskEncryptionStatus, + IHostMdmProfile, IMdmProfile, MdmProfileStatus, } from "interfaces/mdm"; @@ -47,6 +48,10 @@ export interface IUploadProfileApiParams { labels?: string[]; } +export const isDDMProfile = (profile: IMdmProfile | IHostMdmProfile) => { + return profile.profile_uuid.startsWith("d"); +}; + interface IUpdateSetupExperienceBody { team_id?: number; enable_release_device_manually: boolean; diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index a7ece58a81..2b9dc59ba4 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -2,6 +2,7 @@ import React from "react"; import { isEmpty, flatMap, + find, omit, pick, size, @@ -25,6 +26,7 @@ import { buildQueryStringFromParams } from "utilities/url"; import { IHost } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { IPack } from "interfaces/pack"; +import { IQueryTableColumn } from "interfaces/osquery_table"; import { IScheduledQuery, IPackQueryFormData, @@ -39,6 +41,8 @@ import { UserRole } from "interfaces/user"; import stringUtils from "utilities/strings"; import sortUtils from "utilities/sort"; +import { checkTable } from "utilities/sql_tools"; +import { osqueryTables } from "utilities/osquery_tables"; import { DEFAULT_EMPTY_CELL_VALUE, DEFAULT_GRAVATAR_LINK, @@ -887,6 +891,40 @@ export const getUniqueColumnNamesFromRows = < // can allow additional dropdown value types in the future type DropdownOptionValue = IDropdownOption["value"]; +/** Generates the column schema for a sql query */ +export const getTableColumnsFromSql = ( + sql: string +): IQueryTableColumn[] | [] => { + const tableNames = (sql && checkTable(sql).tables) || []; + + let sqlColumns: IQueryTableColumn[] | [] = []; + tableNames.forEach((tableName: string) => { + const tableColumns = + find(osqueryTables, { name: tableName })?.columns || []; + sqlColumns = [...sqlColumns, ...tableColumns]; + }); + // TODO: Edge case of tables sharing column names with different typing not considered + + return sqlColumns; +}; + +/** Sorts sql results numerical columns correctly while perserving case insensitive sort for text columns */ +export const getSortTypeFromColumnType = ( + colName: string | number | symbol, + tableColumns?: IQueryTableColumn[] | [] +) => { + if (typeof colName === "string") { + const numberTypes = ["integer", "bigint", "unsigned_bigint", "double"]; + + const type = find(tableColumns, { name: colName })?.type; + + if (type && numberTypes.includes(type)) { + return "alphanumeric"; + } + } + return "caseInsensitive"; +}; + export function getCustomDropdownOptions( defaultOptions: IDropdownOption[], customValue: DropdownOptionValue, @@ -918,6 +956,8 @@ export default { generateRole, generateTeam, getUniqueColumnNamesFromRows, + getTableColumnsFromSql, + getSortTypeFromColumnType, getCustomDropdownOptions, greyCell, humanHostLastSeen, diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index 2775e36c4e..991c9135de 100644 --- a/infrastructure/dogfood/terraform/aws/variables.tf +++ b/infrastructure/dogfood/terraform/aws/variables.tf @@ -56,7 +56,7 @@ variable "database_name" { variable "fleet_image" { description = "the name of the container image to run" - default = "fleetdm/fleet:v4.47.2" + default = "fleetdm/fleet:v4.47.3" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index 98f012a0cd..b3cddde5ca 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,5 +68,5 @@ variable "redis_mem" { } variable "image" { - default = "fleet:v4.47.2" + default = "fleet:v4.47.3" } diff --git a/orbit/pkg/packaging/windows.go b/orbit/pkg/packaging/windows.go index 731e57efe0..b35a00c5bb 100644 --- a/orbit/pkg/packaging/windows.go +++ b/orbit/pkg/packaging/windows.go @@ -222,7 +222,7 @@ func checkWine(wineChecked bool) error { cmd := exec.Command(wix.WineCmd, "--version") if err := cmd.Run(); err != nil { return fmt.Errorf( - "%s failed. Is Wine installed? Creating a fleetd agent for Windows (.msi) requires Wine. To install Wine see the script here: https://github.com/fleetdm/fleet/blob/fleet-v4.44.0/scripts/macos-install-wine.sh %w", + "%s failed. Is Wine installed? Creating a fleetd agent for Windows (.msi) requires Wine. To install Wine see the script here: https://fleetdm.com/install-wine %w", wix.WineCmd, err, ) } diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go index 0f15c7c453..deb52664b7 100644 --- a/pkg/mdm/mdmtest/apple.go +++ b/pkg/mdm/mdmtest/apple.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -21,6 +22,7 @@ import ( "time" "github.com/fleetdm/fleet/v4/pkg/fleethttp" + "github.com/fleetdm/fleet/v4/server/fleet" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util" @@ -31,7 +33,6 @@ import ( httptransport "github.com/go-kit/kit/transport/http" "github.com/google/uuid" "github.com/groob/plist" - micromdm "github.com/micromdm/micromdm/mdm/mdm" "go.mozilla.org/pkcs7" ) @@ -386,6 +387,30 @@ func (c *TestAppleMDMClient) TokenUpdate() error { return err } +// DeclarativeManagement sends a DeclarativeManagement checkin request to the server. +// +// The endpoint argument is used as the value for the `Endpoint` key in the request payload. +// +// For more details check https://developer.apple.com/documentation/devicemanagement/declarativemanagementrequest +func (c *TestAppleMDMClient) DeclarativeManagement(endpoint string, data ...fleet.MDMAppleDDMStatusReport) (*http.Response, error) { + payload := map[string]any{ + "MessageType": "DeclarativeManagement", + "UDID": c.UUID, + "Topic": "com.apple.mgmt.External." + c.UUID, + "EnrollmentID": "testenrollmentid-" + c.UUID, + "Endpoint": endpoint, + } + if len(data) != 0 { + rawData, err := json.Marshal(data[0]) + if err != nil { + return nil, fmt.Errorf("marshaling status report: %w", err) + } + payload["Data"] = rawData + } + r, err := c.request("application/x-apple-aspen-mdm-checkin", payload) + return r, err +} + // Checkout sends the CheckOut message to the MDM server. func (c *TestAppleMDMClient) Checkout() error { payload := map[string]any{ @@ -404,7 +429,7 @@ func (c *TestAppleMDMClient) Checkout() error { // receive commands. The server can signal back with either a command to run // or an empty (nil, nil) response body to end the communication // (i.e. no commands to run). -func (c *TestAppleMDMClient) Idle() (*micromdm.CommandPayload, error) { +func (c *TestAppleMDMClient) Idle() (*mdm.Command, error) { payload := map[string]any{ "Status": "Idle", "Topic": "com.apple.mgmt.External." + c.UUID, @@ -420,7 +445,7 @@ func (c *TestAppleMDMClient) Idle() (*micromdm.CommandPayload, error) { // The server can signal back with either a command to run // or an empty (nil, nil) response body to end the communication // (i.e. no commands to run). -func (c *TestAppleMDMClient) Acknowledge(cmdUUID string) (*micromdm.CommandPayload, error) { +func (c *TestAppleMDMClient) Acknowledge(cmdUUID string) (*mdm.Command, error) { payload := map[string]any{ "Status": "Acknowledged", "Topic": "com.apple.mgmt.External." + c.UUID, @@ -473,7 +498,7 @@ func (c *TestAppleMDMClient) GetBootstrapToken() ([]byte, error) { // The server can signal back with either a command to run // or an empty (nil, nil) response body to end the communication // (i.e. no commands to run). -func (c *TestAppleMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*micromdm.CommandPayload, error) { +func (c *TestAppleMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*mdm.Command, error) { payload := map[string]any{ "Status": "Error", "Topic": "com.apple.mgmt.External." + c.UUID, @@ -485,7 +510,7 @@ func (c *TestAppleMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*mi return c.sendAndDecodeCommandResponse(payload) } -func (c *TestAppleMDMClient) sendAndDecodeCommandResponse(payload map[string]any) (*micromdm.CommandPayload, error) { +func (c *TestAppleMDMClient) sendAndDecodeCommandResponse(payload map[string]any) (*mdm.Command, error) { res, err := c.request("", payload) if err != nil { return nil, fmt.Errorf("request error: %w", err) @@ -510,11 +535,12 @@ func (c *TestAppleMDMClient) sendAndDecodeCommandResponse(payload map[string]any if err != nil { return nil, fmt.Errorf("decode command: %w", err) } - var p micromdm.CommandPayload + var p mdm.Command err = plist.Unmarshal(cmd.Raw, &p) if err != nil { return nil, fmt.Errorf("unmarshal command payload: %w", err) } + p.Raw = cmd.Raw return &p, nil } diff --git a/scripts/macos-install-wine.sh b/scripts/macos-install-wine.sh index fd5c55532b..5a29f352d2 100755 --- a/scripts/macos-install-wine.sh +++ b/scripts/macos-install-wine.sh @@ -2,8 +2,9 @@ set -eo pipefail +# Run this script in user context (not root). # Reference: https://wiki.winehq.org/MacOS -# NOTE: Wine can be installed without brew via a distribution such as https://github.com/Gcenx/macOS_Wine_builds/releases/tag/9.0, or by building from source. +# Wine can be installed without brew via a distribution such as https://github.com/Gcenx/macOS_Wine_builds/releases/tag/9.0, or by building from source. # Check if brew is installed if ! command -v brew >/dev/null 2>&1 ; then diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 61f1979db2..b22f178300 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -16,8 +16,8 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/ptr" - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" + "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) @@ -30,6 +30,8 @@ INSERT INTO (SELECT ?, ?, ?, ?, ?, UNHEX(MD5(?)), CURRENT_TIMESTAMP() FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ? + ) AND NOT EXISTS ( + SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ? ) )` @@ -41,7 +43,7 @@ INSERT INTO var profileID int64 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) + profUUID, teamID, cp.Identifier, cp.Name, cp.Mobileconfig, cp.Mobileconfig, cp.Name, teamID, cp.Name, teamID) if err != nil { switch { case isDuplicate(err): @@ -106,6 +108,25 @@ func formatErrorDuplicateConfigProfile(err error, cp *fleet.MDMAppleConfigProfil } } +func formatErrorDuplicateDeclaration(err error, decl *fleet.MDMAppleDeclaration) error { + switch { + case strings.Contains(err.Error(), "idx_mdm_apple_declaration_team_identifier"): + return &existsError{ + ResourceType: "MDMAppleDeclaration.Identifier", + Identifier: decl.Identifier, + TeamID: decl.TeamID, + } + case strings.Contains(err.Error(), "idx_mdm_apple_declaration_team_name"): + return &existsError{ + ResourceType: "MDMAppleDeclaration.Name", + Identifier: decl.Name, + TeamID: decl.TeamID, + } + default: + return err + } +} + func (ds *Datastore) ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error) { stmt := ` SELECT @@ -192,7 +213,7 @@ WHERE // 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}) + labels, err := ds.listProfileLabelsForProfiles(ctx, nil, []string{res.ProfileUUID}, nil) if err != nil { return nil, err } @@ -205,11 +226,52 @@ WHERE return &res, nil } +func (ds *Datastore) GetMDMAppleDeclaration(ctx context.Context, declUUID string) (*fleet.MDMAppleDeclaration, error) { + stmt := ` +SELECT + declaration_uuid, + team_id, + name, + identifier, + raw_json, + checksum, + created_at, + uploaded_at +FROM + mdm_apple_declarations +WHERE + declaration_uuid = ?` + + var res fleet.MDMAppleDeclaration + err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, declUUID) + if err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("MDMAppleDeclaration").WithName(declUUID)) + } + + return nil, ctxerr.Wrap(ctx, err, "get mdm apple declaration") + } + + labels, err := ds.listProfileLabelsForProfiles(ctx, nil, nil, []string{res.DeclarationUUID}) + 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 +} + func (ds *Datastore) DeleteMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) error { return ds.deleteMDMAppleConfigProfileByIDOrUUID(ctx, profileID, "") } func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error { + if strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix) { + return ds.deleteMDMAppleDeclaration(ctx, profileUUID) + } return ds.deleteMDMAppleConfigProfileByIDOrUUID(ctx, 0, profileUUID) } @@ -239,11 +301,28 @@ func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, return nil } +func (ds *Datastore) deleteMDMAppleDeclaration(ctx context.Context, uuid string) error { + stmt := `DELETE FROM mdm_apple_declarations WHERE declaration_uuid = ?` + + res, err := ds.writer(ctx).ExecContext(ctx, stmt, uuid) + if err != nil { + return ctxerr.Wrap(ctx, err) + } + + deleted, _ := res.RowsAffected() + if deleted != 1 { + return ctxerr.Wrap(ctx, notFound("MDMAppleDeclaration").WithName(uuid)) + } + + return nil +} + func (ds *Datastore) DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx context.Context, teamID *uint, profileIdentifier string) error { if teamID == nil { teamID = ptr.Uint(0) } + // TODO: add deletion of declarations here or separate method? res, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM mdm_apple_configuration_profiles WHERE team_id = ? AND identifier = ?`, teamID, profileIdentifier) if err != nil { return ctxerr.Wrap(ctx, err) @@ -273,16 +352,38 @@ COALESCE(detail, '') AS detail FROM host_mdm_apple_profiles WHERE +host_uuid = ? AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s')) + +UNION ALL +SELECT +declaration_uuid AS profile_uuid, +declaration_name AS name, +declaration_identifier AS identifier, +-- internally, a NULL status implies that the cron needs to pick up +-- this profile, for the user that difference doesn't exist, the +-- profile is effectively pending. This is consistent with all our +-- aggregation functions. +COALESCE(status, '%s') AS status, +COALESCE(operation_type, '') AS operation_type, +COALESCE(detail, '') AS detail +FROM +host_mdm_apple_declarations +WHERE host_uuid = ? AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s'))`, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryPending, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified, + fleet.MDMDeliveryPending, + fleet.MDMOperationTypeRemove, + fleet.MDMDeliveryPending, + fleet.MDMDeliveryVerifying, + fleet.MDMDeliveryVerified, ) var profiles []fleet.HostMDMAppleProfile - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, hostUUID); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, hostUUID, hostUUID); err != nil { return nil, err } return profiles, nil @@ -1411,6 +1512,8 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( return nil } + appleMDMProfilesDesiredStateQuery := generateDesiredStateQuery("profile") + // TODO(mna): the conditions here (and in toRemoveStmt) are subtly different // than the ones in ListMDMAppleProfilesToInstall/Remove, so I'm keeping // those statements distinct to avoid introducing a subtle bug, but we should @@ -1641,20 +1744,30 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( return nil } -const appleMDMProfilesDesiredStateQuery = ` - -- non label-based profiles +// mdmEntityTypeToTable tracks what table should be used in the templates for +// SQL statements based on the given entity type. +var mdmEntityTypeToTable = map[string]string{ + "declaration": "declaration", + "profile": "configuration_profile", +} + +// generateDesiredStateQuery generates a query string that represents the +// desired state of an Apple entity based on its type (profile or declaration) +func generateDesiredStateQuery(entityType string) string { + return fmt.Sprintf(` + -- non label-based entities SELECT - macp.profile_uuid, + mae.%[1]s_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, + mae.identifier as %[1]s_identifier, + mae.name as %[1]s_name, + mae.checksum as checksum, + 0 as count_%[1]s_labels, 0 as count_host_labels FROM - mdm_apple_configuration_profiles macp + mdm_apple_%[2]ss mae JOIN hosts h - ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0) + ON h.team_id = mae.team_id OR (h.team_id IS NULL AND mae.team_id = 0) JOIN nano_enrollments ne ON ne.device_id = h.uuid WHERE @@ -1663,81 +1776,146 @@ const appleMDMProfilesDesiredStateQuery = ` ne.type = 'Device' AND NOT EXISTS ( SELECT 1 - FROM mdm_configuration_profile_labels mcpl - WHERE mcpl.apple_profile_uuid = macp.profile_uuid + FROM mdm_%[2]s_labels mel + WHERE mel.apple_%[1]s_uuid = mae.%[1]s_uuid ) AND - ( %s ) + ( %[3]s ) UNION - -- label-based profiles where the host is a member of all the labels + -- label-based entities where the host is a member of all the labels SELECT - macp.profile_uuid, + mae.%[1]s_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, + mae.identifier as %[1]s_identifier, + mae.name as %[1]s_name, + mae.checksum as checksum, + COUNT(*) as count_%[1]s_labels, COUNT(lm.label_id) as count_host_labels FROM - mdm_apple_configuration_profiles macp + mdm_apple_%[2]ss mae JOIN hosts h - ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0) + ON h.team_id = mae.team_id OR (h.team_id IS NULL AND mae.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 + JOIN mdm_%[2]s_labels mel + ON mel.apple_%[1]s_uuid = mae.%[1]s_uuid LEFT OUTER JOIN label_membership lm - ON lm.label_id = mcpl.label_id AND lm.host_id = h.id + ON lm.label_id = mel.label_id AND lm.host_id = h.id WHERE h.platform = 'darwin' AND ne.enabled = 1 AND ne.type = 'Device' AND - ( %s ) + ( %[3]s ) GROUP BY - macp.profile_uuid, h.uuid, macp.identifier, macp.name, macp.checksum + mae.%[1]s_uuid, h.uuid, mae.identifier, mae.name, mae.checksum HAVING - count_profile_labels > 0 AND count_host_labels = count_profile_labels -` + count_%[1]s_labels > 0 AND count_host_labels = count_%[1]s_labels + + `, entityType, mdmEntityTypeToTable[entityType], "%s") +} + +// generateEntitiesToInstallQuery is a set difference between: +// +// - Set A (ds), the "desired state", can be obtained from a JOIN between +// mdm_apple_x and hosts. +// +// - Set B, the "current state" given by host_mdm_apple_x. +// +// A - B gives us the entities that need to be installed: +// +// - entities that are in A but not in B +// +// - entities which contents have changed, but their identifier are +// the same (by checking the checksums) +// +// - entities that are in A and in B, but with an operation type of +// "remove", regardless of the status. (technically, if status is NULL then +// the entity should be already installed - it has not been queued for +// remove yet -, and same if status is failed, but the proper thing to do +// with it would be to remove the row, not return it as "to install". For +// simplicity of implementation here (and to err on the safer side - the +// entity's content could've changed), we'll return it as "to install" for +// now, which will cause the row to be updated with the correct operation +// type and status). +// +// - entities that are in A and in B, with an operation type of "install" +// and a NULL status. Other statuses mean that the operation is already in +// flight (pending), the operation has been completed but is still subject +// to independent verification by Fleet (verifying), or has reached a terminal +// state (failed or verified). If the entity's content is edited, all +// relevant hosts will be marked as status NULL so that it gets +// re-installed. +// +// Note that for label-based entities, only fully-satisfied entities are +// considered for installation. This means that a broken label-based entity, +// where one of the labels does not exist anymore, will not be considered for +// installation. +func generateEntitiesToInstallQuery(entityType string) string { + return fmt.Sprintf(` + ( %[3]s ) as ds + LEFT JOIN host_mdm_apple_%[1]ss hmae + ON hmae.%[1]s_uuid = ds.%[1]s_uuid AND hmae.host_uuid = ds.host_uuid + WHERE + -- entity has been updated + ( hmae.checksum != ds.checksum ) OR + -- entity in A but not in B + ( hmae.%[1]s_uuid IS NULL AND hmae.host_uuid IS NULL ) OR + -- entities in A and B but with operation type "remove" + ( hmae.host_uuid IS NOT NULL AND ( hmae.operation_type = ? OR hmae.operation_type IS NULL ) ) OR + -- entities in A and B with operation type "install" and NULL status + ( hmae.host_uuid IS NOT NULL AND hmae.operation_type = ? AND hmae.status IS NULL ) +`, entityType, mdmEntityTypeToTable[entityType], fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE")) +} + +// generateEntitiesToRemoveQuery is a set difference between: +// +// - Set A (ds), the "desired state", can be obtained from a JOIN between +// mdm_apple_configuration_x and hosts. +// +// - Set B, the "current state" given by host_mdm_apple_x. +// +// B - A gives us the entities that need to be removed: +// +// - entities that are in B but not in A, except those with operation type +// "remove" and a terminal state (failed) or a state indicating +// that the operation is in flight (pending) or the operation has been completed +// but is still subject to independent verification by Fleet (verifying) +// or the operation has been completed and independenly verified by Fleet (verified). +// +// Any other case are entities that are in both B and A, and as such are +// processed by the generateEntitiesToInstallQuery query (since they are in +// both, their desired state is necessarily to be installed). +// +// Note that for label-based entities, only those that are fully-sastisfied +// by the host are considered for install (are part of the desired state used +// to compute the ones to remove). However, as a special case, a broken +// label-based entity will NOT be removed from a host where it was +// previously installed. However, if a host used to satisfy a label-based +// entity but no longer does (and that label-based entity is not "broken"), +// the entity will be removed from the host. +func generateEntitiesToRemoveQuery(entityType string) string { + return fmt.Sprintf(` + ( %[3]s ) as ds + RIGHT JOIN host_mdm_apple_%[1]ss hmae + ON hmae.%[1]s_uuid = ds.%[1]s_uuid AND hmae.host_uuid = ds.host_uuid + WHERE + -- entities that are in B but not in A + ds.%[1]s_uuid IS NULL AND ds.host_uuid IS NULL AND + -- except "remove" operations in a terminal state or already pending + ( hmae.operation_type IS NULL OR hmae.operation_type != ? OR hmae.status IS NULL ) AND + -- except "would be removed" entities if they are a broken label-based entities + NOT EXISTS ( + SELECT 1 + FROM mdm_%[2]s_labels mcpl + WHERE + mcpl.apple_%[1]s_uuid = hmae.%[1]s_uuid AND + mcpl.label_id IS NULL + ) +`, entityType, mdmEntityTypeToTable[entityType], fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE")) +} func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) { - // The query below is a set difference between: - // - // - Set A (ds), the "desired state", can be obtained from a JOIN between - // mdm_apple_configuration_profiles and hosts. - // - // - Set B, the "current state" given by host_mdm_apple_profiles. - // - // A - B gives us the profiles that need to be installed: - // - // - profiles that are in A but not in B - // - // - profiles which contents have changed, but their identifier are - // the same (by checking the checksums) - // - // - 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 - // the profile should be already installed - it has not been queued for - // remove yet -, and same if status is failed, but the proper thing to do - // with it would be to remove the row, not return it as "to install". For - // simplicity of implementation here (and to err on the safer side - the - // profile's content could've changed), we'll return it as "to install" for - // now, which will cause the row to be updated with the correct operation - // type and status). - // - // - profiles that are in A and in B, with an operation type of "install" - // and a NULL status. Other statuses mean that the operation is already in - // flight (pending), the operation has been completed but is still subject - // 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 be marked as status NULL so that it gets - // re-installed. - // - // Note that for label-based profiles, only fully-satisfied profiles are - // considered for installation. This means that a broken label-based profile, - // where one of the labels does not exist anymore, will not be considered for - // installation. - query := fmt.Sprintf(` SELECT ds.profile_uuid, @@ -1745,82 +1923,26 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee ds.profile_identifier, ds.profile_name, ds.checksum - FROM ( %s ) as ds - LEFT JOIN host_mdm_apple_profiles hmap - ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid - WHERE - -- profile has been updated - ( hmap.checksum != ds.checksum ) OR - -- profiles in A but not in B - ( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR - -- 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 ) ) 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 ) -`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "TRUE", "TRUE")) - + FROM %s `, + generateEntitiesToInstallQuery("profile")) var profiles []*fleet.MDMAppleProfilePayload err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove, fleet.MDMOperationTypeInstall) return profiles, err } func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) { - // The query below is a set difference between: - // - // - Set A (ds), the "desired state", can be obtained from a JOIN between - // mdm_apple_configuration_profiles and hosts. - // - // - Set B, the "current state" given by host_mdm_apple_profiles. - // - // B - A gives us the profiles that need to be removed: - // - // - profiles that are in B but not in A, except those with operation type - // "remove" and a terminal state (failed) or a state indicating - // that the operation is in flight (pending) or the operation has been completed - // but is still subject to independent verification by Fleet (verifying) - // or the operation has been completed and independenly verified by Fleet (verified). - // - // 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 - // both, their desired state is necessarily to be installed). - // - // Note that for label-based profiles, only those that are fully-sastisfied - // by the host are considered for install (are part of the desired state used - // to compute the ones to remove). However, as a special case, a broken - // label-based profile will NOT be removed from a host where it was - // previously installed. However, if a host used to satisfy a label-based - // profile but no longer does (and that label-based profile is not "broken"), - // the profile will be removed from the host. - query := fmt.Sprintf(` SELECT - hmap.profile_uuid, - hmap.profile_identifier, - hmap.profile_name, - hmap.host_uuid, - hmap.checksum, - hmap.operation_type, - COALESCE(hmap.detail, '') as detail, - hmap.status, - hmap.command_uuid - FROM ( %s ) as ds - RIGHT JOIN host_mdm_apple_profiles hmap - ON hmap.profile_uuid = ds.profile_uuid AND hmap.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 - -- 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")) - + hmae.profile_uuid, + hmae.profile_identifier, + hmae.profile_name, + hmae.host_uuid, + hmae.checksum, + hmae.operation_type, + COALESCE(hmae.detail, '') as detail, + hmae.status, + hmae.command_uuid + FROM %s`, generateEntitiesToRemoveQuery("profile")) var profiles []*fleet.MDMAppleProfilePayload err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove) return profiles, err @@ -1890,11 +2012,11 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload return err } - var ( - args []any - sb strings.Builder - batchCount int - ) + generateValueArgs := func(p *fleet.MDMAppleBulkUpsertHostProfilePayload) (string, []any) { + valuePart := "(?, ?, ?, ?, ?, ?, ?, ?, ?)," + args := []any{p.ProfileUUID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.Checksum} + return valuePart, args + } const defaultBatchSize = 1000 // results in this times 9 placeholders batchSize := defaultBatchSize @@ -1902,30 +2024,10 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload batchSize = ds.testUpsertMDMDesiredProfilesBatchSize } - resetBatch := func() { - batchCount = 0 - args = args[:0] - sb.Reset() + if err := batchProcessDB(payload, batchSize, generateValueArgs, executeUpsertBatch); err != nil { + return err } - for _, p := range payload { - args = append(args, p.ProfileUUID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.Checksum) - sb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") - batchCount++ - - if batchCount >= batchSize { - if err := executeUpsertBatch(sb.String(), args); err != nil { - return err - } - resetBatch() - } - } - - if batchCount > 0 { - if err := executeUpsertBatch(sb.String(), args); err != nil { - return err - } - } return nil } @@ -2090,57 +2192,251 @@ func subqueryAppleProfileStatus(status fleet.MDMDeliveryStatus) (string, []any, return query, args, nil } +// subqueryAppleDeclarationStatus builds out the subquery for declaration status +func subqueryAppleDeclarationStatus() (string, []any, error) { + const declNamedStmt = ` + CASE WHEN EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d1 + WHERE + h.uuid = d1.host_uuid + AND d1.status = :failed) THEN + 'declarations_failed' + WHEN EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d2 + WHERE + h.uuid = d2.host_uuid + AND(d2.status IS NULL + OR d2.status = :pending) + AND NOT EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d3 + WHERE + h.uuid = d3.host_uuid + AND d3.status = :failed)) THEN + 'declarations_pending' + WHEN EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d4 + WHERE + h.uuid = d4.host_uuid + AND d4.status = :verifying + AND NOT EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d5 + WHERE (h.uuid = d5.host_uuid + AND(d5.status IS NULL + OR d5.status IN(:pending, :failed))))) THEN + 'declarations_verifying' + WHEN EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d6 + WHERE + h.uuid = d6.host_uuid + AND d6.status = :verified + AND NOT EXISTS ( + SELECT + 1 + FROM + host_mdm_apple_declarations d7 + WHERE (h.uuid = d7.host_uuid + AND(d7.status IS NULL + OR d7.status IN(:pending, :failed, :verifying))))) THEN + 'declarations_verified' + ELSE + '' + END` + + // TODO: do we need to differentiate between install and remove? + arg := map[string]any{ + // "install": fleet.MDMOperationTypeInstall, + // "remove": fleet.MDMOperationTypeRemove, + "verifying": fleet.MDMDeliveryVerifying, + "failed": fleet.MDMDeliveryFailed, + "verified": fleet.MDMDeliveryVerified, + "pending": fleet.MDMDeliveryPending, + } + query, args, err := sqlx.Named(declNamedStmt, arg) + if err != nil { + return "", nil, fmt.Errorf("subqueryAppleDeclarationStatus: %w", err) + } + + return query, args, nil +} + +func subqueryOSSettingsStatusMac() (string, []any, error) { + var profArgs []any + profFailed, profFailedArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryFailed) + if err != nil { + return "", nil, err + } + profArgs = append(profArgs, profFailedArgs...) + + profPending, profPendingArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryPending) + if err != nil { + return "", nil, err + } + profArgs = append(profArgs, profPendingArgs...) + + profVerifying, profVerifyingArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying) + if err != nil { + return "", nil, err + } + profArgs = append(profArgs, profVerifyingArgs...) + + profVerified, profVerifiedArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryVerified) + if err != nil { + return "", nil, err + } + profArgs = append(profArgs, profVerifiedArgs...) + + profStmt := fmt.Sprintf(` + CASE WHEN EXISTS (%s) THEN + 'profiles_failed' + WHEN EXISTS (%s) THEN + 'profiles_pending' + WHEN EXISTS (%s) THEN + 'profiles_verifying' + WHEN EXISTS (%s) THEN + 'profiles_verified' + ELSE + '' + END`, + profFailed, + profPending, + profVerifying, + profVerified, + ) + + declStmt, declArgs, err := subqueryAppleDeclarationStatus() + if err != nil { + return "", nil, err + } + + stmt := fmt.Sprintf(` + CASE (%s) + WHEN 'profiles_failed' THEN + 'failed' + WHEN 'profiles_pending' THEN ( + CASE (%s) + WHEN 'declarations_failed' THEN + 'failed' + ELSE + 'pending' + END) + WHEN 'profiles_verifying' THEN ( + CASE (%s) + WHEN 'declarations_failed' THEN + 'failed' + WHEN 'declarations_pending' THEN + 'pending' + ELSE + 'verifying' + END) + WHEN 'profiles_verified' THEN ( + CASE (%s) + WHEN 'declarations_failed' THEN + 'failed' + WHEN 'declarations_pending' THEN + 'pending' + WHEN 'declarations_verifying' THEN + 'verifying' + ELSE + 'verified' + END) + ELSE + REPLACE((%s), 'declarations_', '') + END`, profStmt, declStmt, declStmt, declStmt, declStmt) + + args := append(profArgs, declArgs...) + args = append(args, declArgs...) + args = append(args, declArgs...) + args = append(args, declArgs...) + + // FIXME(roberto): we found issues in MySQL 5.7.17 (only that version, + // which we must support for now) with prepared statements on this + // query. The results returned by the DB were always different what + // expected unless the arguments are inlined in the query. + // + // We decided to do this given: + // + // - The time constraints we were given to develop DDM + // - The fact that all the variables in this query are really strings managed by us + // - The imminent deprecation of MySQL 5.7 + return fmt.Sprintf(strings.Replace(stmt, "?", "'%s'", -1), args...), []any{}, nil +} + func (ds *Datastore) GetMDMAppleProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) { - var args []interface{} - - subqueryFailed, subqueryFailedArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryFailed) + subquery, args, err := subqueryOSSettingsStatusMac() if err != nil { - return nil, ctxerr.Wrap(ctx, err, "building failed subquery") + return nil, ctxerr.Wrap(ctx, err, "building os settings subquery") } - args = append(args, subqueryFailedArgs...) - - subqueryPending, subqueryPendingArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryPending) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "building pending subquery") - } - args = append(args, subqueryPendingArgs...) - - subqueryVerifying, subqueryVerifyingArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "building verifying subquery") - } - args = append(args, subqueryVerifyingArgs...) - - subqueryVerified, subqueryVerifiedArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryVerified) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "building verified subquery") - } - args = append(args, subqueryVerifiedArgs...) sqlFmt := ` - SELECT - COUNT(CASE WHEN EXISTS (%s) THEN 1 END) AS failed, - COUNT(CASE WHEN EXISTS (%s) THEN 1 END) AS pending, - COUNT(CASE WHEN EXISTS (%s) THEN 1 END) AS verifying, - COUNT(CASE WHEN EXISTS (%s) THEN 1 END) AS verified - FROM - hosts h - WHERE - h.platform = 'darwin' AND %s` +SELECT + %s as status, + COUNT(id) as count +FROM + hosts h +GROUP BY status, platform, team_id HAVING platform = 'darwin' AND status IN (?, ?, ?, ?) AND %s` - teamFilter := "h.team_id IS NULL" + args = append(args, fleet.MDMDeliveryFailed, fleet.MDMDeliveryPending, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified) + + teamFilter := "team_id IS NULL" if teamID != nil && *teamID > 0 { - teamFilter = "h.team_id = ?" + teamFilter = "team_id = ?" args = append(args, *teamID) } - stmt := fmt.Sprintf(sqlFmt, subqueryFailed, subqueryPending, subqueryVerifying, subqueryVerified, teamFilter) - var res fleet.MDMProfilesSummary - err = sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, args...) + stmt := fmt.Sprintf(sqlFmt, subquery, teamFilter) + + var dest []struct { + Count uint `db:"count"` + Status string `db:"status"` + } + + err = sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, args...) if err != nil { return nil, err } + byStatus := make(map[string]uint) + for _, s := range dest { + if _, ok := byStatus[s.Status]; ok { + return nil, fmt.Errorf("duplicate status %s", s.Status) + } + byStatus[s.Status] = s.Count + } + + var res fleet.MDMProfilesSummary + for s, c := range byStatus { + switch fleet.MDMDeliveryStatus(s) { + case fleet.MDMDeliveryFailed: + res.Failed = c + case fleet.MDMDeliveryPending: + res.Pending = c + case fleet.MDMDeliveryVerifying: + res.Verifying = c + case fleet.MDMDeliveryVerified: + res.Verified = c + default: + return nil, fmt.Errorf("unknown status %s", s) + } + } + return &res, nil } @@ -3080,3 +3376,608 @@ WHERE h.uuid = ? return nil } + +func (ds *Datastore) batchSetMDMAppleDeclarations(ctx context.Context, tx sqlx.ExtContext, tmID *uint, incomingDeclarations []*fleet.MDMAppleDeclaration) ([]*fleet.MDMAppleDeclaration, error) { + const insertStmt = ` +INSERT INTO mdm_apple_declarations ( + declaration_uuid, + identifier, + name, + raw_json, + checksum, + uploaded_at, + team_id +) +VALUES ( + ?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP(),? +) +ON DUPLICATE KEY UPDATE + uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()), + checksum = VALUES(checksum), + name = VALUES(name), + raw_json = VALUES(raw_json) +` + + fmtDeleteStmt := ` +DELETE FROM + mdm_apple_declarations +WHERE + team_id = ? AND %s +` + andIdentNotInList := "identifier NOT IN (?)" // added to fmtDeleteStmt if needed + + const loadExistingDecls = ` +SELECT + identifier, + declaration_uuid, + raw_json +FROM + mdm_apple_declarations +WHERE + team_id = ? AND + identifier IN (?) +` + + var declTeamID uint + if tmID != nil { + declTeamID = *tmID + } + + // build a list of identifiers for the incoming declarations, will keep the + // existing ones if there's a match and no change + incomingIdents := make([]string, len(incomingDeclarations)) + // at the same time, index the incoming declarations keyed by identifier for ease + // or processing + incomingDecls := make(map[string]*fleet.MDMAppleDeclaration, len(incomingDeclarations)) + for i, p := range incomingDeclarations { + incomingIdents[i] = p.Identifier + incomingDecls[p.Identifier] = p + } + + var existingDecls []*fleet.MDMAppleDeclaration + + if len(incomingIdents) > 0 { + // load existing declarations that match the incoming declarations by identifiers + stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingIdents) + if err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "inselect") { // TODO(JVE): do we need to create similar errors for testing decls? + if err == nil { + err = errors.New(ds.testBatchSetMDMAppleProfilesErr) + } + return nil, ctxerr.Wrap(ctx, err, "build query to load existing declarations") + } + if err := sqlx.SelectContext(ctx, tx, &existingDecls, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "select") { + if err == nil { + err = errors.New(ds.testBatchSetMDMAppleProfilesErr) + } + return nil, ctxerr.Wrap(ctx, err, "load existing declarations") + } + } + + // figure out if we need to delete any declarations + keepIdents := make([]any, 0, len(incomingIdents)) + for _, p := range existingDecls { + if newP := incomingDecls[p.Identifier]; newP != nil { + keepIdents = append(keepIdents, p.Identifier) + } + } + + var delArgs []any + var delStmt string + if len(keepIdents) == 0 { + // delete all declarations for the team + delStmt = fmt.Sprintf(fmtDeleteStmt, "TRUE") + delArgs = []any{declTeamID} + } else { + // delete the obsolete declarations (all those that are not in keepIdents) + stmt, args, err := sqlx.In(fmt.Sprintf(fmtDeleteStmt, andIdentNotInList), declTeamID, keepIdents) + // if err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "inselect") { // TODO(JVE): do we need to create similar errors for testing decls? + // if err == nil { + // err = errors.New(ds.testBatchSetMDMAppleProfilesErr) + // } + // return nil, ctxerr.Wrap(ctx, err, "build query to load existing declarations") + // } + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "build query to delete obsolete profiles") + } + delStmt = stmt + delArgs = args + } + + if _, err := tx.ExecContext(ctx, delStmt, delArgs...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "delete") { + if err == nil { + err = errors.New(ds.testBatchSetMDMAppleProfilesErr) + } + return nil, ctxerr.Wrap(ctx, err, "delete obsolete declarations") + } + + for _, d := range incomingDeclarations { + checksum := md5ChecksumScriptContent(string(d.RawJSON)) + declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString() + if _, err := tx.ExecContext(ctx, insertStmt, + declUUID, + d.Identifier, + d.Name, + d.RawJSON, + checksum, + declTeamID); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") { + if err == nil { + err = errors.New(ds.testBatchSetMDMAppleProfilesErr) + } + return nil, ctxerr.Wrapf(ctx, err, "insert new/edited declaration with identifier %q", d.Identifier) + } + } + + incomingLabels := []fleet.ConfigurationProfileLabel{} + if len(incomingIdents) > 0 { + var newlyInsertedDecls []*fleet.MDMAppleDeclaration + // load current declarations (again) that match the incoming declarations by name to grab their uuids + // this is an easy way to grab the identifiers for both the existing declarations and the new ones we generated. + // + // TODO(roberto): if we're a bit careful, we can harvest this + // information without this extra request in the previous DB + // calls. Due to time constraints, I'm leaving that + // optimization for a later iteration. + stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingIdents) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "build query to load newly inserted declarations") + } + if err := sqlx.SelectContext(ctx, tx, &newlyInsertedDecls, stmt, args...); err != nil { + return nil, ctxerr.Wrap(ctx, err, "load newly inserted declarations") + } + + for _, newlyInsertedDecl := range newlyInsertedDecls { + incomingDecl, ok := incomingDecls[newlyInsertedDecl.Identifier] + if !ok { + return nil, ctxerr.Wrapf(ctx, err, "declaration %q is in the database but was not incoming", newlyInsertedDecl.Identifier) + } + + for _, label := range incomingDecl.Labels { + label.ProfileUUID = newlyInsertedDecl.DeclarationUUID + incomingLabels = append(incomingLabels, label) + } + } + } + + if err := batchSetDeclarationLabelAssociationsDB(ctx, tx, incomingLabels); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "labels") { + if err == nil { + err = errors.New(ds.testBatchSetMDMAppleProfilesErr) + } + return nil, ctxerr.Wrap(ctx, err, "inserting apple declaration label associations") + } + + return incomingDeclarations, nil +} + +func (ds *Datastore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString() + checksum := md5ChecksumScriptContent(string(declaration.RawJSON)) + + stmt := ` +INSERT INTO mdm_apple_declarations ( + declaration_uuid, + team_id, + identifier, + name, + raw_json, + checksum, + uploaded_at) +(SELECT ?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP() FROM DUAL WHERE + NOT EXISTS ( + SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ? + ) AND NOT EXISTS ( + SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ? + ) +)` + + var tmID uint + if declaration.TeamID != nil { + tmID = *declaration.TeamID + } + + err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { + res, err := tx.ExecContext(ctx, stmt, + declUUID, tmID, declaration.Identifier, declaration.Name, declaration.RawJSON, checksum, declaration.Name, tmID, declaration.Name, tmID) + if err != nil { + switch { + case isDuplicate(err): + return ctxerr.Wrap(ctx, formatErrorDuplicateDeclaration(err, declaration)) + default: + return ctxerr.Wrap(ctx, err, "creating new apple mdm declaration") + } + } + + aff, _ := res.RowsAffected() + if aff == 0 { + return &existsError{ + ResourceType: "MDMAppleDeclaration.Name", + Identifier: declaration.Name, + TeamID: declaration.TeamID, + } + } + + for i := range declaration.Labels { + declaration.Labels[i].ProfileUUID = declUUID + } + if err := batchSetDeclarationLabelAssociationsDB(ctx, tx, declaration.Labels); err != nil { + return ctxerr.Wrap(ctx, err, "inserting mdm declaration label associations") + } + + return nil + }) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "inserting declaration and label associations") + } + + declaration.DeclarationUUID = declUUID + return declaration, nil +} + +func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtContext, declarationLabels []fleet.ConfigurationProfileLabel) error { + if len(declarationLabels) == 0 { + return nil + } + + // 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_declaration_labels + WHERE (apple_declaration_uuid, label_id) NOT IN (%s) AND + apple_declaration_uuid IN (?) + ` + + upsertStmt := ` + INSERT INTO mdm_declaration_labels + (apple_declaration_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 declarationLabels { + 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, insertBuilder.String()), insertParams...) + if err != nil { + if isChildForeignKeyError(err) { + // one of the provided labels doesn't exist + return foreignKey("mdm_declaration_labels", fmt.Sprintf("(declaration, label)=(%v)", insertParams)) + } + + return ctxerr.Wrap(ctx, err, "setting label associations for declarations") + } + + deleteStmt = fmt.Sprintf(deleteStmt, deleteBuilder.String()) + + 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 declarations") + } + if _, err := tx.ExecContext(ctx, deleteStmt, args...); err != nil { + return ctxerr.Wrap(ctx, err, "deleting labels for declarations") + } + + return nil +} + +func (ds *Datastore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) { + const stmt = ` +SELECT + md5((count(0) + group_concat(hex(mad.checksum) + ORDER BY + mad.uploaded_at DESC separator ''))) AS checksum, + max(mad.created_at) AS latest_created_timestamp +FROM + host_mdm_apple_declarations hmad + JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid +WHERE + hmad.host_uuid = ?` + + var res fleet.MDMAppleDDMDeclarationsToken + if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID); err != nil { + return nil, ctxerr.Wrap(ctx, err, "get DDM declarations token") + } + + return &res, nil +} + +func (ds *Datastore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error) { + const stmt = ` +SELECT + HEX(mad.checksum) as checksum, + mad.identifier +FROM + host_mdm_apple_declarations hmad + JOIN mdm_apple_declarations mad ON mad.declaration_uuid = hmad.declaration_uuid +WHERE + hmad.host_uuid = ? AND operation_type = ?` + + var res []fleet.MDMAppleDDMDeclarationItem + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, fleet.MDMOperationTypeInstall); err != nil { + return nil, ctxerr.Wrap(ctx, err, "get DDM declaration items") + } + + return res, nil +} + +func (ds *Datastore) MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error) { + // TODO: When hosts table is indexed by uuid, consider joining on hosts to ensure that the + // declaration for the host's current team is returned. In the case where the specified + // identifier is not unique to the team, the cron should ensure that any conflicting + // declarations are removed, but the join would provide an extra layer of safety. + const stmt = ` +SELECT + mad.raw_json, HEX(mad.checksum) as checksum +FROM + host_mdm_apple_declarations hmad + JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid +WHERE + host_uuid = ? AND identifier = ? AND operation_type = ?` + + var res fleet.MDMAppleDeclaration + if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, identifier, fleet.MDMOperationTypeInstall); err != nil { + if err == sql.ErrNoRows { + return nil, notFound("MDMAppleDeclaration").WithName(identifier) + } + return nil, ctxerr.Wrap(ctx, err, "get ddm declarations response") + } + + return &res, nil +} + +func (ds *Datastore) MDMAppleBatchSetHostDeclarationState(ctx context.Context) ([]string, error) { + var uuids []string + + const defaultBatchSize = 1000 + batchSize := defaultBatchSize + if ds.testUpsertMDMDesiredProfilesBatchSize > 0 { + batchSize = ds.testUpsertMDMDesiredProfilesBatchSize + } + + err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + var err error + uuids, err = mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, &fleet.MDMDeliveryPending) + return err + }) + + return uuids, ctxerr.Wrap(ctx, err, "upserting host declaration state") +} + +func mdmAppleBatchSetHostDeclarationStateDB(ctx context.Context, tx sqlx.ExtContext, batchSize int, status *fleet.MDMDeliveryStatus) ([]string, error) { + // once all the declarations are in place, compute the desired state + // and find which hosts need a DDM sync. + changedDeclarations, err := mdmAppleGetHostsWithChangedDeclarationsDB(ctx, tx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "find hosts with changed declarations") + } + + if len(changedDeclarations) == 0 { + return []string{}, nil + } + + // a host might have more than one declaration to sync, we do this to + // collect unique host UUIDs in order to send a single command to each + // host in the next step + uuidMap := map[string]struct{}{} + for _, d := range changedDeclarations { + uuidMap[d.HostUUID] = struct{}{} + } + uuids := make([]string, 0, len(uuidMap)) + for uuid := range uuidMap { + uuids = append(uuids, uuid) + } + + // mark the host declarations as pending, this serves two purposes: + // + // - support the APIs/methods that track host status (summaries, filters, etc) + // + // - support the DDM endpoints, which use data from the + // `host_mdm_apple_declarations` table to compute which declarations to + // serve + if err := mdmAppleBatchSetPendingHostDeclarationsDB(ctx, tx, batchSize, changedDeclarations, status); err != nil { + return nil, ctxerr.Wrap(ctx, err, "batch insert mdm apple host declarations") + } + + return uuids, nil +} + +// mdmAppleBatchSetPendingHostDeclarationsDB tracks the current status of all +// the host declarations provided. +func mdmAppleBatchSetPendingHostDeclarationsDB( + ctx context.Context, + tx sqlx.ExtContext, + batchSize int, + changedDeclarations []*fleet.MDMAppleHostDeclaration, + status *fleet.MDMDeliveryStatus, +) error { + baseStmt := ` + INSERT INTO host_mdm_apple_declarations + (host_uuid, status, operation_type, checksum, declaration_uuid, declaration_identifier, declaration_name) + VALUES + %s + ON DUPLICATE KEY UPDATE + status = VALUES(status), + operation_type = VALUES(operation_type), + checksum = VALUES(checksum) + ` + + executeUpsertBatch := func(valuePart string, args []any) error { + _, err := tx.ExecContext( + ctx, + fmt.Sprintf(baseStmt, strings.TrimSuffix(valuePart, ",")), + args..., + ) + return err + } + + generateValueArgs := func(d *fleet.MDMAppleHostDeclaration) (string, []any) { + valuePart := "(?, ?, ?, ?, ?, ?, ?)," + args := []any{d.HostUUID, status, d.OperationType, d.Checksum, d.DeclarationUUID, d.Identifier, d.Name} + return valuePart, args + } + + err := batchProcessDB(changedDeclarations, batchSize, generateValueArgs, executeUpsertBatch) + return ctxerr.Wrap(ctx, err, "inserting changed host declaration state") +} + +// mdmAppleGetHostsWithChangedDeclarationsDB returns a +// MDMAppleHostDeclaration item for each (host x declaration) pair that +// needs an status change, this includes declarations to install and +// declarations to be removed. Those can be differentiated by the +// OperationType field on each struct. +func mdmAppleGetHostsWithChangedDeclarationsDB(ctx context.Context, tx sqlx.ExtContext) ([]*fleet.MDMAppleHostDeclaration, error) { + stmt := fmt.Sprintf(` + ( + SELECT + ds.host_uuid, + 'install' as operation_type, + ds.checksum, + ds.declaration_uuid, + ds.declaration_identifier, + ds.declaration_name + FROM + %s + ) + UNION ALL + ( + SELECT + hmae.host_uuid, + 'remove' as operation_type, + hmae.checksum, + hmae.declaration_uuid, + hmae.declaration_identifier, + hmae.declaration_name + FROM + %s + ) + `, + generateEntitiesToInstallQuery("declaration"), + generateEntitiesToRemoveQuery("declaration"), + ) + + var decls []*fleet.MDMAppleHostDeclaration + if err := sqlx.SelectContext(ctx, tx, &decls, stmt, fleet.MDMOperationTypeRemove, fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove); err != nil { + return nil, ctxerr.Wrap(ctx, err, "running sql statement") + } + return decls, nil +} + +func (ds *Datastore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error { + getHostDeclarationsStmt := ` + SELECT host_uuid, status, operation_type, HEX(checksum) as checksum, declaration_uuid, declaration_identifier, declaration_name + FROM host_mdm_apple_declarations + WHERE host_uuid = ? + ` + + updateHostDeclarationsStmt := ` +INSERT INTO host_mdm_apple_declarations + (host_uuid, declaration_uuid, status, operation_type, detail, declaration_name, declaration_identifier, checksum) +VALUES + %s +ON DUPLICATE KEY UPDATE + status = VALUES(status), + operation_type = VALUES(operation_type), + detail = VALUES(detail) + ` + + deletePendingRemovesStmt := ` + DELETE FROM host_mdm_apple_declarations + WHERE host_uuid = ? AND operation_type = 'remove' AND (status = 'pending' OR status IS NULL) + ` + + var current []*fleet.MDMAppleHostDeclaration + if err := sqlx.SelectContext(ctx, ds.reader(ctx), ¤t, getHostDeclarationsStmt, hostUUID); err != nil { + return ctxerr.Wrap(ctx, err, "getting current host declarations") + } + + updatesByChecksum := make(map[string]*fleet.MDMAppleHostDeclaration, len(updates)) + for _, u := range updates { + updatesByChecksum[u.Checksum] = u + } + + var args []any + var insertVals strings.Builder + for _, c := range current { + if u, ok := updatesByChecksum[c.Checksum]; ok { + insertVals.WriteString("(?, ?, ?, ?, ?, ?, ?, UNHEX(?)),") + args = append(args, hostUUID, c.DeclarationUUID, u.Status, u.OperationType, u.Detail, c.Identifier, c.Name, c.Checksum) + } + } + + err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + if len(args) != 0 { + stmt := fmt.Sprintf(updateHostDeclarationsStmt, strings.TrimSuffix(insertVals.String(), ",")) + if _, err := tx.ExecContext(ctx, stmt, args...); err != nil { + return ctxerr.Wrap(ctx, err, "updating existing declarations") + } + } + + if _, err := tx.ExecContext(ctx, deletePendingRemovesStmt, hostUUID); err != nil { + return ctxerr.Wrap(ctx, err, "deleting pending removals") + } + + return nil + }) + + return ctxerr.Wrap(ctx, err, "updating host declarations") +} + +func (ds *Datastore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error { + stmt := ` + UPDATE host_mdm_apple_declarations + SET status = ? + WHERE + operation_type = ? + AND status = ? + AND host_uuid = ? + ` + + _, err := ds.writer(ctx).ExecContext( + ctx, stmt, fleet.MDMDeliveryVerifying, + fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending, hostUUID, + ) + return ctxerr.Wrap(ctx, err, "updating host declaration status to verifying") +} + +func (ds *Datastore) InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType, rawJSON string) error { + const stmt = ` +INSERT INTO + mdm_apple_declarative_requests ( + enrollment_id, + message_type, + raw_json + ) +VALUES + (?, ?, ?) +` + if _, err := ds.writer(ctx).ExecContext(ctx, stmt, hostUUID, messageType, rawJSON); err != nil { + return ctxerr.Wrap(ctx, err, "writing apple declarative request to db") + } + + return nil +} diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index ed38fe7b2a..d989cc2719 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -992,6 +992,80 @@ func expectAppleProfiles( return m } +func expectAppleDeclarations( + t *testing.T, + ds *Datastore, + tmID *uint, + want []*fleet.MDMAppleDeclaration, +) map[string]string { + if tmID == nil { + tmID = ptr.Uint(0) + } + + var got []*fleet.MDMAppleDeclaration + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + ctx := context.Background() + return sqlx.SelectContext(ctx, q, &got, `SELECT * FROM mdm_apple_declarations WHERE team_id = ?`, tmID) + }) + + // create map of expected declarations keyed by identifier + wantMap := make(map[string]*fleet.MDMAppleDeclaration, len(want)) + for _, cp := range want { + wantMap[cp.Identifier] = cp + } + + JSONRemarshal := func(bytes []byte) ([]byte, error) { + var ifce interface{} + err := json.Unmarshal(bytes, &ifce) + if err != nil { + return nil, err + } + return json.Marshal(ifce) + } + + // compare only the fields we care about, and build the resulting map of + // declaration identifier as key to declaration UUID as value + m := make(map[string]string) + for _, gotD := range got { + + wantD := wantMap[gotD.Identifier] + + m[gotD.Identifier] = gotD.DeclarationUUID + if gotD.TeamID != nil && *gotD.TeamID == 0 { + gotD.TeamID = nil + } + + // DeclarationUUID is non-empty and starts with "d", but otherwise we don't + // care about it for test assertions. + require.NotEmpty(t, gotD.DeclarationUUID) + require.True(t, strings.HasPrefix(gotD.DeclarationUUID, fleet.MDMAppleDeclarationUUIDPrefix)) + gotD.DeclarationUUID = "" + gotD.Checksum = "" // don't care about md5checksum here + + gotD.CreatedAt = time.Time{} + + gotBytes, err := JSONRemarshal(gotD.RawJSON) + require.NoError(t, err) + + wantBytes, err := JSONRemarshal(wantD.RawJSON) + require.NoError(t, err) + + require.Equal(t, wantBytes, gotBytes) + + // if an expected uploaded_at timestamp is provided for this declaration, keep + // its value, otherwise clear it as we don't care about asserting its + // value. + if wantD == nil || wantD.UploadedAt.IsZero() { + gotD.UploadedAt = time.Time{} + } + + require.Equal(t, wantD.Name, gotD.Name) + require.Equal(t, wantD.Identifier, gotD.Identifier) + require.Equal(t, wantD.Labels, gotD.Labels) + } + return m +} + func testBatchSetMDMAppleProfiles(t *testing.T, ds *Datastore) { ctx := context.Background() applyAndExpect := func(newSet []*fleet.MDMAppleConfigProfile, tmID *uint, want []*fleet.MDMAppleConfigProfile) map[string]string { @@ -1170,6 +1244,30 @@ func configProfileForTest(t *testing.T, name, identifier, uuid string, labels .. return cp } +func declForTest(name, identifier, payloadContent string, labels ...*fleet.Label) *fleet.MDMAppleDeclaration { + tmpl := `{ + "Type": "com.apple.configuration.decl%s", + "Identifier": "com.fleet.config%s", + "Payload": { + "ServiceType": "com.apple.service%s" + } + }` + + declBytes := []byte(fmt.Sprintf(tmpl, identifier, identifier, payloadContent)) + + decl := &fleet.MDMAppleDeclaration{ + RawJSON: declBytes, + Identifier: fmt.Sprintf("com.fleet.config%s", identifier), + Name: name, + } + + for _, l := range labels { + decl.Labels = append(decl.Labels, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID}) + } + + return decl +} + func teamConfigProfileForTest(t *testing.T, name, identifier, uuid string, teamID uint) *fleet.MDMAppleConfigProfile { prof := configProfileBytesForTest(name, identifier, uuid) cp, err := fleet.NewMDMAppleConfigProfile(configProfileBytesForTest(name, identifier, uuid), &teamID) @@ -1689,13 +1787,19 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) expectedIDs = append(expectedIDs, h.ID) } - gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID}) + gotHosts, err := ds.ListHosts( + ctx, + fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, + fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID}, + ) gotIDs := []uint{} for _, h := range gotHosts { gotIDs = append(gotIDs, h.ID) } - return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs) + return assert.NoError(t, err) && + assert.Len(t, gotHosts, len(expected)) && + assert.ElementsMatch(t, expectedIDs, gotIDs) } var hosts []*fleet.Host @@ -2517,7 +2621,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(2), allProfilesSummary.Pending) require.Equal(t, uint(0), allProfilesSummary.Failed) require.Equal(t, uint(1), allProfilesSummary.Verifying) @@ -2543,7 +2647,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(2), allProfilesSummary.Pending) require.Equal(t, uint(0), allProfilesSummary.Failed) require.Equal(t, uint(1), allProfilesSummary.Verifying) @@ -2571,7 +2675,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(2), allProfilesSummary.Pending) require.Equal(t, uint(0), allProfilesSummary.Failed) require.Equal(t, uint(1), allProfilesSummary.Verifying) @@ -2593,7 +2697,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(2), allProfilesSummary.Pending) require.Equal(t, uint(1), allProfilesSummary.Failed) require.Equal(t, uint(1), allProfilesSummary.Verifying) @@ -2615,7 +2719,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(3), allProfilesSummary.Pending) require.Equal(t, uint(1), allProfilesSummary.Failed) require.Equal(t, uint(1), allProfilesSummary.Verifying) @@ -2645,7 +2749,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(0), allProfilesSummary.Pending) require.Equal(t, uint(0), allProfilesSummary.Failed) require.Equal(t, uint(1), allProfilesSummary.Verifying) @@ -2671,7 +2775,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID) require.NoError(t, err) - require.NotNil(t, fvProfileSummary) + require.NotNil(t, allProfilesSummary) require.Equal(t, uint(0), allProfilesSummary.Pending) require.Equal(t, uint(0), allProfilesSummary.Failed) require.Equal(t, uint(0), allProfilesSummary.Verifying) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index ca8986e2e5..7d94e41c9e 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1211,34 +1211,22 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par return sql, params, nil } - newSQL := "" + whereStatus := "" + // macOS settings filter is not compatible with the "all teams" option so append the "no + // team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) if opt.TeamFilter == nil { - // macOS settings filter is not compatible with the "all teams" option so append the "no - // team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) - newSQL += ` AND h.team_id IS NULL` + whereStatus += ` AND h.team_id IS NULL` } - var subquery string - var subqueryParams []any - var err error - switch opt.MacOSSettingsFilter { - case fleet.OSSettingsFailed: - subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryFailed) - case fleet.OSSettingsPending: - subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryPending) - case fleet.OSSettingsVerifying: - subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying) - case fleet.OSSettingsVerified: - subquery, subqueryParams, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerified) - } + subqueryStatus, paramsStatus, err := subqueryOSSettingsStatusMac() if err != nil { - return "", nil, fmt.Errorf("building subquery for %s filter: %w", opt.MacOSSettingsFilter, err) - } - if subquery != "" { - newSQL += fmt.Sprintf(` AND EXISTS (%s)`, subquery) + return "", nil, err } - return sql + newSQL, append(params, subqueryParams...), nil + whereStatus += fmt.Sprintf(` AND %s = ?`, subqueryStatus) + paramsStatus = append(paramsStatus, opt.MacOSSettingsFilter) + + return sql + whereStatus, append(params, paramsStatus...), nil } func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { @@ -1286,30 +1274,16 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis sqlFmt += ` AND h.team_id IS NULL` } var whereMacOS, whereWindows string - sqlFmt += ` AND ((h.platform = 'windows' AND (%s)) OR (h.platform = 'darwin' AND (%s)))` + sqlFmt += ` +AND ((h.platform = 'windows' AND (%s)) +OR (h.platform = 'darwin' AND (%s)))` - // construct the WHERE for macOS - var subqueryMacOS string - var paramsMacOS []interface{} - var err error - switch opt.OSSettingsFilter { - case fleet.OSSettingsFailed: - subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryFailed) - case fleet.OSSettingsPending: - subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryPending) - case fleet.OSSettingsVerifying: - subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying) - case fleet.OSSettingsVerified: - subqueryMacOS, paramsMacOS, err = subqueryAppleProfileStatus(fleet.MDMDeliveryVerified) - } + whereMacOS, paramsMacOS, err := subqueryOSSettingsStatusMac() if err != nil { - return "", nil, fmt.Errorf("building subquery for %s filter: %w", opt.OSSettingsFilter, err) - } - if subqueryMacOS != "" { - whereMacOS = "EXISTS (" + subqueryMacOS + ")" - } else { - whereMacOS = "FALSE" + return "", nil, err } + whereMacOS += ` = ?` + paramsMacOS = append(paramsMacOS, opt.OSSettingsFilter) // construct the WHERE for windows whereWindows = `hmdm.name = ? AND hmdm.enrolled = 1 AND hmdm.is_server = 0` diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index eb1ddcd9fa..ea9d455c9a 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -364,6 +364,7 @@ func testLabelsListHostsInLabelAndStatus(t *testing.T, db *Datastore) { NodeKey: ptr.String("1"), UUID: "1", Hostname: "foo.local", + Platform: "darwin", }) require.NoError(t, err) @@ -377,6 +378,7 @@ func testLabelsListHostsInLabelAndStatus(t *testing.T, db *Datastore) { NodeKey: ptr.String("2"), UUID: "2", Hostname: "bar.local", + Platform: "darwin", }) require.NoError(t, err) h3, err := db.NewHost(context.Background(), &fleet.Host{ @@ -388,6 +390,7 @@ func testLabelsListHostsInLabelAndStatus(t *testing.T, db *Datastore) { NodeKey: ptr.String("3"), UUID: "3", Hostname: "baz.local", + Platform: "darwin", }) require.NoError(t, err) @@ -427,6 +430,7 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da NodeKey: ptr.String("1"), UUID: "1", Hostname: "foo.local", + Platform: "darwin", }) require.Nil(t, err) @@ -440,6 +444,7 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da NodeKey: ptr.String("2"), UUID: "2", Hostname: "bar.local", + Platform: "darwin", }) require.Nil(t, err) diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index 8960bb6287..0ef788c464 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -82,7 +82,6 @@ func (ds *Datastore) ListMDMCommands( tmFilter fleet.TeamFilter, listOpts *fleet.MDMCommandListOptions, ) ([]*fleet.MDMCommand, error) { - jointStmt := getCombinedMDMCommandsQuery() + ds.whereFilterHostsByTeams(tmFilter, "h") jointStmt, params := appendListOptionsWithCursorToSQL(jointStmt, nil, &listOpts.ListOptions) var results []*fleet.MDMCommand @@ -102,7 +101,7 @@ func (ds *Datastore) getMDMCommand(ctx context.Context, q sqlx.QueryerContext, c return &cmd, nil } -func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error { +func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error { return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { if err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles); err != nil { return ctxerr.Wrap(ctx, err, "batch set windows profiles") @@ -112,6 +111,10 @@ func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macPro return ctxerr.Wrap(ctx, err, "batch set apple profiles") } + if _, err := ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations); err != nil { + return ctxerr.Wrap(ctx, err, "batch set apple declarations") + } + return nil }) } @@ -122,6 +125,7 @@ func (ds *Datastore) ListMDMConfigProfiles(ctx context.Context, teamID *uint, op var profs []*fleet.MDMConfigProfilePayload + // TODO(roberto): Consider using UNION ALL here, as we know there won't be any duplicates between the tables. const selectStmt = ` SELECT profile_uuid, @@ -164,6 +168,20 @@ FROM ( WHERE team_id = ? AND name NOT IN (?) + + UNION + + SELECT + declaration_uuid AS profile_uuid, + team_id, + name, + 'darwin' AS platform, + identifier, + checksum AS checksum, + created_at, + uploaded_at + FROM mdm_apple_declarations + WHERE team_id = ? ) as combined_profiles ` @@ -183,7 +201,7 @@ FROM ( fleetNames = append(fleetNames, k) } - args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames} + args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID} stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt) stmt, args, err := sqlx.In(stmt, args...) @@ -205,15 +223,20 @@ FROM ( } // load the labels associated with those profiles - var winProfUUIDs, macProfUUIDs []string + var winProfUUIDs, macProfUUIDs, macDeclUUIDs []string for _, prof := range profs { if prof.Platform == "windows" { winProfUUIDs = append(winProfUUIDs, prof.ProfileUUID) } else { + if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) { + macDeclUUIDs = append(macDeclUUIDs, prof.ProfileUUID) + continue + } + macProfUUIDs = append(macProfUUIDs, prof.ProfileUUID) } } - labels, err := ds.listProfileLabelsForProfiles(ctx, winProfUUIDs, macProfUUIDs) + labels, err := ds.listProfileLabelsForProfiles(ctx, winProfUUIDs, macProfUUIDs, macDeclUUIDs) if err != nil { return nil, nil, err } @@ -232,7 +255,7 @@ FROM ( return profs, metaData, nil } -func (ds *Datastore) listProfileLabelsForProfiles(ctx context.Context, winProfUUIDs, macProfUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) { +func (ds *Datastore) listProfileLabelsForProfiles(ctx context.Context, winProfUUIDs, macProfUUIDs, macDeclUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) { // load the labels associated with those profiles const labelsStmt = ` SELECT @@ -245,6 +268,16 @@ FROM WHERE mcpl.apple_profile_uuid IN (?) OR mcpl.windows_profile_uuid IN (?) +UNION ALL +SELECT + apple_declaration_uuid as profile_uuid, + label_name, + COALESCE(label_id, 0) as label_id, + IF(label_id IS NULL, 1, 0) as broken +FROM + mdm_declaration_labels mdl +WHERE + mdl.apple_declaration_uuid IN (?) ORDER BY profile_uuid, label_name ` @@ -257,8 +290,11 @@ ORDER BY if len(macProfUUIDs) == 0 { macProfUUIDs = []string{"-"} } + if len(macDeclUUIDs) == 0 { + macDeclUUIDs = []string{"-"} + } - stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs) + stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs, macDeclUUIDs) if err != nil { return nil, ctxerr.Wrap(ctx, err, "sqlx.In to list labels for profiles") } @@ -415,6 +451,20 @@ WHERE return ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles") } + const defaultBatchSize = 1000 + batchSize := defaultBatchSize + if ds.testUpsertMDMDesiredProfilesBatchSize > 0 { + batchSize = ds.testUpsertMDMDesiredProfilesBatchSize + } + // TODO(roberto): this method currently sets the state of all + // declarations for all hosts. I don't see an immediate concern + // (and my hunch is that we could even do the same for + // profiles) but this could be optimized to use only a provided + // set of host uuids. + if _, err := mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil); err != nil { + return ctxerr.Wrap(ctx, err, "bulk set pending apple declarations") + } + return nil }) } diff --git a/server/datastore/mysql/mdm_test.go b/server/datastore/mysql/mdm_test.go index 6cd8d0c783..3f4988d13e 100644 --- a/server/datastore/mysql/mdm_test.go +++ b/server/datastore/mysql/mdm_test.go @@ -7,6 +7,7 @@ import ( "fmt" "sort" "strconv" + "strings" "testing" "time" @@ -43,12 +44,12 @@ func TestMDMShared(t *testing.T) { {"TestMDMEULA", testMDMEULA}, {"TestGetHostCertAssociationsToExpire", testSCEPRenewalHelpers}, {"TestSCEPRenewalHelpers", testSCEPRenewalHelpers}, + {"TestMDMProfilesSummaryAndHostFilters", testMDMProfilesSummaryAndHostFilters}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { defer TruncateTables(t, ds) - c.fn(t, ds) }) } @@ -189,15 +190,18 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { applyAndExpect := func( newAppleSet []*fleet.MDMAppleConfigProfile, newWindowsSet []*fleet.MDMWindowsConfigProfile, + newAppleDeclSet []*fleet.MDMAppleDeclaration, tmID *uint, wantApple []*fleet.MDMAppleConfigProfile, wantWindows []*fleet.MDMWindowsConfigProfile, + wantAppleDecl []*fleet.MDMAppleDeclaration, ) { ctx := context.Background() - err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet) + err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet, newAppleDeclSet) require.NoError(t, err) expectAppleProfiles(t, ds, tmID, wantApple) expectWindowsProfiles(t, ds, tmID, wantWindows) + expectAppleDeclarations(t, ds, tmID, wantAppleDecl) } withTeamIDApple := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile { @@ -205,30 +209,39 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { return p } + withTeamIDDecl := func(d *fleet.MDMAppleDeclaration, tmID uint) *fleet.MDMAppleDeclaration { + d.TeamID = &tmID + return d + } + withTeamIDWindows := func(p *fleet.MDMWindowsConfigProfile, tmID uint) *fleet.MDMWindowsConfigProfile { p.TeamID = &tmID return p } // empty set for no team (both Apple and Windows) - applyAndExpect(nil, nil, nil, nil, nil) + applyAndExpect(nil, nil, nil, nil, nil, nil, nil) // single Apple and Windows profile set for a specific team applyAndExpect( []*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")}, []*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")}, + []*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")}, ptr.Uint(1), []*fleet.MDMAppleConfigProfile{withTeamIDApple(configProfileForTest(t, "N1", "I1", "a"), 1)}, []*fleet.MDMWindowsConfigProfile{withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1)}, + []*fleet.MDMAppleDeclaration{withTeamIDDecl(declForTest("D1", "D1", "foo"), 1)}, ) // single Apple and Windows profile set for no team applyAndExpect( []*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")}, []*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")}, + []*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")}, nil, []*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")}, []*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")}, + []*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")}, ) // new Apple and Windows profile sets for a specific team @@ -241,6 +254,10 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "W1", "l1"), // unchanged windowsConfigProfileForTest(t, "W2", "l2"), }, + []*fleet.MDMAppleDeclaration{ + declForTest("D1", "D1", "foo"), // unchanged + declForTest("D2", "D2", "foo"), + }, ptr.Uint(1), []*fleet.MDMAppleConfigProfile{ withTeamIDApple(configProfileForTest(t, "N1", "I1", "a"), 1), @@ -250,6 +267,10 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1), withTeamIDWindows(windowsConfigProfileForTest(t, "W2", "l2"), 1), }, + []*fleet.MDMAppleDeclaration{ + withTeamIDDecl(declForTest("D1", "D1", "foo"), 1), + withTeamIDDecl(declForTest("D2", "D2", "foo"), 1), + }, ) // edited profiles, unchanged profiles, and new profiles for a specific team @@ -264,6 +285,11 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "W2", "l2"), // unchanged windowsConfigProfileForTest(t, "W3", "l3"), // new }, + []*fleet.MDMAppleDeclaration{ + declForTest("D1", "D1", "foo-updated"), // content updated + declForTest("D2", "D2", "foo"), // unchanged + declForTest("D3", "D3", "bar"), // new + }, ptr.Uint(1), []*fleet.MDMAppleConfigProfile{ withTeamIDApple(configProfileForTest(t, "N1", "I1", "a-updated"), 1), @@ -275,6 +301,11 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { withTeamIDWindows(windowsConfigProfileForTest(t, "W2", "l2"), 1), withTeamIDWindows(windowsConfigProfileForTest(t, "W3", "l3"), 1), }, + []*fleet.MDMAppleDeclaration{ + withTeamIDDecl(declForTest("D1", "D1", "foo-updated"), 1), + withTeamIDDecl(declForTest("D2", "D2", "foo"), 1), + withTeamIDDecl(declForTest("D3", "D3", "bar"), 1), + }, ) // new Apple and Windows profiles to no team @@ -287,6 +318,10 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "W4", "l4"), windowsConfigProfileForTest(t, "W5", "l5"), }, + []*fleet.MDMAppleDeclaration{ + declForTest("D5", "D4", "foo"), + declForTest("D4", "D5", "foo"), + }, nil, []*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N4", "I4", "d"), @@ -296,10 +331,14 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "W4", "l4"), windowsConfigProfileForTest(t, "W5", "l5"), }, + []*fleet.MDMAppleDeclaration{ + declForTest("D5", "D4", "foo"), + declForTest("D4", "D5", "foo"), + }, ) // Test Case 8: Clear profiles for a specific team - applyAndExpect(nil, nil, ptr.Uint(1), nil, nil) + applyAndExpect(nil, nil, nil, ptr.Uint(1), nil, nil, nil) } func testListMDMConfigProfiles(t *testing.T, ds *Datastore) { @@ -612,9 +651,30 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { wantProfs = sortProfs(wantProfs) for i, wp := range wantProfs { gp := gotProfs[i] - require.Equal(t, wp.ProfileUUID, gp.ProfileUUID, "host uuid: %s, prof id or name: %s", h.UUID, gp.IdentifierOrName) - require.Equal(t, wp.Status, gp.Status, "host uuid: %s, prof id or name: %s", h.UUID, gp.IdentifierOrName) - require.Equal(t, wp.OperationType, gp.OperationType, "host uuid: %s, prof id or name: %s", h.UUID, gp.IdentifierOrName) + require.Equal( + t, + wp.ProfileUUID, + gp.ProfileUUID, + "host uuid: %s, prof id or name: %s", + h.UUID, + gp.IdentifierOrName, + ) + require.Equal( + t, + wp.Status, + gp.Status, + "host uuid: %s, prof id or name: %s", + h.UUID, + gp.IdentifierOrName, + ) + require.Equal( + t, + wp.OperationType, + gp.OperationType, + "host uuid: %s, prof id or name: %s", + h.UUID, + gp.IdentifierOrName, + ) } } } @@ -719,18 +779,22 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { configProfileForTest(t, "G2a", "G2a", "b"), configProfileForTest(t, "G3a", "G3a", "c"), } + macGlobalDeclarations := []*fleet.MDMAppleDeclaration{ + declForTest("G1d", "G1d", "foo"), + declForTest("G2d", "G2d", "bar"), + } winGlobalProfiles := []*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "G1w", "L1"), windowsConfigProfileForTest(t, "G2w", "L2"), windowsConfigProfileForTest(t, "G3w", "L3"), } - err = ds.BatchSetMDMProfiles(ctx, nil, macGlobalProfiles, winGlobalProfiles) + err = ds.BatchSetMDMProfiles(ctx, nil, macGlobalProfiles, winGlobalProfiles, macGlobalDeclarations) require.NoError(t, err) macGlobalProfiles, err = ds.ListMDMAppleConfigProfiles(ctx, nil) require.NoError(t, err) require.Len(t, macGlobalProfiles, 3) globalProfiles := getProfs(nil) - require.Len(t, globalProfiles, 6) + require.Len(t, globalProfiles, 8) // list profiles to install, should result in the global profiles for all // enrolled hosts @@ -754,36 +818,153 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -812,37 +993,148 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { require.Len(t, toRemoveWindows, 3) // update status of the moved host (team has no profiles) - err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(darwinHosts[0], windowsHosts[0]), nil, nil, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + hostIDsFromHosts(darwinHosts[0], windowsHosts[0]), + nil, + nil, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, unenrolledHost: {}, linuxHost: {}, // windows profiles are directly deleted without a pending state (there's no on-host removal of profiles) windowsHosts[0]: {}, windowsHosts[1]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -872,23 +1164,110 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { require.Len(t, toRemoveWindows, 3) // update status of the moved host via its uuid (team has no profiles) - err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, nil, []string{darwinHosts[1].UUID, windowsHosts[1].UUID}) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + nil, + nil, + nil, + []string{darwinHosts[1].UUID, windowsHosts[1].UUID}, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, unenrolledHost: {}, linuxHost: {}, @@ -896,9 +1275,21 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { // windows profiles are directly deleted without a pending state windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -911,7 +1302,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "T1.1w", "T1.1"), windowsConfigProfileForTest(t, "T1.2w", "T1.2"), } - err = ds.BatchSetMDMProfiles(ctx, &team1.ID, tm1DarwinProfiles, tm1WindowsProfiles) + err = ds.BatchSetMDMProfiles(ctx, &team1.ID, tm1DarwinProfiles, tm1WindowsProfiles, nil) require.NoError(t, err) tm1Profiles := getProfs(&team1.ID) @@ -939,33 +1330,144 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, + { + ProfileUUID: tm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm1Profiles[0].Identifier, + }, + { + ProfileUUID: tm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm1Profiles[1].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: tm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -983,15 +1485,15 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { // rows in this test since we don't have command uuids. err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { - HostUUID: darwinHosts[0].UUID, ProfileUUID: darwinGlobalProfiles[0].ProfileUUID, + HostUUID: darwinHosts[0].UUID, ProfileUUID: darwinGlobalProfiles[0].ProfileUUID, ProfileIdentifier: darwinGlobalProfiles[0].Identifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeRemove, Checksum: []byte("csum"), }, { - HostUUID: darwinHosts[0].UUID, ProfileUUID: darwinGlobalProfiles[1].ProfileUUID, + HostUUID: darwinHosts[0].UUID, ProfileUUID: darwinGlobalProfiles[1].ProfileUUID, ProfileIdentifier: darwinGlobalProfiles[1].Identifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeRemove, Checksum: []byte("csum"), }, { - HostUUID: darwinHosts[0].UUID, ProfileUUID: darwinGlobalProfiles[2].ProfileUUID, + HostUUID: darwinHosts[0].UUID, ProfileUUID: darwinGlobalProfiles[2].ProfileUUID, ProfileIdentifier: darwinGlobalProfiles[2].Identifier, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove, Checksum: []byte("csum"), }, }) @@ -1007,7 +1509,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "T1.3w", "T1.3"), } - err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles) + err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles, nil) require.NoError(t, err) newTm1Profiles := getProfs(&team1.ID) @@ -1019,32 +1521,138 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: tm1Profiles[0].Identifier, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[0].Identifier, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1063,7 +1671,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "T1.3w", "T1.3"), } - err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles) + err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles, nil) require.NoError(t, err) newTm1Profiles = getProfs(&team1.ID) require.Len(t, newTm1Profiles, 6) @@ -1074,33 +1682,143 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[0].Identifier, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[1].Identifier, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[1].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[3].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: globalProfiles[4].Identifier, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: globalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: globalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: globalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1117,7 +1835,8 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "G4w", "G4"), } - err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles) + // TODO(roberto): add new darwin declarations for this and all subsequent assertions + err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil) require.NoError(t, err) newGlobalProfiles := getProfs(nil) @@ -1127,36 +1846,112 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil) require.NoError(t, err) + require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[0].UUID, nil)) + require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[1].UUID, nil)) + require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[2].UUID, nil)) + assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[0].Identifier, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[1].Identifier, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: newTm1Profiles[2].Identifier, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1176,7 +1971,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "G5w", "G5"), } - err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles) + err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil) require.NoError(t, err) newGlobalProfiles = getProfs(nil) require.Len(t, newGlobalProfiles, 8) @@ -1188,35 +1983,107 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1227,36 +2094,112 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: {}, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1270,7 +2213,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "T2.1w", "T2.1"), } - err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles) + err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles, nil) require.NoError(t, err) tm2Profiles := getProfs(&team2.ID) require.Len(t, tm2Profiles, 2) @@ -1281,39 +2224,123 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1369,7 +2396,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "G7w", "G7", labels[5]), } - err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles) + err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil) require.NoError(t, err) newGlobalProfiles = getProfs(nil) require.Len(t, newGlobalProfiles, 12) @@ -1381,51 +2408,150 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { // simulate an entry with some values set to NULL ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET detail = NULL WHERE profile_uuid = ?`, globalProfiles[2].ProfileUUID) + _, err := q.ExecContext( + ctx, + `UPDATE host_mdm_apple_profiles SET detail = NULL WHERE profile_uuid = ?`, + globalProfiles[2].ProfileUUID, + ) return err }) // do a sync of all hosts, should not change anything as no host is a member // of the new label-based profiles (indices change due to new Apple and // Windows profiles) - err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...), nil, nil, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + hostIDsFromHosts( + append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...), + nil, + nil, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm2Profiles[0].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1459,135 +2585,422 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { // make the new Apple host a member of labels[0] and [1] // make the new Windows host a member of labels[3] and [4] err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{ - {labels[0].ID, darwinHosts[3].ID}, {labels[1].ID, darwinHosts[3].ID}, - {labels[3].ID, windowsHosts[3].ID}, {labels[4].ID, windowsHosts[3].ID}, + {labels[0].ID, darwinHosts[3].ID}, + {labels[1].ID, darwinHosts[3].ID}, + {labels[3].ID, windowsHosts[3].ID}, + {labels[4].ID, windowsHosts[3].ID}, }) require.NoError(t, err) // do a full sync, the new global hosts get the standard global profiles and // also the label-based profile that they are a member of - err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...), nil, nil, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + hostIDsFromHosts( + append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...), + nil, + nil, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) // make the darwinHosts[2] host a member of all labels // make the windowsHosts[2] host a member of all labels err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{ - {labels[0].ID, darwinHosts[2].ID}, {labels[1].ID, darwinHosts[2].ID}, {labels[2].ID, darwinHosts[2].ID}, - {labels[3].ID, darwinHosts[2].ID}, {labels[4].ID, darwinHosts[2].ID}, {labels[5].ID, darwinHosts[2].ID}, - {labels[0].ID, windowsHosts[2].ID}, {labels[1].ID, windowsHosts[2].ID}, {labels[2].ID, windowsHosts[2].ID}, - {labels[3].ID, windowsHosts[2].ID}, {labels[4].ID, windowsHosts[2].ID}, {labels[5].ID, windowsHosts[2].ID}, + {labels[0].ID, darwinHosts[2].ID}, + {labels[1].ID, darwinHosts[2].ID}, + {labels[2].ID, darwinHosts[2].ID}, + {labels[3].ID, darwinHosts[2].ID}, + {labels[4].ID, darwinHosts[2].ID}, + {labels[5].ID, darwinHosts[2].ID}, + {labels[0].ID, windowsHosts[2].ID}, + {labels[1].ID, windowsHosts[2].ID}, + {labels[2].ID, windowsHosts[2].ID}, + {labels[3].ID, windowsHosts[2].ID}, + {labels[4].ID, windowsHosts[2].ID}, + {labels[5].ID, windowsHosts[2].ID}, }) require.NoError(t, err) // do a sync of those hosts, they will get the two label-based profiles of their platform - err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(darwinHosts[2], windowsHosts[2]), nil, nil, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + hostIDsFromHosts(darwinHosts[2], windowsHosts[2]), + nil, + nil, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[11].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[11].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1596,131 +3009,427 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { require.NoError(t, ds.DeleteLabel(ctx, labels[3].Name)) // sync the affected profiles - err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newGlobalProfiles[4].ProfileUUID}, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + nil, + nil, + []string{newGlobalProfiles[4].ProfileUUID}, + nil, + ) require.NoError(t, err) - err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newGlobalProfiles[10].ProfileUUID}, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + nil, + nil, + []string{newGlobalProfiles[10].ProfileUUID}, + nil, + ) require.NoError(t, err) // nothing changes - broken label-based profiles are simply ignored assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[11].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[11].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) // update darwin/windows[2] so they are not members of labels[1][2] and [4][5], which // should remove the G7 label-based profile, but not G6 as it is broken. err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{ - {labels[1].ID, darwinHosts[2].ID}, {labels[2].ID, darwinHosts[2].ID}, - {labels[4].ID, windowsHosts[2].ID}, {labels[5].ID, windowsHosts[2].ID}, + {labels[1].ID, darwinHosts[2].ID}, + {labels[2].ID, darwinHosts[2].ID}, + {labels[4].ID, windowsHosts[2].ID}, + {labels[5].ID, windowsHosts[2].ID}, }) require.NoError(t, err) - err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(darwinHosts[2], windowsHosts[2]), nil, nil, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + hostIDsFromHosts(darwinHosts[2], windowsHosts[2]), + nil, + nil, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1729,62 +3438,206 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { setProfileLabels(t, newGlobalProfiles[4], labels[1]) setProfileLabels(t, newGlobalProfiles[10], labels[4]) - err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newGlobalProfiles[4].ProfileUUID}, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + nil, + nil, + []string{newGlobalProfiles[4].ProfileUUID}, + nil, + ) require.NoError(t, err) - err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newGlobalProfiles[10].ProfileUUID}, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + nil, + nil, + []string{newGlobalProfiles[10].ProfileUUID}, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1798,7 +3651,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "T2.2w", "T2.2", labels[4], labels[5]), } - err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles) + err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles, nil) require.NoError(t, err) tm2Profiles = getProfs(&team2.ID) require.Len(t, tm2Profiles, 4) @@ -1813,62 +3666,196 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) // make darwinHosts[1] and windowsHosts[1] members of the required labels err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{ - {labels[1].ID, darwinHosts[1].ID}, {labels[2].ID, darwinHosts[1].ID}, - {labels[4].ID, windowsHosts[1].ID}, {labels[5].ID, windowsHosts[1].ID}, + {labels[1].ID, darwinHosts[1].ID}, + {labels[2].ID, darwinHosts[1].ID}, + {labels[4].ID, windowsHosts[1].ID}, + {labels[5].ID, windowsHosts[1].ID}, }) require.NoError(t, err) @@ -1878,57 +3865,202 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm2Profiles[0].Identifier, + }, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm2Profiles[1].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm2Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -1943,64 +4075,211 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[0].Identifier, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[2].Identifier, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + IdentifierOrName: globalProfiles[4].Identifier, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm2Profiles[0].Identifier, + }, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + IdentifierOrName: tm2Profiles[1].Identifier, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm2Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) // remove team 2 hosts membership from labels err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{ - {labels[1].ID, darwinHosts[1].ID}, {labels[2].ID, darwinHosts[1].ID}, - {labels[4].ID, windowsHosts[1].ID}, {labels[5].ID, windowsHosts[1].ID}, + {labels[1].ID, darwinHosts[1].ID}, + {labels[2].ID, darwinHosts[1].ID}, + {labels[4].ID, windowsHosts[1].ID}, + {labels[5].ID, windowsHosts[1].ID}, }) require.NoError(t, err) @@ -2011,57 +4290,197 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm2Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) @@ -2076,115 +4495,394 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) { assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) // sanity-check, a full sync does not change anything - err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...), nil, nil, nil) + err = ds.BulkSetPendingMDMHostProfiles( + ctx, + hostIDsFromHosts( + append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...), + nil, + nil, + nil, + ) require.NoError(t, err) assertHostProfiles(map[*fleet.Host][]anyProfile{ darwinHosts[0]: { - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newTm1Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryFailed, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newTm1Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, darwinHosts[1]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: globalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: tm2Profiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: tm2Profiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: globalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: tm2Profiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: tm2Profiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[2]: { - {ProfileUUID: globalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, - {ProfileUUID: newGlobalProfiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + { + ProfileUUID: globalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, + { + ProfileUUID: newGlobalProfiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeRemove, + }, }, darwinHosts[3]: { - {ProfileUUID: newGlobalProfiles[0].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[1].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[0].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[1].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, unenrolledHost: {}, linuxHost: {}, windowsHosts[0]: { - {ProfileUUID: newTm1Profiles[3].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[4].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newTm1Profiles[5].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newTm1Profiles[3].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[4].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newTm1Profiles[5].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[1]: { - {ProfileUUID: tm2Profiles[2].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: tm2Profiles[2].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[2]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, windowsHosts[3]: { - {ProfileUUID: newGlobalProfiles[6].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[7].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[8].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[9].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, - {ProfileUUID: newGlobalProfiles[10].ProfileUUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + { + ProfileUUID: newGlobalProfiles[6].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[7].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[8].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[9].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, + { + ProfileUUID: newGlobalProfiles[10].ProfileUUID, + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + }, }, }) } @@ -2221,7 +4919,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) configProfileForTest(t, "T1.2", "T1.2", "e"), } - err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil) require.NoError(t, err) profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{}) @@ -2259,7 +4957,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) label, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_1"}) require.NoError(t, err) - err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil) require.NoError(t, err) var uid string @@ -2332,7 +5030,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) testLabel3, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_3"}) require.NoError(t, err) - err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil) require.NoError(t, err) var uid string @@ -2416,7 +5114,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) testLabel4, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_4"}) require.NoError(t, err) - err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil) require.NoError(t, err) var uid string @@ -2490,7 +5188,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) windowsConfigProfileForTest(t, "T5.2", "T5.2"), } - err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil) require.NoError(t, err) profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{}) @@ -2528,7 +5226,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) label, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_6"}) require.NoError(t, err) - err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil) require.NoError(t, err) var uid string @@ -2601,7 +5299,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) testLabel3, err := ds.NewLabel(ctx, &fleet.Label{Name: uuid.NewString()}) require.NoError(t, err) - err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil) require.NoError(t, err) var uid string @@ -2685,7 +5383,7 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) label, err := ds.NewLabel(ctx, &fleet.Label{Name: uuid.NewString()}) require.NoError(t, err) - err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles) + err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil) require.NoError(t, err) var uid string @@ -3076,7 +5774,7 @@ func testBatchSetMDMProfilesTransactionError(t *testing.T, ds *Datastore) { windowsConfigProfileForTest(t, "W2", "l2"), } // set the initial profiles without error - err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs) + err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs, nil) require.NoError(t, err) // now ensure all steps are required (add a profile, delete a profile, set labels) @@ -3092,7 +5790,7 @@ func testBatchSetMDMProfilesTransactionError(t *testing.T, ds *Datastore) { ds.testBatchSetMDMAppleProfilesErr = c.appleErr ds.testBatchSetMDMWindowsProfilesErr = c.windowsErr - err = ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs) + err = ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs, nil) require.ErrorContains(t, err, c.wantErr) }) } @@ -3175,7 +5873,8 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) { // use the host UUID, just to make sure they're // different from each other, we don't care about the // DER contents here - Raw: []byte(h.UUID)} + Raw: []byte(h.UUID), + } err = scepDepot.Put(cert.Subject.CommonName, cert) require.NoError(t, err) req := mdm.Request{ @@ -3275,3 +5974,372 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) { require.NoError(t, err) checkSCEPRenew(assocs[0], nil) } + +func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) { + // TODO: Expand this test to include: + // - more scenarios for windows + // - disk encryption (mac and windows) + // - more scenarios for labels + + ctx := context.Background() + + checkSummaryWindows := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) { + ps, err := ds.GetMDMWindowsProfilesSummary(ctx, teamID) + require.NoError(t, err) + require.NotNil(t, ps) + require.Equal(t, expected, *ps) + } + + checkSummaryMac := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) { + ps, err := ds.GetMDMAppleProfilesSummary(ctx, teamID) + require.NoError(t, err) + require.NotNil(t, ps) + require.Equal(t, expected, *ps) + } + + checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) { + gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status}) + require.NoError(t, err) + if len(expectedIDs) != len(gotHosts) { + gotIDs := make([]uint, len(gotHosts)) + for _, h := range gotHosts { + gotIDs = append(gotIDs, h.ID) + } + require.Len(t, gotHosts, len(expectedIDs), fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs)) + + } + for _, h := range gotHosts { + require.Contains(t, expectedIDs, h.ID) + } + + count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status}) + require.NoError(t, err) + require.Equal(t, len(expectedIDs), count, "status: %s", status) + } + + type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint + + checkExpected := func(t *testing.T, teamID *uint, ep hostIDsByProfileStatus) { + expectSummaryWindows := map[fleet.MDMDeliveryStatus]uint{} + expectSummaryMac := map[fleet.MDMDeliveryStatus]uint{} + for status, ids := range ep { + if len(ids) > 0 { + for _, id := range ids { + if id < 5 { + expectSummaryWindows[status]++ + } else { + expectSummaryMac[status]++ + } + } + } + } + checkSummaryMac(t, teamID, fleet.MDMProfilesSummary{ + Pending: expectSummaryMac[fleet.MDMDeliveryPending], + Failed: expectSummaryMac[fleet.MDMDeliveryFailed], + Verifying: expectSummaryMac[fleet.MDMDeliveryVerifying], + Verified: expectSummaryMac[fleet.MDMDeliveryVerified], + }) + + checkSummaryWindows(t, teamID, fleet.MDMProfilesSummary{ + Pending: expectSummaryWindows[fleet.MDMDeliveryPending], + Failed: expectSummaryWindows[fleet.MDMDeliveryFailed], + Verifying: expectSummaryWindows[fleet.MDMDeliveryVerifying], + Verified: expectSummaryWindows[fleet.MDMDeliveryVerified], + }) + + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified]) + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying]) + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed]) + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending]) + } + + // checkWinHostProfiles := func(t *testing.T, hostUUID string, statusByProfUUID map[string]string) { + // profs, err := ds.GetHostMDMWindowsProfiles(ctx, hostUUID) + // require.NoError(t, err) + // require.Len(t, profs, len(statusByProfUUID)) + // for _, prof := range profs { + // ep, ok := statusByProfUUID[prof.ProfileUUID] + // require.True(t, ok) + // require.Equal(t, ep, prof.Status) + // } + // } + + checkMacHostProfiles := func(t *testing.T, hostUUID string, statusByProfUUID map[string]string) { + profs, err := ds.GetHostMDMAppleProfiles(ctx, hostUUID) + require.NoError(t, err) + require.Len(t, profs, len(statusByProfUUID)) + for _, prof := range profs { + ep, ok := statusByProfUUID[prof.ProfileUUID] + require.True(t, ok) + require.NotNil(t, prof.Status) + require.Equal(t, fleet.MDMDeliveryStatus(ep), *prof.Status) + } + } + + upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status *fleet.MDMDeliveryStatus) { + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + var table string + var profType string + switch { + case strings.HasPrefix(profUUID, "a"): + table = "host_mdm_apple_profiles" + profType = "profile" + case strings.HasPrefix(profUUID, "w"): + table = "host_mdm_windows_profiles" + profType = "profile" + case strings.HasPrefix(profUUID, "d"): + table = "host_mdm_apple_declarations" + profType = "declaration" + default: + require.FailNow(t, "unknown profile type") + } + stmt := fmt.Sprintf(`INSERT INTO %s (host_uuid, %s_uuid, status) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`, table, profType) + _, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, status) + if err != nil { + require.NoError(t, err) + return err + } + stmt = fmt.Sprintf(`UPDATE %s SET operation_type = ? WHERE host_uuid = ? AND %s_uuid = ?`, table, profType) + _, err = q.ExecContext(ctx, stmt, fleet.MDMOperationTypeInstall, hostUUID, profUUID) + require.NoError(t, err) + return err + }) + } + + cleanupTables := func(t *testing.T) { + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_windows_profiles`) + return err + }) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_profiles`) + return err + }) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_declarations`) + return err + }) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_disk_encryption_keys`) + return err + }) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_disks`) + return err + }) + } + + // updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) { + // ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + // stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?` + // _, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID) + // return err + // }) + // } + + // Create some hosts + var hosts []*fleet.Host + macHostsByID := make(map[uint]*fleet.Host, 5) + winHostsByID := make(map[uint]*fleet.Host, 5) + for i := 0; i < 10; i++ { + p := "windows" + if i >= 5 { + p = "darwin" + } + u := uuid.New().String() + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: &u, + UUID: u, + Hostname: u, + Platform: p, + }) + require.NoError(t, err) + require.NotNil(t, h) + hosts = append(hosts, h) + if p == "darwin" { + macHostsByID[h.ID] = h + } else { + winHostsByID[h.ID] = h + } + + require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "")) + } + + checkExpected(t, nil, nil) + + upsertHostProfileStatus(t, hosts[0].UUID, "w1", &fleet.MDMDeliveryPending) + + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + }) + + // add some mac profiles with different statuses + upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryFailed) + upsertHostProfileStatus(t, hosts[9].UUID, "a2", &fleet.MDMDeliveryPending) + upsertHostProfileStatus(t, hosts[9].UUID, "a3", &fleet.MDMDeliveryVerifying) + upsertHostProfileStatus(t, hosts[9].UUID, "a4", &fleet.MDMDeliveryVerified) + + // add some mac declarations with different statuses + upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryFailed) + upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryPending) + upsertHostProfileStatus(t, hosts[9].UUID, "d3", &fleet.MDMDeliveryVerifying) + upsertHostProfileStatus(t, hosts[9].UUID, "d4", &fleet.MDMDeliveryVerified) + + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryFailed: []uint{hosts[9].ID}, + }) + expectedHostProfiles := map[string]string{ + "a1": "failed", + "a2": "pending", + "a3": "verifying", + "a4": "verified", + "d1": "failed", + "d2": "pending", + "d3": "verifying", + "d4": "verified", + } + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set failed mac profile to pending, still failed because of failed declaration + upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryPending) + expectedHostProfiles["a1"] = "pending" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryFailed: []uint{hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set failed mac declaration to pending, now host stsatus is pending + upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryPending) + expectedHostProfiles["d1"] = "pending" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set pending mac declaration to failed, host status is now failed + upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryFailed) + expectedHostProfiles["d2"] = "failed" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryFailed: []uint{hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set failed mac declaration to verifying, host status is now pending + upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryVerifying) + expectedHostProfiles["d2"] = "verifying" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set pending mac profiles to verifying, host status is still pending because d1 is still pending + upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryVerifying) + expectedHostProfiles["a1"] = "verifying" + upsertHostProfileStatus(t, hosts[9].UUID, "a2", &fleet.MDMDeliveryVerifying) + expectedHostProfiles["a2"] = "verifying" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set pending mac declarations to verifying, host status is now verifying + upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryVerifying) + expectedHostProfiles["d1"] = "verifying" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryVerifying: []uint{hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set a mac profile to failed, host status is now failed + upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryFailed) + expectedHostProfiles["a1"] = "failed" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryFailed: []uint{hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set mac profiles to verified, host status is now verifying because declarations are still + // verifying + upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryVerified) + expectedHostProfiles["a1"] = "verified" + upsertHostProfileStatus(t, hosts[9].UUID, "a2", &fleet.MDMDeliveryVerified) + expectedHostProfiles["a2"] = "verified" + upsertHostProfileStatus(t, hosts[9].UUID, "a3", &fleet.MDMDeliveryVerified) + expectedHostProfiles["a3"] = "verified" + upsertHostProfileStatus(t, hosts[9].UUID, "a4", &fleet.MDMDeliveryVerified) + expectedHostProfiles["a4"] = "verified" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryVerifying: []uint{hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set mac declarations to verified, host status is now verified + upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryVerified) + expectedHostProfiles["d1"] = "verified" + upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryVerified) + expectedHostProfiles["d2"] = "verified" + upsertHostProfileStatus(t, hosts[9].UUID, "d3", &fleet.MDMDeliveryVerified) + expectedHostProfiles["d3"] = "verified" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryVerified: []uint{hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // set a mac declaration to nil, host status is now pending + upsertHostProfileStatus(t, hosts[9].UUID, "d1", nil) + expectedHostProfiles["d1"] = "pending" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // works as expected if we remove mac declarations + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_declarations`) + return err + }) + delete(expectedHostProfiles, "d1") + delete(expectedHostProfiles, "d2") + delete(expectedHostProfiles, "d3") + delete(expectedHostProfiles, "d4") + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + fleet.MDMDeliveryVerified: []uint{hosts[9].ID}, // all profiles were verified + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // works as expected if we remove mac profiles + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_profiles`) + return err + }) + delete(expectedHostProfiles, "a1") + delete(expectedHostProfiles, "a2") + delete(expectedHostProfiles, "a3") + delete(expectedHostProfiles, "a4") + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + // works as expected if declarations but no profiles + upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryPending) + expectedHostProfiles["d1"] = "pending" + checkExpected(t, nil, hostIDsByProfileStatus{ + fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID}, + }) + checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles) + + cleanupTables(t) +} diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 9cd2a784db..e8904bb5a9 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -734,7 +734,7 @@ WHERE return nil, ctxerr.Wrap(ctx, err, "get mdm windows config profile") } - labels, err := ds.listProfileLabelsForProfiles(ctx, []string{res.ProfileUUID}, nil) + labels, err := ds.listProfileLabelsForProfiles(ctx, []string{res.ProfileUUID}, nil, nil) if err != nil { return nil, err } @@ -1495,6 +1495,8 @@ INSERT INTO (SELECT ?, ?, ?, ?, CURRENT_TIMESTAMP() FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ? + ) AND NOT EXISTS ( + SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ? ) )` @@ -1504,7 +1506,7 @@ INSERT INTO } err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { - res, err := tx.ExecContext(ctx, insertProfileStmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID) + res, err := tx.ExecContext(ctx, insertProfileStmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID, cp.Name, teamID) if err != nil { switch { case isDuplicate(err): @@ -1556,6 +1558,8 @@ INSERT INTO (SELECT ?, ?, ?, ?, CURRENT_TIMESTAMP() FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ? + ) AND NOT EXISTS ( + SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ? ) ) ON DUPLICATE KEY UPDATE @@ -1568,7 +1572,7 @@ ON DUPLICATE KEY UPDATE teamID = *cp.TeamID } - res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID) + res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID, cp.Name, teamID) if err != nil { switch { case isDuplicate(err): diff --git a/server/datastore/mysql/migrations/tables/20240327115530_AddDDMTables.go b/server/datastore/mysql/migrations/tables/20240327115530_AddDDMTables.go new file mode 100644 index 0000000000..80bf0b0208 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240327115530_AddDDMTables.go @@ -0,0 +1,135 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20240327115530, Down_20240327115530) +} + +func Up_20240327115530(tx *sql.Tx) error { + _, err := tx.Exec(` +CREATE TABLE mdm_apple_declarations ( + -- declaration_uuid is used as the primary key of the declaration + declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + + -- team_id references the team that owns this declaration + team_id int(10) unsigned NOT NULL DEFAULT '0', + + -- identifier is the "Identifier" field in the declaration, surfaced for convenience. + identifier varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + + -- name is the name of the declaration + name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + + -- raw_json contains a JSON blob with the declaration contents + raw_json json NOT NULL, + + -- checksum is an MD5 checksum of the declaration, in binary form + checksum binary(16) NOT NULL, + + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + uploaded_at timestamp NULL DEFAULT NULL, + + PRIMARY KEY (declaration_uuid), + UNIQUE KEY idx_mdm_apple_declaration_team_identifier (team_id, identifier), + UNIQUE KEY idx_mdm_apple_declaration_team_name (team_id, name) +) + `) + if err != nil { + return fmt.Errorf("creating mdm_apple_declarations table: %w", err) + } + + _, err = tx.Exec(` +CREATE TABLE mdm_declaration_labels ( + -- id is used as the primary key of this table + id int(10) unsigned NOT NULL AUTO_INCREMENT, + + -- apple_declaration_uuid references a declaration + apple_declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + + + -- 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) COLLATE utf8mb4_unicode_ci 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 + label_id int(10) unsigned DEFAULT NULL, + + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + uploaded_at timestamp NULL DEFAULT NULL, + + PRIMARY KEY (id), + UNIQUE KEY idx_mdm_declaration_labels_label_name (apple_declaration_uuid, label_name), + KEY label_id (label_id), + CONSTRAINT mdm_declaration_labels_ibfk_1 FOREIGN KEY (apple_declaration_uuid) REFERENCES mdm_apple_declarations (declaration_uuid) ON DELETE CASCADE, + CONSTRAINT mdm_declaration_labels_ibfk_3 FOREIGN KEY (label_id) REFERENCES labels (id) ON DELETE SET NULL +) + `) + if err != nil { + return fmt.Errorf("creating mdm_declaration_labels table: %w", err) + } + + _, err = tx.Exec(` +CREATE TABLE mdm_apple_declaration_activation_references ( + -- declaration_uuid is the declaration that contains the references + declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + + -- reference is the declaration_uuid of another declaration + reference varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + + PRIMARY KEY (declaration_uuid, reference), + CONSTRAINT FOREIGN KEY (declaration_uuid) REFERENCES mdm_apple_declarations (declaration_uuid) ON UPDATE CASCADE, + CONSTRAINT FOREIGN KEY (reference) REFERENCES mdm_apple_declarations (declaration_uuid) ON UPDATE CASCADE +) + `) + if err != nil { + return fmt.Errorf("creating mdm_apple_declaration_activation_references table: %w", err) + } + + _, err = tx.Exec(` +CREATE TABLE host_mdm_apple_declarations ( + -- host_uuid references a host in the hosts table + host_uuid varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + + -- status represents the status of the declaration in the host + status varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + + -- operation_type is used to signal if the declaration is being added, removed, etc + operation_type varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + + -- detail contains any messages or errors from the protocol or Fleet + detail text COLLATE utf8mb4_unicode_ci, + + -- checksum of the currently implemented declaration + checksum binary(16) NOT NULL, + + -- declaration_uuid references the declaration assigned to the host's team + declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + + -- declaration_identifier is the identifier of the declaration + declaration_identifier varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + + -- declaration_name is the name of the declaration + declaration_name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + + PRIMARY KEY (host_uuid, declaration_uuid), + KEY status (status), + KEY operation_type (operation_type), + CONSTRAINT host_mdm_apple_declarations_ibfk_1 FOREIGN KEY (status) REFERENCES mdm_delivery_status (status) ON UPDATE CASCADE, + CONSTRAINT host_mdm_apple_declarations_ibfk_2 FOREIGN KEY (operation_type) REFERENCES mdm_operation_types (operation_type) ON UPDATE CASCADE +) + `) + if err != nil { + return fmt.Errorf("creatign host_mdm_apple_declarations table %w", err) + } + + return nil +} + +func Down_20240327115530(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go b/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go new file mode 100644 index 0000000000..2aa6de300b --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go @@ -0,0 +1,36 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20240327115617, Down_20240327115617) +} + +func Up_20240327115617(tx *sql.Tx) error { + _, err := tx.Exec(` +CREATE TABLE mdm_apple_declarative_requests ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + enrollment_id VARCHAR(255) NOT NULL, + -- Should be one of "tokens", "declaration-items", "status", or "declaration/…/…" where the ellipses reference a declaration on the server + message_type VARCHAR(255) NOT NULL, + -- json payload + raw_json TEXT, + PRIMARY KEY (id), + CONSTRAINT mdm_apple_declarative_requests_enrollment_id FOREIGN KEY (enrollment_id) REFERENCES nano_enrollments (id) ON DELETE CASCADE +) +`) + + if err != nil { + return fmt.Errorf("creating mdm_apple_declarative_requsts: %w", err) + } + + return nil +} + +func Down_20240327115617(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index 8c4d7c62c2..866f9d6d1d 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -1281,3 +1281,57 @@ func (ds *Datastore) optimisticGetOrInsert(ctx context.Context, readStmt, insert } return id, nil } + +// batchProcessDB abstracts the batch processing logic, for a given payload: +// +// - generateValueArgs will get called for each item, the expected return values are: +// - a string containing the placeholders for each item in the batch +// - a slice of arguments containing one item for each placeholder +// +// - executeBatch will get called on each batch to perform the operation in the db +// +// TODO(roberto): use this function in all the functions where we do ad-hoc +// batch processing. +func batchProcessDB[T any]( + payload []T, + batchSize int, + generateValueArgs func(T) (string, []any), + executeBatch func(string, []any) error, +) error { + if len(payload) == 0 { + return nil + } + + var ( + args []any + sb strings.Builder + batchCount int + ) + + resetBatch := func() { + batchCount = 0 + args = args[:0] + sb.Reset() + } + + for _, item := range payload { + valuePart, itemArgs := generateValueArgs(item) + args = append(args, itemArgs...) + sb.WriteString(valuePart) + batchCount++ + + if batchCount >= batchSize { + if err := executeBatch(sb.String(), args); err != nil { + return err + } + resetBatch() + } + } + + if batchCount > 0 { + if err := executeBatch(sb.String(), args); err != nil { + return err + } + } + return nil +} diff --git a/server/datastore/mysql/mysql_test.go b/server/datastore/mysql/mysql_test.go index 171426016d..de5eee8905 100644 --- a/server/datastore/mysql/mysql_test.go +++ b/server/datastore/mysql/mysql_test.go @@ -1229,3 +1229,55 @@ func TestWhereFilterGlobalOrTeamIDByTeams(t *testing.T) { }) } } + +func TestBatchProcessDB(t *testing.T) { + type testData struct { + id int + value string + } + + payload := []interface{}{ + &testData{id: 1, value: "a"}, + &testData{id: 2, value: "b"}, + &testData{id: 3, value: "c"}, + } + + generateValueArgs := func(item interface{}) (string, []any) { + p := item.(*testData) + valuePart := "(?, ?)," + args := []any{p.id, p.value} + return valuePart, args + } + + t.Run("TestEmptyPayload", func(t *testing.T) { + executeBatch := func(valuePart string, args []any) error { + return errors.New("execute shouldn't be called for an empty payload") + } + err := batchProcessDB([]interface{}{}, 1000, generateValueArgs, executeBatch) + require.NoError(t, err) + }) + + t.Run("TestSingleBatch", func(t *testing.T) { + callCount := 0 + executeBatch := func(valuePart string, args []any) error { + callCount++ + require.Equal(t, 2, len(args)/2) // each item adds 2 args + return nil + } + err := batchProcessDB(payload[:2], 2, generateValueArgs, executeBatch) + require.NoError(t, err) + require.Equal(t, 1, callCount) + }) + + t.Run("TestMultipleBatches", func(t *testing.T) { + callCount := 0 + executeBatch := func(valuePart string, args []any) error { + callCount++ + require.Equal(t, 2/callCount, len(args)/2) // each item adds 2 args + return nil + } + err := batchProcessDB(payload, 2, generateValueArgs, executeBatch) + require.NoError(t, err) + require.Equal(t, 2, callCount) + }) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 1b60c5b32e..1d8726bbfb 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -334,6 +334,24 @@ CREATE TABLE `host_mdm_apple_bootstrap_packages` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `host_mdm_apple_declarations` ( + `host_uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `operation_type` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `detail` text COLLATE utf8mb4_unicode_ci, + `checksum` binary(16) NOT NULL, + `declaration_uuid` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `declaration_identifier` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `declaration_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + PRIMARY KEY (`host_uuid`,`declaration_uuid`), + KEY `status` (`status`), + KEY `operation_type` (`operation_type`), + CONSTRAINT `host_mdm_apple_declarations_ibfk_1` FOREIGN KEY (`status`) REFERENCES `mdm_delivery_status` (`status`) ON UPDATE CASCADE, + CONSTRAINT `host_mdm_apple_declarations_ibfk_2` FOREIGN KEY (`operation_type`) REFERENCES `mdm_operation_types` (`operation_type`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `host_mdm_apple_profiles` ( `profile_identifier` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `host_uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, @@ -661,6 +679,46 @@ CREATE TABLE `mdm_apple_configuration_profiles` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `mdm_apple_declaration_activation_references` ( + `declaration_uuid` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `reference` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + PRIMARY KEY (`declaration_uuid`,`reference`), + KEY `reference` (`reference`), + CONSTRAINT `mdm_apple_declaration_activation_references_ibfk_1` FOREIGN KEY (`declaration_uuid`) REFERENCES `mdm_apple_declarations` (`declaration_uuid`) ON UPDATE CASCADE, + CONSTRAINT `mdm_apple_declaration_activation_references_ibfk_2` FOREIGN KEY (`reference`) REFERENCES `mdm_apple_declarations` (`declaration_uuid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `mdm_apple_declarations` ( + `declaration_uuid` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `team_id` int(10) unsigned NOT NULL DEFAULT '0', + `identifier` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `raw_json` json NOT NULL, + `checksum` binary(16) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `uploaded_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`declaration_uuid`), + UNIQUE KEY `idx_mdm_apple_declaration_team_identifier` (`team_id`,`identifier`), + UNIQUE KEY `idx_mdm_apple_declaration_team_name` (`team_id`,`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `mdm_apple_declarative_requests` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `enrollment_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `message_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `raw_json` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + KEY `mdm_apple_declarative_requests_enrollment_id` (`enrollment_id`), + CONSTRAINT `mdm_apple_declarative_requests_enrollment_id` FOREIGN KEY (`enrollment_id`) REFERENCES `nano_enrollments` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `mdm_apple_default_setup_assistants` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `team_id` int(10) unsigned DEFAULT NULL, @@ -738,6 +796,22 @@ CREATE TABLE `mdm_configuration_profile_labels` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `mdm_declaration_labels` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `apple_declaration_uuid` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `label_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `label_id` int(10) unsigned DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `uploaded_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_mdm_declaration_labels_label_name` (`apple_declaration_uuid`,`label_name`), + KEY `label_id` (`label_id`), + CONSTRAINT `mdm_declaration_labels_ibfk_1` FOREIGN KEY (`apple_declaration_uuid`) REFERENCES `mdm_apple_declarations` (`declaration_uuid`) ON DELETE CASCADE, + CONSTRAINT `mdm_declaration_labels_ibfk_3` FOREIGN KEY (`label_id`) REFERENCES `labels` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `mdm_delivery_status` ( `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`status`) @@ -809,9 +883,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=260 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=262 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go index 6a329f7f97..a1eadba60d 100644 --- a/server/datastore/mysql/testing_utils.go +++ b/server/datastore/mysql/testing_utils.go @@ -313,11 +313,12 @@ func TruncateTables(t testing.TB, ds *Datastore, tables ...string) { // be truncated - a more precise approach must be used for those, e.g. // delete where id > max before test, or something like that. nonEmptyTables := map[string]bool{ - "app_config_json": true, - "migration_status_tables": true, - "osquery_options": true, - "mdm_delivery_status": true, - "mdm_operation_types": true, + "app_config_json": true, + "migration_status_tables": true, + "osquery_options": true, + "mdm_delivery_status": true, + "mdm_operation_types": true, + "mdm_apple_declaration_categories": true, } ctx := context.Background() diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 2fb7a4dbc0..abd4835a4d 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -84,6 +84,9 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeLockedHost{}, ActivityTypeUnlockedHost{}, ActivityTypeWipedHost{}, + + ActivityTypeCreatedDeclarationProfile{}, + ActivityTypeDeletedDeclarationProfile{}, } type ActivityDetails interface { @@ -1317,6 +1320,56 @@ func (a ActivityTypeWipedHost) Documentation() (activity, details, detailsExampl }` } +type ActivityTypeCreatedDeclarationProfile struct { + ProfileName string `json:"profile_name"` + Identifier string `json:"identifier"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeCreatedDeclarationProfile) ActivityName() string { + return "created_declaration_profile" +} + +func (a ActivityTypeCreatedDeclarationProfile) Documentation() (activity string, details string, detailsExample string) { + return `Generated when a user adds a new macOS declaration to a team (or no team).`, + `This activity contains the following fields: +- "profile_name": Name of the declaration. +- "identifier": Identifier of the declaration. +- "team_id": The ID of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team. +- "team_name": The name of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team.`, `{ + "profile_name": "Passcode requirements", + "profile_identifier": "com.my.declaration", + "team_id": 123, + "team_name": "Workstations" +}` +} + +type ActivityTypeDeletedDeclarationProfile struct { + ProfileName string `json:"profile_name"` + Identifier string `json:"identifier"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeDeletedDeclarationProfile) ActivityName() string { + return "deleted_declaration_profile" +} + +func (a ActivityTypeDeletedDeclarationProfile) Documentation() (activity string, details string, detailsExample string) { + return `Generated when a user removes a macOS declaration from a team (or no team).`, + `This activity contains the following fields: +- "profile_name": Name of the declaration. +- "identifier": Identifier of the declaration. +- "team_id": The ID of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team. +- "team_name": The name of the team that the declaration applies to, ` + "`null`" + ` if it applies to devices that are not in a team.`, `{ + "profile_name": "Passcode requirements", + "profile_identifier": "com.my.declaration", + "team_id": 123, + "team_name": "Workstations" +}` +} + // LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams. func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error { if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) { diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 0b074b87dc..22bd82d6d4 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -451,6 +451,8 @@ const ( DEPAssignProfileResponseFailed DEPAssignProfileResponseStatus = "FAILED" ) +const MDMAppleDeclarationUUIDPrefix = "d" + // NanoEnrollment represents a row in the nano_enrollments table managed by // nanomdm. It is meant to be used internally by the server, not to be returned // as part of endpoints, and as a precaution its json-encoding is explicitly @@ -533,3 +535,287 @@ type SCEPIdentityAssociation struct { EnrollReference string `db:"enroll_reference"` RenewCommandUUID string `db:"renew_command_uuid"` } + +// MDMAppleDeclaration represents a DDM JSON declaration. +type MDMAppleDeclaration struct { + // DeclarationUUID is the unique identifier of the declaration in + // Fleet. Since we use the same endpoints for declarations and profiles: + // - This is marshalled as profile_uuid + // - The value has a prefix (TODO: @jahzielv to determine and document this) + DeclarationUUID string `db:"declaration_uuid" json:"profile_uuid"` + + // TeamID is the id of the team with which the declaration is associated. A nil team id + // represents a declaration that is not associated with any team. + TeamID *uint `db:"team_id" json:"team_id"` + + // Identifier corresponds to the "Identifier" key of the associated declaration. + // Fleet requires that Identifier must be unique in combination with the Name and TeamID. + Identifier string `db:"identifier" json:"identifier"` + + // Name corresponds to the file name of the associated JSON declaration payload. + // Fleet requires that Name must be unique in combination with the Identifier and TeamID. + Name string `db:"name" json:"name"` + + // RawJSON is the raw JSON content of the declaration + RawJSON json.RawMessage `db:"raw_json" json:"-"` + + // Checksum is a checksum of the JSON contents + Checksum string `db:"checksum" json:"-"` + + // Labels are the labels associated with this Declaration + Labels []ConfigurationProfileLabel `db:"labels" json:"labels,omitempty"` + + CreatedAt time.Time `db:"created_at" json:"created_at"` + UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"` +} + +type MDMAppleRawDeclaration struct { + // Type is the "Type" field on the raw declaration JSON. + Type string `json:"Type"` + Identifier string `json:"Identifier"` +} + +// ForbiddenDeclTypes is a set of declaration types that are not allowed to be +// added by users into Fleet. +var ForbiddenDeclTypes = map[string]struct{}{ + "com.apple.configuration.account.caldav": {}, + "com.apple.configuration.account.carddav": {}, + "com.apple.configuration.account.exchange": {}, + "com.apple.configuration.account.google": {}, + "com.apple.configuration.account.ldap": {}, + "com.apple.configuration.account.mail": {}, + "com.apple.configuration.screensharing.connection": {}, + "com.apple.configuration.security.certificate": {}, + "com.apple.configuration.security.identity": {}, + "com.apple.configuration.security.passkey.attestation": {}, + "com.apple.configuration.services.configuration-files": {}, + "com.apple.configuration.watch.enrollment": {}, +} + +func (r *MDMAppleRawDeclaration) ValidateUserProvided() error { + var err error + + // Check against types we don't allow + if r.Type == `com.apple.configuration.softwareupdate.enforcement.specific` { + return NewInvalidArgumentError(r.Type, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.") + } + + if _, forbidden := ForbiddenDeclTypes[r.Type]; forbidden { + return NewInvalidArgumentError(r.Type, "Only configuration declarations that don’t require an asset reference are supported.") + } + + if r.Type == "com.apple.configuration.management.status-subscriptions" { + return NewInvalidArgumentError(r.Type, "Declaration profile can’t include status subscription type. To get host’s vitals, please use queries and policies.") + } + + if !strings.HasPrefix(r.Type, "com.apple.configuration") { + return NewInvalidArgumentError(r.Type, "Only configuration declarations (com.apple.configuration) are supported.") + } + + return err +} + +func GetRawDeclarationValues(raw []byte) (*MDMAppleRawDeclaration, error) { + var rawDecl MDMAppleRawDeclaration + if err := json.Unmarshal(raw, &rawDecl); err != nil { + return nil, NewInvalidArgumentError("declaration", fmt.Sprintf("Couldn't upload. The file should include valid JSON: %s", err)).WithStatus(http.StatusBadRequest) + } + + return &rawDecl, nil +} + +// MDMAppleHostDeclaration represents the state of a declaration on a host +type MDMAppleHostDeclaration struct { + // HostUUID is the uuid of the host affected by this declaration + HostUUID string `db:"host_uuid" json:"-"` + + // DeclarationUUID is the unique identifier of the declaration in + // Fleet. Since we use the same endpoints for declarations and profiles: + // - This is marshalled as profile_uuid + // - The value has a prefix (TODO: @jahzielv to determine and document this) + DeclarationUUID string `db:"declaration_uuid" json:"profile_uuid"` + + // Name corresponds to the file name of the associated JSON declaration payload. + Name string `db:"declaration_name" json:"name"` + + // Identifier corresponds to the "Identifier" key of the associated declaration. + Identifier string `db:"declaration_identifier" json:"-"` + + // Status represent the current state of the declaration, as known by the Fleet server. + Status *MDMDeliveryStatus `db:"status" json:"status"` + + // Operation type represents the operation being performed. + OperationType MDMOperationType `db:"operation_type" json:"operation_type"` + + // Detail contains any messages that must be surfaced to the user, + // either by the MDM protocol or the Fleet server. + Detail string `db:"detail" json:"detail"` + + // Checksum contains the MD5 checksum of the declaration JSON uploaded + // by the IT admin. Fleet uses this value as the ServerToken. + Checksum string `db:"checksum" json:"-"` +} + +func NewMDMAppleDeclaration(raw []byte, teamID *uint, name string, declType, ident string) *MDMAppleDeclaration { + var decl MDMAppleDeclaration + + decl.Identifier = ident + decl.Name = name + decl.RawJSON = raw + decl.TeamID = teamID + + return &decl +} + +// MDMAppleDDMTokensResponse is the response from the DDM tokens endpoint. +// +// https://developer.apple.com/documentation/devicemanagement/tokensresponse +type MDMAppleDDMTokensResponse struct { + SyncTokens MDMAppleDDMDeclarationsToken +} + +// MDMAppleDDMDeclarationsToken is dictionary describes the state of declarations on the server. +// +// https://developer.apple.com/documentation/devicemanagement/synchronizationtokens +type MDMAppleDDMDeclarationsToken struct { + DeclarationsToken string `db:"checksum"` + Timestamp time.Time `db:"latest_created_timestamp"` +} + +// MDMAppleDDMDeclarationItemsResponse is the response from the DDM declaration items endpoint. +// +// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse +type MDMAppleDDMDeclarationItemsResponse struct { + Declarations MDMAppleDDMManifestItems + DeclarationsToken string +} + +// MDMAppleDDMManifestItems is a dictionary that contains the lists of declarations available on the +// server. +// +// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse/manifestdeclarationitems +type MDMAppleDDMManifestItems struct { + Activations []MDMAppleDDMManifest + Assets []MDMAppleDDMManifest + Configurations []MDMAppleDDMManifest + Management []MDMAppleDDMManifest +} + +// MDMAppleDDMManifest is a dictionary that describes a declaration. +// +// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse/manifestdeclarationitems +type MDMAppleDDMManifest struct { + Identifier string + ServerToken string +} + +// MDMAppleDDMDeclarationItem represents a declaration item in the datastore. It is used to +// construct the DDM `declaration-items` endpoint response. +// +// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse +type MDMAppleDDMDeclarationItem struct { + Identifier string `db:"identifier"` + ServerToken string `db:"checksum"` +} + +// MDMAppleDDMDeclarationResponse represents a declaration in the datastore. It is used for the DDM +// `declaration/.../...` enpoint response. +// +// https://developer.apple.com/documentation/devicemanagement/declarationresponse +type MDMAppleDDMDeclarationResponse struct { + Identifier string `db:"identifier"` + Type string `db:"type"` + Payload json.RawMessage `db:"payload"` + ServerToken string `db:"server_token"` +} + +// MDMAppleDDMStatusReport represents a report of the device's current state. +// +// https://developer.apple.com/documentation/devicemanagement/statusreport +type MDMAppleDDMStatusReport struct { + StatusItems MDMAppleDDMStatusItems `json:"StatusItems"` + Errors []MDMAppleDDMErrors `json:"Errors"` +} + +// MDMAppleDDMStatusItems are the status items for a report. +// +// https://developer.apple.com/documentation/devicemanagement/statusreport/statusitems +type MDMAppleDDMStatusItems struct { + Management MDMAppleDDMStatusManagement `json:"management"` +} + +// MDMAppleDDMStatusManagement represents status report of the client's +// processed declarations. +// +// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarations +type MDMAppleDDMStatusManagement struct { + Declarations MDMAppleDDMStatusDeclarations `json:"declarations"` +} + +// MDMAppleDDMStatusDeclarations represents a collection of the client's +// processed declarations. +// +// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarationsdeclarationsobject +type MDMAppleDDMStatusDeclarations struct { + // Activations is an array of declarations that represent the client's + // processed activation types. + Activations []MDMAppleDDMStatusDeclaration `json:"activations"` + // Configurations is an array of declarations that represent the + // client's processed configuration types. + Configurations []MDMAppleDDMStatusDeclaration `json:"configurations"` + // Assets is an array of declarations that represent the client's + // processed assets. + Assets []MDMAppleDDMStatusDeclaration `json:"assets"` + // Management is an array of declarations that represent the client's + // processed declaration types. + Management []MDMAppleDDMStatusDeclaration `json:"management"` +} + +type MDMAppleDeclarationValidity string + +const ( + MDMAppleDeclarationValid MDMAppleDeclarationValidity = "valid" + MDMAppleDeclarationInvalid MDMAppleDeclarationValidity = "invalid" + MDMAppleDeclarationUnknown MDMAppleDeclarationValidity = "valid" +) + +// MDMAppleDDMStatusDeclaration represents a processed declaration for the client. +// +// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarationsdeclarationobject +type MDMAppleDDMStatusDeclaration struct { + // Active signals if the declaration is active on the device. + Active bool `json:"active"` + // Identifier is the identifier of the declaration this status report refers to. + Identifier string `json:"identifier"` + // Valid defines the validity of the declaration. If it's invalid, the + // reasons property contains more details. + Valid MDMAppleDeclarationValidity `json:"valid"` + // ServerToken of the declaration this status report refers to. + ServerToken string `json:"server-token"` + // Reasons are the details of any client errors. + Reasons []MDMAppleDDMStatusErrorReason `json:"reasons,omitempty"` +} + +// A status report's error that contains the status item and the reasons for +// the error. +// +// https://developer.apple.com/documentation/devicemanagement/statusreport/error +type MDMAppleDDMErrors struct { + // StatusItem is the status item that this error pertains to. + StatusItem string `json:"StatusItem"` + // Reasons is an array of reasons for the error. + Reasons []MDMAppleDDMStatusErrorReason `json:"Reasons"` +} + +// A status report that contains details about an error. +// +// https://developer.apple.com/documentation/devicemanagement/statusreason +type MDMAppleDDMStatusErrorReason struct { + // Code is the error code for this error. + Code string `json:"Code"` + // Description is a short error description. + Description string `json:"Description"` + // Details is a dictionary that contains further details about this + // error. + Details map[string]any `json:"Details"` +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 827ba0be36..99ba5b7f0a 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -927,6 +927,9 @@ type Datastore interface { // profile uuid. GetMDMAppleConfigProfile(ctx context.Context, profileUUID string) (*MDMAppleConfigProfile, error) + // GetMDMAppleDeclaration returns the declaration corresponding to the specified uuid. + GetMDMAppleDeclaration(ctx context.Context, declUUID string) (*MDMAppleDeclaration, error) + // ListMDMAppleConfigProfiles lists mdm config profiles associated with the specified team id. // For global config profiles, specify nil as the team id. ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint) ([]*MDMAppleConfigProfile, error) @@ -1169,6 +1172,29 @@ type Datastore interface { // serials. UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error + // InsertMDMAppleDDMRequest inserts a DDM request. + InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType, rawJSON string) error + + // MDMAppleDDMDeclarationsToken returns the token used to synchronize declarations for the + // specified host UUID. + MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*MDMAppleDDMDeclarationsToken, error) + // MDMAppleDDMDeclarationItems returns the declaration items for the specified host UUID. + MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]MDMAppleDDMDeclarationItem, error) + // MDMAppleDDMDeclarationPayload returns the declaration payload for the specified identifier and team. + MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*MDMAppleDeclaration, error) + //MDMAppleBatchSetHostDeclarationState + MDMAppleBatchSetHostDeclarationState(ctx context.Context) ([]string, error) + // MDMAppleStoreDDMStatusReport receives a host.uuid and a slice + // of declarations, and updates the tracked host declaration status for + // matching declarations. + // + // It also takes care of cleaning up all host declarations that are + // pending removal. + MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*MDMAppleHostDeclaration) error + // MDMAppleSetDeclarationsAsVerifying updates all + // ("pending", "install") declarations for a host to be ("verifying", "install") + MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error + /////////////////////////////////////////////////////////////////////////////// // Microsoft MDM @@ -1287,7 +1313,10 @@ type Datastore interface { // BatchSetMDMProfiles sets the MDM Apple or Windows profiles for the given team or // no team in a single transaction. - BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile) error + BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile, macDeclarations []*MDMAppleDeclaration) error + + // NewMDMAppleDeclaration creates and returns a new MDM Apple declaration. + NewMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error) /////////////////////////////////////////////////////////////////////////////// // Host Script Results diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index dbb3808aab..baa501bb4b 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -413,6 +413,24 @@ func NewMDMConfigProfilePayloadFromApple(cp *MDMAppleConfigProfile) *MDMConfigPr } } +func NewMDMConfigProfilePayloadFromAppleDDM(decl *MDMAppleDeclaration) *MDMConfigProfilePayload { + var tid *uint + if decl.TeamID != nil && *decl.TeamID > 0 { + tid = decl.TeamID + } + return &MDMConfigProfilePayload{ + ProfileUUID: decl.DeclarationUUID, + TeamID: tid, + Name: decl.Name, + Identifier: decl.Identifier, + Platform: "darwin", + Checksum: []byte(decl.Checksum), + CreatedAt: decl.CreatedAt, + UploadedAt: decl.UploadedAt, + Labels: decl.Labels, + } +} + // MDMProfileSpec represents the spec used to define configuration // profiles via yaml files. type MDMProfileSpec struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index 2fef5eeec5..c38c9550c5 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -652,18 +652,29 @@ type Service interface { // NewMDMAppleConfigProfile creates a new configuration profile for the specified team. NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, labels []string) (*MDMAppleConfigProfile, error) + // NewMDMAppleConfigProfileWithPayload creates a new declaration for the specified team. + NewMDMAppleDeclaration(ctx context.Context, teamID uint, r io.Reader, labels []string, name string) (*MDMAppleDeclaration, error) + // GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple // configuration profile via its numeric ID. This method is deprecated and // should not be used for new endpoints. GetMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) (*MDMAppleConfigProfile, error) // GetMDMAppleConfigProfile retrieves the specified configuration profile. GetMDMAppleConfigProfile(ctx context.Context, profileUUID string) (*MDMAppleConfigProfile, error) + + // GetMDMAppleDeclaration retrieves the specified declaration. + GetMDMAppleDeclaration(ctx context.Context, declarationUUID string) (*MDMAppleDeclaration, error) + // DeleteMDMAppleConfigProfileByDeprecatedID deletes the specified Apple // configuration profile via its numeric ID. This method is deprecated and // should not be used for new endpoints. DeleteMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) error // DeleteMDMAppleConfigProfile deletes the specified configuration profile. DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error + + // DeleteMDMAppleDeclaration deletes the specified declaration. + DeleteMDMAppleDeclaration(ctx context.Context, declarationUUID string) error + // ListMDMAppleConfigProfiles returns the list of all the configuration profiles for the // specified team. ListMDMAppleConfigProfiles(ctx context.Context, teamID uint) ([]*MDMAppleConfigProfile, error) diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go index 24f8def567..33d17daa78 100644 --- a/server/mdm/apple/commander.go +++ b/server/mdm/apple/commander.go @@ -226,6 +226,28 @@ func (svc *MDMAppleCommander) AccountConfiguration(ctx context.Context, hostUUID return svc.EnqueueCommand(ctx, hostUUIDs, raw) } +// DeclarativeManagement sends the homonym [command][1] to the device to enable DDM or start a new DDM session. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/declarativemanagementcommand +func (svc *MDMAppleCommander) DeclarativeManagement(ctx context.Context, hostUUIDs []string, uuid string) error { + raw := fmt.Sprintf(` + + + + Command + + RequestType + DeclarativeManagement + + + CommandUUID + %s + + `, uuid) + + return svc.EnqueueCommand(ctx, hostUUIDs, raw) +} + func (svc *MDMAppleCommander) DeviceConfigured(ctx context.Context, hostUUID, cmdUUID string) error { raw := fmt.Sprintf(` diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go index 801ca37d67..bc8b2bc6e4 100644 --- a/server/mdm/apple/util.go +++ b/server/mdm/apple/util.go @@ -101,6 +101,7 @@ func GenerateRandomPin(length int) string { return fmt.Sprintf(f, v) } +// FmtErrorChain formats Command error message for macOS MDM v1 func FmtErrorChain(chain []mdm.ErrorChain) string { var sb strings.Builder for _, mdmErr := range chain { @@ -113,6 +114,15 @@ func FmtErrorChain(chain []mdm.ErrorChain) string { return sb.String() } +// FmtDDMError formats a DDM error message +func FmtDDMError(reasons []fleet.MDMAppleDDMStatusErrorReason) string { + var errMsg strings.Builder + for _, r := range reasons { + errMsg.WriteString(fmt.Sprintf("%s: %s %+v\n", r.Code, r.Description, r.Details)) + } + return errMsg.String() +} + func EnrollURL(token string, appConfig *fleet.AppConfig) (string, error) { enrollURL, err := url.Parse(appConfig.ServerSettings.ServerURL) if err != nil { diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 00e515667b..5a15dd8c00 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -50,7 +50,7 @@ func GetRawProfilePlatform(profile []byte) string { bytes.EqualFold(prefix, trimmedProfile[:len(prefix)]) } - if prefixMatches([]byte("= 1 { + tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, &teamID, nil) + if err != nil { + return nil, ctxerr.Wrap(ctx, err) + } + teamName = tm.Name + } + + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + var tmID *uint + if teamID >= 1 { + tmID = &teamID + } + + validatedLabels, err := svc.validateDeclarationLabels(ctx, labels) + if err != nil { + return nil, err + } + + // TODO(roberto): Maybe GetRawDeclarationValues belongs inside NewMDMAppleDeclaration? We can refactor this in a follow up. + rawDecl, err := fleet.GetRawDeclarationValues(data) + if err != nil { + return nil, err + } + + if err := rawDecl.ValidateUserProvided(); err != nil { + return nil, err + } + + d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier) + + // TODO(roberto): Is this already handled in NewMDMAppleDeclaration? Could we add the labels as well? + d.Labels = validatedLabels + d.TeamID = tmID + + decl, err := svc.ds.NewMDMAppleDeclaration(ctx, d) + if err != nil { + return nil, err + } + + var ( + actTeamID *uint + actTeamName *string + ) + if teamID > 0 { + actTeamID = &teamID + actTeamName = &teamName + } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedDeclarationProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: decl.Name, + Identifier: decl.Identifier, + }); err != nil { + return nil, ctxerr.Wrap(ctx, err, "logging activity for create mdm apple declaration") + } + + return decl, nil +} + +func (svc *Service) batchValidateDeclarationLabels(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) validateDeclarationLabels(ctx context.Context, labelNames []string) ([]fleet.ConfigurationProfileLabel, error) { + labelMap, err := svc.batchValidateDeclarationLabels(ctx, labelNames) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "validating declaration labels") + } + + var declLabels []fleet.ConfigurationProfileLabel + for _, label := range labelMap { + declLabels = append(declLabels, label) + } + return declLabels, nil +} + type listMDMAppleConfigProfilesRequest struct { TeamID uint `query:"team_id,optional"` } @@ -515,6 +646,25 @@ func (svc *Service) GetMDMAppleConfigProfile(ctx context.Context, profileUUID st return cp, nil } +func (svc *Service) GetMDMAppleDeclaration(ctx context.Context, profileUUID string) (*fleet.MDMAppleDeclaration, error) { + // first we perform a perform basic authz check + if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil { + return nil, err + } + + cp, err := svc.ds.GetMDMAppleDeclaration(ctx, profileUUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err) + } + + // now we can do a specific authz check based on team id of profile before we return the profile + if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: cp.TeamID}, fleet.ActionRead); err != nil { + return nil, err + } + + return cp, nil +} + type deleteMDMAppleConfigProfileRequest struct { ProfileID uint `url:"profile_id"` } @@ -623,6 +773,85 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID return nil } +func (svc *Service) DeleteMDMAppleDeclaration(ctx context.Context, declUUID string) error { + // first we perform a perform basic authz check + if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil { + return ctxerr.Wrap(ctx, err) + } + + // check that Apple MDM is enabled - the middleware of that endpoint checks + // only that any MDM is enabled, maybe it's just Windows + if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { + err := fleet.NewInvalidArgumentError("profile_uuid", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest) + return ctxerr.Wrap(ctx, err, "check macOS MDM enabled") + } + + decl, err := svc.ds.GetMDMAppleDeclaration(ctx, declUUID) + if err != nil { + return ctxerr.Wrap(ctx, err) + } + + if _, ok := mdm_types.FleetReservedProfileNames()[decl.Name]; ok { + return &fleet.BadRequestError{ + Message: "profiles managed by Fleet can't be deleted using this endpoint.", + InternalErr: fmt.Errorf("deleting profile %s is not allowed because it's managed by Fleet", decl.Name), + } + } + + // TODO: refine our approach to deleting restricted/forbidden types of declarations so that we + // can check that Fleet-managed aren't being deleted; this can be addressed once we add support + // for more types of declarations + var d fleet.MDMAppleRawDeclaration + if err := json.Unmarshal(decl.RawJSON, &d); err != nil { + return ctxerr.Wrap(ctx, err, "unmarshalling declaration") + } + if err := d.ValidateUserProvided(); err != nil { + return ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: err.Error()}) + } + + var teamName string + teamID := *decl.TeamID + if teamID >= 1 { + tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, &teamID, nil) + if err != nil { + return ctxerr.Wrap(ctx, err) + } + teamName = tm.Name + } + + // now we can do a specific authz check based on team id of profile before we delete the profile + if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: decl.TeamID}, fleet.ActionWrite); err != nil { + return ctxerr.Wrap(ctx, err) + } + + if err := svc.ds.DeleteMDMAppleConfigProfile(ctx, declUUID); err != nil { + return ctxerr.Wrap(ctx, err) + } + + if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil { + return ctxerr.Wrap(ctx, err, "bulk set pending host profiles") + } + + var ( + actTeamID *uint + actTeamName *string + ) + if teamID > 0 { + actTeamID = &teamID + actTeamName = &teamName + } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedDeclarationProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: decl.Name, + Identifier: decl.Identifier, + }); err != nil { + return ctxerr.Wrap(ctx, err, "logging activity for delete mdm apple declaration") + } + + return nil +} + type getMDMAppleFileVaultSummaryRequest struct { TeamID *uint `query:"team_id,optional"` } @@ -2225,7 +2454,6 @@ func (svc *MDMAppleCheckinAndCommandService) Authenticate(r *mdm.Request, m *mdm InstalledFromDEP: info.DEPAssignedToFleet, MDMPlatform: fleet.MDMPlatformApple, }) - } // TokenUpdate handles MDM [TokenUpdate][1] requests. @@ -2354,7 +2582,8 @@ func (svc *MDMAppleCheckinAndCommandService) UserAuthenticate(*mdm.Request, *mdm // This method is executed after the request has been handled by nanomdm. // // [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin -func (svc *MDMAppleCheckinAndCommandService) DeclarativeManagement(*mdm.Request, *mdm.DeclarativeManagement) ([]byte, error) { +func (svc *MDMAppleCheckinAndCommandService) DeclarativeManagement(r *mdm.Request, dm *mdm.DeclarativeManagement) ([]byte, error) { + // DeclarativeManagement is handled by the MDMAppleDDMService. return nil, nil } @@ -2409,7 +2638,13 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ cmdResult.Status == fleet.MDMAppleStatusCommandFormatError { return nil, svc.ds.UpdateHostLockWipeStatusFromAppleMDMResult(r.Context, cmdResult.UDID, cmdResult.CommandUUID, requestType, cmdResult.Status == fleet.MDMAppleStatusAcknowledged) } + case "DeclarativeManagement": + // set "pending-install" profiles to "verifying" + err := svc.ds.MDMAppleSetDeclarationsAsVerifying(r.Context, cmdResult.UDID) + return nil, ctxerr.Wrap(r.Context, err, "update declaration status on DeclarativeManagement ack") + } + return nil, nil } @@ -2504,6 +2739,34 @@ func ensureFleetdConfig(ctx context.Context, ds fleet.Datastore, logger kitlog.L return nil } +func ReconcileAppleDeclarations( + ctx context.Context, + ds fleet.Datastore, + commander *apple_mdm.MDMAppleCommander, + logger kitlog.Logger, +) error { + + // batch set declarations as pending + changedHosts, err := ds.MDMAppleBatchSetHostDeclarationState(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating host declaration state") + } + + if len(changedHosts) == 0 { + logger.Log("msg", "no hosts with changed declarations") + return nil + } + + // send a DeclarativeManagement command to start a sync + if err := commander.DeclarativeManagement(ctx, changedHosts, uuid.NewString()); err != nil { + return ctxerr.Wrap(ctx, err, "issuing DeclarativeManagement command") + } + + logger.Log("msg", "sent DeclarativeManagement command", "host_number", len(changedHosts)) + + return nil +} + func ReconcileAppleProfiles( ctx context.Context, ds fleet.Datastore, @@ -2946,3 +3209,231 @@ func RenewSCEPCertificates( return nil } + +// MDMAppleDDMService is the service that handles MDM [DeclarativeManagement][1] requests. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin +type MDMAppleDDMService struct { + ds fleet.Datastore + logger kitlog.Logger +} + +func NewMDMAppleDDMService(ds fleet.Datastore, logger kitlog.Logger) *MDMAppleDDMService { + return &MDMAppleDDMService{ + ds: ds, + logger: logger, + } +} + +// DeclarativeManagement handles MDM [DeclarativeManagement][1] requests. +// +// This method is when the request has been handled by nanomdm. +// +// [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin +func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.DeclarativeManagement) ([]byte, error) { + if dm == nil { + level.Debug(svc.logger).Log("msg", "ddm request received with nil payload") + return nil, nil + } + level.Debug(svc.logger).Log("msg", "ddm request received", "endpoint", dm.Endpoint) + + if err := svc.ds.InsertMDMAppleDDMRequest(r.Context, dm.UDID, dm.Endpoint, string(dm.Data)); err != nil { + return nil, ctxerr.Wrap(r.Context, err, "insert ddm request history") + } + + if dm.UDID == "" { + return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(r.Context, "missing UDID in request")) + } + + switch { + case dm.Endpoint == "tokens": + level.Debug(svc.logger).Log("msg", "received tokens request") + return svc.handleTokens(r.Context, dm.UDID) + + case dm.Endpoint == "declaration-items": + level.Debug(svc.logger).Log("msg", "received declaration-items request") + return svc.handleDeclarationItems(r.Context, dm.UDID) + + case dm.Endpoint == "status": + level.Debug(svc.logger).Log("msg", "received status request") + return nil, svc.handleDeclarationStatus(r.Context, dm) + + case strings.HasPrefix(dm.Endpoint, "declaration/"): + level.Debug(svc.logger).Log("msg", "received declarations request") + return svc.handleDeclarationsResponse(r.Context, dm.Endpoint, dm.UDID) + + default: + return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(r.Context, fmt.Sprintf("unrecognized declarations endpoint: %s", dm.Endpoint))) + } +} + +func (svc *MDMAppleDDMService) handleTokens(ctx context.Context, hostUUID string) ([]byte, error) { + tok, err := svc.ds.MDMAppleDDMDeclarationsToken(ctx, hostUUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens") + } + + b, err := json.Marshal(fleet.MDMAppleDDMTokensResponse{ + SyncTokens: *tok, + }) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "marshaling synchronization tokens") + } + + return b, nil +} + +func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostUUID string) ([]byte, error) { + di, err := svc.ds.MDMAppleDDMDeclarationItems(ctx, hostUUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens") + } + + activations := []fleet.MDMAppleDDMManifest{} + configurations := []fleet.MDMAppleDDMManifest{} + for _, d := range di { + configurations = append(configurations, fleet.MDMAppleDDMManifest(d)) + activations = append(activations, fleet.MDMAppleDDMManifest{ + Identifier: fmt.Sprintf("%s.activation", d.Identifier), + ServerToken: d.ServerToken, + }) + } + + // TODO: Look for ways to optimize the declaration item query so that we don't have to get the declarations token separately. + dTok, err := svc.ds.MDMAppleDDMDeclarationsToken(ctx, hostUUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting declarations token") + } + + b, err := json.Marshal(fleet.MDMAppleDDMDeclarationItemsResponse{ + Declarations: fleet.MDMAppleDDMManifestItems{ + Activations: activations, + Configurations: configurations, + Assets: []fleet.MDMAppleDDMManifest{}, + Management: []fleet.MDMAppleDDMManifest{}, + }, + DeclarationsToken: dTok.DeclarationsToken, + }) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "marshaling synchronization tokens") + } + + return b, nil +} + +func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, endpoint string, hostUUID string) ([]byte, error) { + parts := strings.Split(endpoint, "/") + if len(parts) != 3 { + return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(ctx, fmt.Sprintf("unrecognized declarations endpoint: %s", endpoint))) + } + level.Debug(svc.logger).Log("msg", "parsed declarations request", "type", parts[1], "identifier", parts[2]) + + switch parts[1] { + case "activation": + return svc.handleActivationDeclaration(ctx, parts, hostUUID) + case "configuration": + return svc.handleConfigurationDeclaration(ctx, parts, hostUUID) + default: + return nil, newNotFoundError() + } +} + +func (svc *MDMAppleDDMService) handleActivationDeclaration(ctx context.Context, parts []string, hostUUID string) ([]byte, error) { + references := strings.TrimSuffix(parts[2], ".activation") + + // ensure the declaration for the requested activation stil exists + d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, references, hostUUID) + if err != nil { + if fleet.IsNotFound(err) { + return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, err) + } + return nil, ctxerr.Wrap(ctx, err, "getting linked configuration for activation declaration") + } + + response := fmt.Sprintf(` +{ + "Identifier": "%s", + "Payload": { + "StandardConfigurations": ["%s"] + }, + "ServerToken": "%s", + "Type": "com.apple.activation.simple" +}`, parts[2], references, d.Checksum) + + return []byte(response), nil +} + +func (svc *MDMAppleDDMService) handleConfigurationDeclaration(ctx context.Context, parts []string, hostUUID string) ([]byte, error) { + d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, parts[2], hostUUID) + if err != nil { + if fleet.IsNotFound(err) { + return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, err) + } + return nil, ctxerr.Wrap(ctx, err, "getting declaration response") + } + + var tempd map[string]any + if err := json.Unmarshal(d.RawJSON, &tempd); err != nil { + return nil, ctxerr.Wrap(ctx, err, "unmarshaling stored declaration") + } + tempd["ServerToken"] = d.Checksum + + b, err := json.Marshal(tempd) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "marshaling declaration") + } + return b, nil +} + +func (svc *MDMAppleDDMService) handleDeclarationStatus(ctx context.Context, dm *mdm.DeclarativeManagement) error { + var status fleet.MDMAppleDDMStatusReport + if err := json.Unmarshal(dm.Data, &status); err != nil { + return ctxerr.Wrap(ctx, err, "unmarshalling response") + } + + configurationReports := status.StatusItems.Management.Declarations.Configurations + updates := make([]*fleet.MDMAppleHostDeclaration, len(configurationReports)) + for i, r := range configurationReports { + var status fleet.MDMDeliveryStatus + var detail string + switch { + case r.Active && r.Valid == fleet.MDMAppleDeclarationValid: + status = fleet.MDMDeliveryVerified + case r.Valid == fleet.MDMAppleDeclarationInvalid: + status = fleet.MDMDeliveryFailed + detail = apple_mdm.FmtDDMError(r.Reasons) + default: + status = fleet.MDMDeliveryVerifying + } + + updates[i] = &fleet.MDMAppleHostDeclaration{ + Status: &status, + OperationType: fleet.MDMOperationTypeInstall, + Detail: detail, + Checksum: r.ServerToken, + } + } + + if len(updates) == 0 { + return nil + } + + // MDMAppleStoreDDMStatusReport takes care of cleaning ("pending", "remove") + // pairs for the host. + // + // TODO(roberto): in the DDM documentation, it's mentioned that status + // report will give you a "remove" status so the server can track + // removals. In my testing, I never saw this (after spending + // considerable time trying to make it work.) + // + // My current guess is that the documentation is implicitly referring + // to asset declarations (which deliver tangible "assets" to the host) + // + // The best indication I found so far, is that if the declaration is + // not in the report, then it's implicitly removed. + if err := svc.ds.MDMAppleStoreDDMStatusReport(ctx, dm.UDID, updates); err != nil { + return ctxerr.Wrap(ctx, err, "updating host declaration status with reports") + } + + return nil +} diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index bd72cbd822..764f4c50ac 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -2727,6 +2727,19 @@ func mobileconfigForTest(name, identifier string) []byte { `, name, identifier, uuid.New().String())) } +func declBytesForTest(identifier string, payloadContent string) []byte { + tmpl := `{ + "Type": "com.apple.configuration.decl%s", + "Identifier": "com.fleet.config%s", + "Payload": { + "ServiceType": "com.apple.service%s" + } + }` + + declBytes := []byte(fmt.Sprintf(tmpl, identifier, identifier, payloadContent)) + return declBytes +} + func mobileconfigForTestWithContent(outerName, outerIdentifier, innerIdentifier, innerType, innerName string) []byte { if innerName == "" { innerName = outerName + ".inner" diff --git a/server/service/client.go b/server/service/client.go index 8384e3e54d..2bb5042235 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -294,8 +294,9 @@ func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fle } // by default, use the file name. macOS profiles use their PayloadDisplayName - name := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) - if mdm.GetRawProfilePlatform(fileContents) == "darwin" { + ext := filepath.Ext(filePath) + name := strings.TrimSuffix(filepath.Base(filePath), ext) + if mdm.GetRawProfilePlatform(fileContents) == "darwin" && ext == ".mobileconfig" { mc, err := fleet.NewMDMAppleConfigProfile(fileContents, nil) if err != nil { return nil, fmt.Errorf("applying fleet config: %w", err) diff --git a/server/service/handler.go b/server/service/handler.go index db3208a537..a67cf5cc8f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -1003,6 +1003,7 @@ func RegisterAppleMDMProtocolServices( scepStorage scep_depot.Depot, logger kitlog.Logger, checkinAndCommandService nanomdm_service.CheckinAndCommandService, + ddmService nanomdm_service.DeclarativeManagement, ) error { scepCACerts, scepCAKey, err := scepStorage.CA([]byte{}) if err != nil { @@ -1011,7 +1012,7 @@ func RegisterAppleMDMProtocolServices( if err := registerSCEP(mux, scepConfig, scepCACerts[0], scepCAKey, scepStorage, logger); err != nil { return fmt.Errorf("scep: %w", err) } - if err := registerMDM(mux, scepCACerts[0], mdmStorage, checkinAndCommandService, logger); err != nil { + if err := registerMDM(mux, scepCACerts[0], mdmStorage, checkinAndCommandService, ddmService, logger); err != nil { return fmt.Errorf("mdm: %w", err) } return nil @@ -1085,6 +1086,7 @@ func registerMDM( scepCACert *x509.Certificate, mdmStorage nanomdm_storage.AllStorage, checkinAndCommandService nanomdm_service.CheckinAndCommandService, + ddmService nanomdm_service.DeclarativeManagement, logger kitlog.Logger, ) error { certVerifier, err := certverify.NewPoolVerifier( @@ -1104,7 +1106,7 @@ func registerMDM( // enrollments and updates the Fleet hosts table accordingly with the UDID and serial number of // the device. // 5. Run actual MDM service operation (checkin handler or command and results handler). - coreMDMService := nanomdm.New(mdmStorage, nanomdm.WithLogger(mdmLogger)) + coreMDMService := nanomdm.New(mdmStorage, nanomdm.WithLogger(mdmLogger), nanomdm.WithDeclarativeManagement(ddmService)) // NOTE: it is critical that the coreMDMService runs first, as the first // service in the multi-service feature is run to completion _before_ running // the other ones in parallel. This way, subsequent services have access to diff --git a/server/service/integration_ddm_test.go b/server/service/integration_ddm_test.go new file mode 100644 index 0000000000..e8c77a192d --- /dev/null +++ b/server/service/integration_ddm_test.go @@ -0,0 +1,909 @@ +package service + +import ( + "bytes" + "context" + "crypto/md5" // nolint:gosec // used only for tests + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest" + "github.com/fleetdm/fleet/v4/server/datastore/mysql" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" + kitlog "github.com/go-kit/log" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() { + t := s.T() + tmpl := ` +{ + "Type": "com.apple.configuration.decl%d", + "Identifier": "com.fleet.config%d", + "Payload": { + "ServiceType": "com.apple.bash", + "DataAssetReference": "com.fleet.asset.bash" %s + } +}` + // TODO: figure out the best way to do this. We might even consider + // starting a different test suite. + t.Cleanup(func() { s.cleanupDeclarations(t) }) + + newDeclBytes := func(i int, payload ...string) []byte { + var p string + if len(payload) > 0 { + p = "," + strings.Join(payload, ",") + } + return []byte(fmt.Sprintf(tmpl, i, i, p)) + } + + var decls [][]byte + + for i := 0; i < 7; i++ { + decls = append(decls, newDeclBytes(i)) + } + + // Non-configuration type should fail + res := s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "bad", Contents: []byte(`{"Type": "com.apple.activation"}`)}, + }}, http.StatusUnprocessableEntity) + + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Only configuration declarations (com.apple.configuration) are supported") + + // "com.apple.configuration.softwareupdate.enforcement.specific" type should fail + res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "bad2", Contents: []byte(`{"Type": "com.apple.configuration.softwareupdate.enforcement.specific"}`)}, + }}, http.StatusUnprocessableEntity) + + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.") + + // Types from our list of forbidden types should fail + for ft := range fleet.ForbiddenDeclTypes { + res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "bad2", Contents: []byte(fmt.Sprintf(`{"Type": "%s"}`, ft))}, + }}, http.StatusUnprocessableEntity) + + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Only configuration declarations that don’t require an asset reference are supported.") + } + + // "com.apple.configuration.management.status-subscriptions" type should fail + res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "bad2", Contents: []byte(`{"Type": "com.apple.configuration.management.status-subscriptions"}`)}, + }}, http.StatusUnprocessableEntity) + + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Declaration profile can’t include status subscription type. To get host’s vitals, please use queries and policies.") + + // Two different payloads with the same name should fail + res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "bad2", Contents: newDeclBytes(1, `"foo": "bar"`)}, + {Name: "bad2", Contents: newDeclBytes(2, `"baz": "bing"`)}, + }}, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "A declaration profile with this name already exists.") + + // Same identifier should fail + res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "N1", Contents: decls[0]}, + {Name: "N2", Contents: decls[0]}, + }}, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "A declaration profile with this identifier already exists.") + + // Create 2 declarations + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "N1", Contents: decls[0]}, + {Name: "N2", Contents: decls[1]}, + }}, http.StatusNoContent) + + var resp listMDMConfigProfilesResponse + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) + + require.Len(t, resp.Profiles, 2) + require.Equal(t, "N1", resp.Profiles[0].Name) + require.Equal(t, "darwin", resp.Profiles[0].Platform) + require.Equal(t, "N2", resp.Profiles[1].Name) + require.Equal(t, "darwin", resp.Profiles[1].Platform) + + // Create 2 new declarations. These should take the place of the first two. + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "N3", Contents: decls[2]}, + {Name: "N4", Contents: decls[3]}, + }}, http.StatusNoContent) + + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) + + require.Len(t, resp.Profiles, 2) + require.Equal(t, "N3", resp.Profiles[0].Name) + require.Equal(t, "darwin", resp.Profiles[0].Platform) + require.Equal(t, "N4", resp.Profiles[1].Name) + require.Equal(t, "darwin", resp.Profiles[1].Platform) + + // replace only 1 declaration, the other one should be the same + + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "N3", Contents: decls[2]}, + {Name: "N5", Contents: decls[4]}, + }}, http.StatusNoContent) + + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) + + require.Len(t, resp.Profiles, 2) + require.Equal(t, "N3", resp.Profiles[0].Name) + require.Equal(t, "darwin", resp.Profiles[0].Platform) + require.Equal(t, "N5", resp.Profiles[1].Name) + require.Equal(t, "darwin", resp.Profiles[1].Platform) + + // update the declarations + + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "N3", Contents: newDeclBytes(2, `"foo": "bar"`)}, + {Name: "N5", Contents: newDeclBytes(4, `"bing": "baz"`)}, + }}, http.StatusNoContent) + + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) + + require.Len(t, resp.Profiles, 2) + require.Equal(t, "N3", resp.Profiles[0].Name) + require.Equal(t, "darwin", resp.Profiles[0].Platform) + require.Equal(t, "N5", resp.Profiles[1].Name) + require.Equal(t, "darwin", resp.Profiles[1].Platform) + + var createResp createLabelResponse + s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String("label_1"), Query: ptr.String("select 1")}, http.StatusOK, &createResp) + require.NotZero(t, createResp.Label.ID) + require.Equal(t, "label_1", createResp.Label.Name) + lbl1 := createResp.Label.Label + + s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String("label_2"), Query: ptr.String("select 1")}, http.StatusOK, &createResp) + require.NotZero(t, createResp.Label.ID) + require.Equal(t, "label_2", createResp.Label.Name) + lbl2 := createResp.Label.Label + + // Add with labels + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + {Name: "N5", Contents: decls[5], Labels: []string{lbl1.Name, lbl2.Name}}, + {Name: "N6", Contents: decls[6], Labels: []string{lbl1.Name}}, + }}, http.StatusNoContent) + + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) + + require.Len(t, resp.Profiles, 2) + require.Equal(t, "N5", resp.Profiles[0].Name) + require.Equal(t, "darwin", resp.Profiles[0].Platform) + require.Equal(t, "N6", resp.Profiles[1].Name) + require.Equal(t, "darwin", resp.Profiles[1].Platform) + require.Len(t, resp.Profiles[0].Labels, 2) + require.Equal(t, lbl1.Name, resp.Profiles[0].Labels[0].LabelName) + require.Equal(t, lbl2.Name, resp.Profiles[0].Labels[1].LabelName) + require.Len(t, resp.Profiles[1].Labels, 1) + require.Equal(t, lbl1.Name, resp.Profiles[1].Labels[0].LabelName) +} + +func (s *integrationMDMTestSuite) TestMDMAppleDeviceManagementRequests() { + t := s.T() + _, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t) + + calcChecksum := func(source []byte) string { + csum := fmt.Sprintf("%x", md5.Sum(source)) //nolint:gosec + return strings.ToUpper(csum) + } + + t.Cleanup(func() { s.cleanupDeclarations(t) }) + + insertDeclaration := func(t *testing.T, decl fleet.MDMAppleDeclaration) { + stmt := ` +INSERT INTO mdm_apple_declarations ( + declaration_uuid, + team_id, + identifier, + name, + raw_json, + checksum, + created_at, + uploaded_at +) VALUES (?,?,?,?,?,UNHEX(?),?,?)` + + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), stmt, + decl.DeclarationUUID, + decl.TeamID, + decl.Identifier, + decl.Name, + decl.RawJSON, + calcChecksum(decl.RawJSON), + decl.CreatedAt, + decl.UploadedAt, + ) + return err + }) + } + + insertHostDeclaration := func(t *testing.T, hostUUID string, decl fleet.MDMAppleDeclaration) { + stmt := ` +INSERT INTO host_mdm_apple_declarations ( + host_uuid, + status, + operation_type, + checksum, + declaration_uuid +) VALUES (?,?,?,UNHEX(?),?)` + + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), stmt, + hostUUID, + fleet.MDMDeliveryPending, + fleet.MDMOperationTypeInstall, + calcChecksum(decl.RawJSON), + decl.DeclarationUUID, + ) + return err + }) + } + + // initialize a time to use for our first declaration, subsequent declarations will be + // incremented by a minute + then := time.Now().UTC().Truncate(time.Second).Add(-1 * time.Hour) + + // insert a declaration with no team + noTeamDeclsByUUID := map[string]fleet.MDMAppleDeclaration{ + "123": { + DeclarationUUID: "123", + TeamID: ptr.Uint(0), + Identifier: "com.example", + Name: "Example", + RawJSON: json.RawMessage(`{ + "Type": "com.apple.configuration.declaration-items.test", + "Payload": {"foo":"bar"}, + "Identifier": "com.example" + }`), + CreatedAt: then, + UploadedAt: then, + }, + } + insertDeclaration(t, noTeamDeclsByUUID["123"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["123"]) + + mapDeclsByChecksum := func(byUUID map[string]fleet.MDMAppleDeclaration) map[string]fleet.MDMAppleDeclaration { + byChecksum := make(map[string]fleet.MDMAppleDeclaration) + for _, d := range byUUID { + byChecksum[calcChecksum(d.RawJSON)] = byUUID[d.DeclarationUUID] + } + return byChecksum + } + + parseTokensResp := func(r *http.Response) fleet.MDMAppleDDMTokensResponse { + require.NotNil(t, r) + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + defer r.Body.Close() + r.Body = io.NopCloser(bytes.NewBuffer(b)) + // t.Log("body", string(b)) + + // unmarsal the response to make sure it's valid + var tok fleet.MDMAppleDDMTokensResponse + err = json.NewDecoder(r.Body).Decode(&tok) + require.NoError(t, err) + // t.Log("decoded", tok) + + return tok + } + + parseDeclarationItemsResp := func(r *http.Response) fleet.MDMAppleDDMDeclarationItemsResponse { + require.NotNil(t, r) + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + defer r.Body.Close() + r.Body = io.NopCloser(bytes.NewBuffer(b)) + // t.Log("body", string(b)) + + // unmarsal the response to make sure it's valid + var di fleet.MDMAppleDDMDeclarationItemsResponse + err = json.NewDecoder(r.Body).Decode(&di) + require.NoError(t, err) + // t.Log("decoded", di) + + return di + } + + assertDeclarationResponse := func(r *http.Response, expected fleet.MDMAppleDeclaration) { + require.NotNil(t, r) + + // unmarsal the response and assert it's valid + var wantParsed fleet.MDMAppleDDMDeclarationResponse + require.NoError(t, json.Unmarshal(expected.RawJSON, &wantParsed)) + var gotParsed fleet.MDMAppleDDMDeclarationResponse + require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed)) + require.EqualValues(t, wantParsed.Payload, gotParsed.Payload) + require.Equal(t, calcChecksum(expected.RawJSON), gotParsed.ServerToken) + require.Equal(t, expected.Identifier, gotParsed.Identifier) + // t.Logf("decoded: %+v", gotParsed) + } + + checkTokensResp := func(t *testing.T, r fleet.MDMAppleDDMTokensResponse, expectedTimestamp time.Time, prevToken string) { + require.Equal(t, expectedTimestamp, r.SyncTokens.Timestamp) + require.NotEmpty(t, r.SyncTokens.DeclarationsToken) + require.NotEqual(t, prevToken, r.SyncTokens.DeclarationsToken) + } + + checkDeclarationItemsResp := func(t *testing.T, r fleet.MDMAppleDDMDeclarationItemsResponse, expectedDeclTok string, expectedDeclsByChecksum map[string]fleet.MDMAppleDeclaration) { + require.Equal(t, expectedDeclTok, r.DeclarationsToken) + // TODO(roberto): better assertions + require.NotEmpty(t, r.Declarations.Activations) + require.Empty(t, r.Declarations.Assets) + require.Empty(t, r.Declarations.Management) + require.Len(t, r.Declarations.Configurations, len(expectedDeclsByChecksum)) + for _, m := range r.Declarations.Configurations { + d, ok := expectedDeclsByChecksum[m.ServerToken] + require.True(t, ok) + require.Equal(t, d.Identifier, m.Identifier) + } + } + + checkRequestsDatabase := func(t *testing.T, messageType, enrollmentID string, expectedCount int) { + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var count int + if err := sqlx.GetContext( + context.Background(), + q, + &count, + "SELECT count(*) AS count FROM mdm_apple_declarative_requests WHERE enrollment_id = ? AND message_type = ?", + enrollmentID, + messageType, + ); err != nil { + return err + } + + require.Equal(t, expectedCount, count, "unexpected db row count for declaration requests") + + return nil + }) + } + + var currDeclToken string // we'll use this to track the expected token across tests + + t.Run("Tokens", func(t *testing.T) { + checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 0) + // get tokens, timestamp should be the same as the declaration and token should be non-empty + r, err := mdmDevice.DeclarativeManagement("tokens") + require.NoError(t, err) + parsed := parseTokensResp(r) + checkTokensResp(t, parsed, then, "") + currDeclToken = parsed.SyncTokens.DeclarationsToken + + // insert a new declaration + noTeamDeclsByUUID["456"] = fleet.MDMAppleDeclaration{ + DeclarationUUID: "456", + TeamID: ptr.Uint(0), + Identifier: "com.example2", + Name: "Example2", + RawJSON: json.RawMessage(`{ + "Type": "com.apple.configuration.declaration-items.test", + "Payload": {"foo":"baz"}, + "Identifier": "com.example2" + }`), + CreatedAt: then.Add(1 * time.Minute), + UploadedAt: then.Add(1 * time.Minute), + } + insertDeclaration(t, noTeamDeclsByUUID["456"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["456"]) + checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 1) + + // get tokens again, timestamp and token should have changed + r, err = mdmDevice.DeclarativeManagement("tokens") + require.NoError(t, err) + parsed = parseTokensResp(r) + checkTokensResp(t, parsed, then.Add(1*time.Minute), currDeclToken) + currDeclToken = parsed.SyncTokens.DeclarationsToken + checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 2) + }) + + t.Run("DeclarationItems", func(t *testing.T) { + checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 0) + r, err := mdmDevice.DeclarativeManagement("declaration-items") + require.NoError(t, err) + checkDeclarationItemsResp(t, parseDeclarationItemsResp(r), currDeclToken, mapDeclsByChecksum(noTeamDeclsByUUID)) + + // insert a new declaration + noTeamDeclsByUUID["789"] = fleet.MDMAppleDeclaration{ + DeclarationUUID: "789", + TeamID: ptr.Uint(0), + Identifier: "com.example3", + Name: "Example3", + RawJSON: json.RawMessage(`{ + "Type": "com.apple.configuration.declaration-items.test", + "Payload": {"foo":"bang"}, + "Identifier": "com.example3" + }`), + CreatedAt: then.Add(2 * time.Minute), + UploadedAt: then.Add(2 * time.Minute), + } + insertDeclaration(t, noTeamDeclsByUUID["789"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["789"]) + checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 1) + + // get tokens again, timestamp and token should have changed + r, err = mdmDevice.DeclarativeManagement("tokens") + require.NoError(t, err) + toks := parseTokensResp(r) + checkTokensResp(t, toks, then.Add(2*time.Minute), currDeclToken) + currDeclToken = toks.SyncTokens.DeclarationsToken + + r, err = mdmDevice.DeclarativeManagement("declaration-items") + require.NoError(t, err) + checkDeclarationItemsResp(t, parseDeclarationItemsResp(r), currDeclToken, mapDeclsByChecksum(noTeamDeclsByUUID)) + checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 2) + }) + + t.Run("Status", func(t *testing.T) { + checkRequestsDatabase(t, "status", mdmDevice.UUID, 0) + _, err := mdmDevice.DeclarativeManagement("status", fleet.MDMAppleDDMStatusReport{}) + require.NoError(t, err) + checkRequestsDatabase(t, "status", mdmDevice.UUID, 1) + }) + + t.Run("Declaration", func(t *testing.T) { + want := noTeamDeclsByUUID["123"] + declarationPath := fmt.Sprintf("declaration/%s/%s", "configuration", want.Identifier) + checkRequestsDatabase(t, declarationPath, mdmDevice.UUID, 0) + r, err := mdmDevice.DeclarativeManagement(declarationPath) + require.NoError(t, err) + + assertDeclarationResponse(r, want) + + // insert a new declaration + noTeamDeclsByUUID["abc"] = fleet.MDMAppleDeclaration{ + DeclarationUUID: "abc", + TeamID: ptr.Uint(0), + Identifier: "com.example4", + Name: "Example4", + RawJSON: json.RawMessage(`{ + "Type": "com.apple.configuration.test", + "Payload": {"foo":"bar"}, + "Identifier": "com.example4" + }`), + CreatedAt: then.Add(3 * time.Minute), + UploadedAt: then.Add(3 * time.Minute), + } + insertDeclaration(t, noTeamDeclsByUUID["abc"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["abc"]) + want = noTeamDeclsByUUID["abc"] + r, err = mdmDevice.DeclarativeManagement(fmt.Sprintf("declaration/%s/%s", "configuration", want.Identifier)) + require.NoError(t, err) + checkRequestsDatabase(t, declarationPath, mdmDevice.UUID, 1) + + // try getting a non-existent declaration, should fail 404 + nonExistantDeclarationPath := fmt.Sprintf("declaration/%s/%s", "configuration", "nonexistent") + checkRequestsDatabase(t, nonExistantDeclarationPath, mdmDevice.UUID, 0) + _, err = mdmDevice.DeclarativeManagement(nonExistantDeclarationPath) + require.Error(t, err) + require.ErrorContains(t, err, "404 Not Found") + checkRequestsDatabase(t, nonExistantDeclarationPath, mdmDevice.UUID, 1) + + // typo should fail as bad request + typoDeclarationPath := fmt.Sprintf("declarations/%s/%s", "configurations", want.Identifier) + checkRequestsDatabase(t, typoDeclarationPath, mdmDevice.UUID, 0) + _, err = mdmDevice.DeclarativeManagement(typoDeclarationPath) + require.Error(t, err) + require.ErrorContains(t, err, "400 Bad Request") + checkRequestsDatabase(t, typoDeclarationPath, mdmDevice.UUID, 1) + + assertDeclarationResponse(r, want) + }) +} + +func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() { + t := s.T() + ctx := context.Background() + // TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG + logger := kitlog.NewJSONLogger(os.Stdout) + + // TODO: use endpoints once those are available. + addDeclaration := func(identifier string, teamID uint) { + stmt := ` + INSERT INTO mdm_apple_declarations + (declaration_uuid, team_id, identifier, name, raw_json, checksum) + VALUES + (UUID(), ?, ?, UUID(), ?, HEX(MD5(raw_json)) )` + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, stmt, teamID, identifier, declarationForTest(identifier)) + return err + }) + } + + deleteDeclaration := func(identifier string, teamID uint) { + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations WHERE team_id = ? AND identifier = ?", teamID, identifier) + return err + }) + } + + // create a team + teamName := t.Name() + "team1" + team := &fleet.Team{ + Name: teamName, + } + var createTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp) + require.NotZero(t, createTeamResp.Team.ID) + team = createTeamResp.Team + + // TODO: figure out the best way to do this. We might even consider + // starting a different test suite. + t.Cleanup(func() { s.cleanupDeclarations(t) }) + + checkNoCommands := func(d *mdmtest.TestAppleMDMClient) { + cmd, err := d.Idle() + require.NoError(t, err) + require.Nil(t, cmd) + } + + checkDDMSync := func(d *mdmtest.TestAppleMDMClient) { + cmd, err := d.Idle() + require.NoError(t, err) + require.NotNil(t, cmd) + require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType) + cmd, err = d.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + require.Nil(t, cmd) + _, err = d.DeclarativeManagement("tokens") + require.NoError(t, err) + } + + // create a windows host + _, err := s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 1, + OsqueryHostID: ptr.String("non-macos-host"), + NodeKey: ptr.String("non-macos-host"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.non.macos", t.Name()), + Platform: "windows", + }) + require.NoError(t, err) + + // create a windows host that's enrolled in MDM + _, _ = createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t) + + // create a linux host + _, err = s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 2, + OsqueryHostID: ptr.String("linux-host"), + NodeKey: ptr.String("linux-host"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.linux", t.Name()), + Platform: "linux", + }) + require.NoError(t, err) + + // create a host that's not enrolled into MDM + _, err = s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 2, + OsqueryHostID: ptr.String("not-mdm-enrolled"), + NodeKey: ptr.String("not-mdm-enrolled"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()), + Platform: "darwin", + }) + require.NoError(t, err) + + // create a host and then enroll in MDM. + mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t) + + // trigger the reconciler, no error + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + + // declarativeManagement command is not sent. + checkNoCommands(device) + + // add global declarations + addDeclaration("I1", 0) + addDeclaration("I2", 0) + + // reconcile again, this time new declarations were added + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + + // TODO: check command is pending + + // declarativeManagement command is sent + checkDDMSync(device) + + // reconcile again, commands for the uploaded declarations are already sent + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // no new commands are sent + checkNoCommands(device) + + // delete a declaration + deleteDeclaration("I1", 0) + // reconcile again + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // a DDM sync is triggered + checkDDMSync(device) + + // add a new host + _, deviceTwo := createHostThenEnrollMDM(s.ds, s.server.URL, t) + // reconcile again + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // DDM sync is triggered only for the new host + checkNoCommands(device) + checkDDMSync(deviceTwo) + + // add device to the team + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHost.ID}}, http.StatusOK) + + // reconcile + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + + // DDM sync is triggered only for the transferred host + // because the team doesn't have any declarations + checkDDMSync(device) + checkNoCommands(deviceTwo) + + // reconcile + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // nobody receives commands this time + checkNoCommands(device) + checkNoCommands(deviceTwo) + + // add declarations to the team + addDeclaration("I1", team.ID) + addDeclaration("I2", team.ID) + + // reconcile + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // DDM sync is triggered for the host in the team + checkDDMSync(device) + checkNoCommands(deviceTwo) + + // add a new host, this one belongs to the team + mdmHostThree, deviceThree := createHostThenEnrollMDM(s.ds, s.server.URL, t) + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHostThree.ID}}, http.StatusOK) + + // reconcile + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // DDM sync is triggered only for the new host + checkNoCommands(device) + checkNoCommands(deviceTwo) + checkDDMSync(deviceThree) + + // no new commands after another reconciliation + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + checkNoCommands(device) + checkNoCommands(deviceTwo) + checkNoCommands(deviceThree) + + // TODO: use proper APIs for this + // add a new label + label declaration + addDeclaration("I3", team.ID) + label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: t.Name(), Query: "select 1;"}) + require.NoError(t, err) + // update label with host membership + mysql.ExecAdhocSQL( + t, s.ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext( + context.Background(), + "INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)", + mdmHostThree.ID, + label.ID, + ) + return err + }, + ) + + // update declaration <-> label mapping + mysql.ExecAdhocSQL( + t, s.ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext( + context.Background(), + `INSERT INTO + mdm_declaration_labels (apple_declaration_uuid, label_name, label_id) + VALUES ((SELECT declaration_uuid FROM mdm_apple_declarations WHERE team_id = ? and identifier = ?), ?, ?)`, + team.ID, + "I3", + label.Name, + label.ID, + ) + return err + }, + ) + + // reconcile + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + // DDM sync is triggered only for the host with the label + checkNoCommands(device) + checkNoCommands(deviceTwo) + checkDDMSync(deviceThree) +} + +func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() { + t := s.T() + ctx := context.Background() + // TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG + logger := kitlog.NewJSONLogger(os.Stdout) + + // TODO: figure out the best way to do this. We might even consider + // starting a different test suite. + t.Cleanup(func() { s.cleanupDeclarations(t) }) + + assertHostDeclarations := func(hostUUID string, wantDecls []*fleet.MDMAppleHostDeclaration) { + var gotDecls []*fleet.MDMAppleHostDeclaration + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.SelectContext(context.Background(), q, &gotDecls, `SELECT declaration_identifier, status, operation_type FROM host_mdm_apple_declarations WHERE host_uuid = ?`, hostUUID) + }) + require.ElementsMatch(t, wantDecls, gotDecls) + } + + // create a host and then enroll in MDM. + mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t) + + declarations := []fleet.MDMProfileBatchPayload{ + {Name: "N1.json", Contents: declarationForTest("I1")}, + {Name: "N2.json", Contents: declarationForTest("I2")}, + } + // add global declarations + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent) + + // reconcile profiles + err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + + // declarations are ("install", "pending") after the cron run + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + {Identifier: "I2", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall}, + }) + + // host gets a DDM sync call + cmd, err := device.Idle() + require.NoError(t, err) + require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType) + _, err = device.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + + r, err := device.DeclarativeManagement("declaration-items") + require.NoError(t, err) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + var items fleet.MDMAppleDDMDeclarationItemsResponse + require.NoError(t, json.Unmarshal(body, &items)) + + var i1ServerToken, i2ServerToken string + for _, d := range items.Declarations.Configurations { + switch d.Identifier { + case "I1": + i1ServerToken = d.ServerToken + case "I2": + i2ServerToken = d.ServerToken + } + } + + // declarations are ("install", "verifying") after the ack + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall}, + {Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall}, + }) + + // host sends a partial DDM report + report := fleet.MDMAppleDDMStatusReport{} + report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{ + {Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken}, + } + _, err = device.DeclarativeManagement("status", report) + require.NoError(t, err) + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall}, + {Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall}, + }) + + // host sends a report with a wrong (could be old) server token for I2, nothing changes + report = fleet.MDMAppleDDMStatusReport{} + report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{ + {Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I2", ServerToken: "foo"}, + } + _, err = device.DeclarativeManagement("status", report) + require.NoError(t, err) + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall}, + {Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall}, + }) + + // host sends a full report, declaration I2 is invalid + report = fleet.MDMAppleDDMStatusReport{} + report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{ + {Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken}, + {Active: false, Valid: fleet.MDMAppleDeclarationInvalid, Identifier: "I2", ServerToken: i2ServerToken}, + } + _, err = device.DeclarativeManagement("status", report) + require.NoError(t, err) + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall}, + {Identifier: "I2", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall}, + }) + + // do a batch request, this time I2 is deleted + declarations = []fleet.MDMProfileBatchPayload{ + {Name: "N1.json", Contents: declarationForTest("I1")}, + } + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent) + + // reconcile profiles + err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger) + require.NoError(t, err) + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall}, + {Identifier: "I2", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove}, + }) + + // host sends a report, declaration I2 is removed from the hosts_* table + report = fleet.MDMAppleDDMStatusReport{} + report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{ + {Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken}, + } + _, err = device.DeclarativeManagement("status", report) + require.NoError(t, err) + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall}, + }) + + // host sends a report, declaration I1 is failing after a while + report = fleet.MDMAppleDDMStatusReport{} + report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{ + {Active: false, Valid: fleet.MDMAppleDeclarationInvalid, Identifier: "I1", ServerToken: i1ServerToken}, + } + _, err = device.DeclarativeManagement("status", report) + require.NoError(t, err) + assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{ + {Identifier: "I1", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall}, + }) +} + +func declarationForTest(identifier string) []byte { + return []byte(fmt.Sprintf(` +{ + "Type": "com.apple.configuration.management.test", + "Payload": { + "Echo": "foo" + }, + "Identifier": "%s" +}`, identifier)) +} + +func (s *integrationMDMTestSuite) cleanupDeclarations(t *testing.T) { + ctx := context.Background() + // TODO: figure out the best way to do this. We might even consider + // starting a different test suite. + // delete declarations to not affect other tests + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations") + return err + }) + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM host_mdm_apple_declarations") + return err + }) + +} diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 4e83e3c0fe..e3f36ea5d7 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -25,6 +25,7 @@ import ( "github.com/fleetdm/fleet/v4/server/worker" kitlog "github.com/go-kit/log" "github.com/google/uuid" + "github.com/groob/plist" "github.com/jmoiron/sqlx" micromdm "github.com/micromdm/micromdm/mdm/mdm" "github.com/stretchr/testify/require" @@ -264,7 +265,9 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de //default: // fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType) //} - cmds = append(cmds, cmd) + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + cmds = append(cmds, &fullCmd) cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) require.NoError(t, err) } @@ -327,7 +330,9 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de cmd, err = mdmDevice.Idle() require.NoError(t, err) for cmd != nil { - cmds = append(cmds, cmd) + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + cmds = append(cmds, &fullCmd) cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) require.NoError(t, err) } @@ -374,12 +379,14 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { cmd, err := mdmDevice.Idle() require.NoError(t, err) for cmd != nil { - if cmd.Command.RequestType == "InstallEnterpriseApplication" && - cmd.Command.InstallEnterpriseApplication.ManifestURL != nil && - strings.Contains(*cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) { - fleetdCmd = cmd + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + if fullCmd.Command.RequestType == "InstallEnterpriseApplication" && + fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil && + strings.Contains(*fullCmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) { + fleetdCmd = &fullCmd } else if cmd.Command.RequestType == "InstallProfile" { - installProfileCmd = cmd + installProfileCmd = &fullCmd } cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) require.NoError(t, err) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index f439ce670e..017704ef02 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -1272,7 +1272,7 @@ func (s *integrationMDMTestSuite) TestWindowsProfileRetries() { } func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forceDeviceErr bool) ([][]byte, []string) { - var cmd *micromdm.CommandPayload + var cmd *mdm.Command var err error installs := [][]byte{} removes := []string{} @@ -1297,11 +1297,13 @@ func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forc break } + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) switch cmd.Command.RequestType { case "InstallProfile": - installs = append(installs, cmd.Command.InstallProfile.Payload) + installs = append(installs, fullCmd.Command.InstallProfile.Payload) case "RemoveProfile": - removes = append(removes, cmd.Command.RemoveProfile.Identifier) + removes = append(removes, fullCmd.Command.RemoveProfile.Identifier) } } @@ -2048,9 +2050,14 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() { mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo) err := mdmDeviceA.Enroll() require.NoError(t, err) + s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(), + fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0) + mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo) err = mdmDeviceB.Enroll() require.NoError(t, err) + s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(), + fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), 0) // Find the ID of Fleet's MDM solution var mdmID uint @@ -2079,23 +2086,6 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() { } } - // Activities are generated for each device - activities := listActivitiesResponse{} - s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities, "order_key", "created_at") - require.GreaterOrEqual(t, len(activities.Activities), 2) - - details := []*json.RawMessage{} - for _, activity := range activities.Activities { - if activity.Type == "mdm_enrolled" { - require.Nil(t, activity.ActorID) - require.Nil(t, activity.ActorFullName) - details = append(details, activity.Details) - } - } - require.Len(t, details, 2) - require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*details[len(details)-2])) - require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), string(*details[len(details)-1])) - // set an enroll secret var applyResp applyEnrollSecretSpecResponse s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{ @@ -2132,7 +2122,7 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() { require.NoError(t, err) // An activity is created - activities = listActivitiesResponse{} + activities := listActivitiesResponse{} s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities) found := false @@ -2141,7 +2131,6 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() { found = true require.Nil(t, activity.ActorID) require.Nil(t, activity.ActorFullName) - details = append(details, activity.Details) require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*activity.Details)) } } @@ -5168,9 +5157,13 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() { cmd, err := d.device.Idle() require.NoError(t, err) for cmd != nil { + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + // if the command is to install the bootstrap package - if manifest := cmd.Command.InstallEnterpriseApplication.Manifest; manifest != nil { + if manifest := fullCmd.Command.InstallEnterpriseApplication.Manifest; manifest != nil { require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType) + require.NotNil(t, manifest) require.Equal(t, "software-package", (*manifest).ManifestItems[0].Assets[0].Kind) wantURL, err := bp.URL(s.server.URL) require.NoError(t, err) @@ -6867,7 +6860,7 @@ func (s *integrationMDMTestSuite) TestSSO() { s.runWorker() // ask for commands and verify that we get AccountConfiguration - var accCmd *micromdm.CommandPayload + var accCmd *mdm.Command cmd, err := mdmDevice.Idle() require.NoError(t, err) for cmd != nil { @@ -6879,9 +6872,12 @@ func (s *integrationMDMTestSuite) TestSSO() { } require.NotNil(t, accCmd) require.NotNil(t, accCmd.Command) - require.True(t, accCmd.Command.AccountConfiguration.LockPrimaryAccountInfo) - require.Equal(t, "SSO User 1", accCmd.Command.AccountConfiguration.PrimaryAccountFullName) - require.Equal(t, "sso_user", accCmd.Command.AccountConfiguration.PrimaryAccountUserName) + + var fullAccCmd *micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(accCmd.Raw, &fullAccCmd)) + require.True(t, fullAccCmd.Command.AccountConfiguration.LockPrimaryAccountInfo) + require.Equal(t, "SSO User 1", fullAccCmd.Command.AccountConfiguration.PrimaryAccountFullName) + require.Equal(t, "sso_user", fullAccCmd.Command.AccountConfiguration.PrimaryAccountUserName) // report host details for the device var hostResp getHostResponse @@ -8725,6 +8721,8 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"}) require.NoError(t, err) + t.Cleanup(func() { s.cleanupDeclarations(t) }) + assertAppleProfile := func(filename, name, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string { fields := map[string][]string{ "labels": labelNames, @@ -8750,6 +8748,39 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { require.Equal(t, "a", string(resp.ProfileUUID[0])) return resp.ProfileUUID } + assertAppleDeclaration := func(filename, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string { + fields := map[string][]string{ + "labels": labelNames, + } + if teamID > 0 { + fields["team_id"] = []string{fmt.Sprintf("%d", teamID)} + } + + bytes := []byte(fmt.Sprintf(`{ + "Type": "com.apple.configuration.foo", + "Payload": { + "Echo": "f1337" + }, + "Identifier": "%s" +}`, ident)) + + body, headers := generateNewProfileMultipartRequest(t, filename, bytes, s.token, fields) + res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), wantStatus, headers) + + if wantErrMsg != "" { + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, wantErrMsg) + return "" + } + + var resp newMDMConfigProfileResponse + err := json.NewDecoder(res.Body).Decode(&resp) + require.NoError(t, err) + require.NotEmpty(t, resp.ProfileUUID) + require.Equal(t, fleet.MDMAppleDeclarationUUIDPrefix, string(resp.ProfileUUID[0])) + return resp.ProfileUUID + } + createAppleProfile := func(name, ident string, teamID uint, labelNames []string) string { uid := assertAppleProfile(name+".mobileconfig", name, ident, teamID, labelNames, http.StatusOK, "") @@ -8764,6 +8795,20 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { return uid } + createAppleDeclaration := func(name, ident string, teamID uint, labelNames []string) string { + uid := assertAppleDeclaration(name+".json", ident, teamID, labelNames, http.StatusOK, "") + + var wantJSON string + if teamID == 0 { + wantJSON = fmt.Sprintf(`{"team_id": null, "team_name": null, "profile_name": %q, "identifier": %q}`, name, ident) + } else { + wantJSON = fmt.Sprintf(`{"team_id": %d, "team_name": %q, "profile_name": %q, "identifier": %q}`, teamID, testTeam.Name, name, ident) + } + s.lastActivityOfTypeMatches(fleet.ActivityTypeCreatedDeclarationProfile{}.ActivityName(), wantJSON, 0) + + return uid + } + assertWindowsProfile := func(filename, locURI string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string { fields := map[string][]string{ "labels": labelNames, @@ -8831,9 +8876,27 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // but no conflict for no-team assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", 0, nil, http.StatusOK, "") + // add some macOS declarations + createAppleDeclaration("apple-declaration", "test-declaration-ident", 0, nil) + // identifier must be unique, it conflicts with existing declaration + assertAppleDeclaration("apple-declaration.json", "test-declaration-ident", 0, nil, http.StatusConflict, "test-declaration-ident already exists") + // name is pulled from filename, it conflicts with existing declaration + assertAppleDeclaration("apple-declaration.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, "apple-declaration already exists") + // uniqueness is checked only within team, so it's fine to have the same name and identifier in different teams + assertAppleDeclaration("apple-declaration.json", "test-declaration-ident", testTeam.ID, nil, http.StatusOK, "") + // name is pulled from filename, it conflicts with existing macOS config profile + assertAppleDeclaration("apple-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, "apple-global-profile already exists") + // name is pulled from filename, it conflicts with existing macOS config profile + assertAppleDeclaration("win-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, "win-global-profile already exists") + // windows profile name conflicts with existing declaration + assertWindowsProfile("apple-declaration.xml", "./Test", 0, nil, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.") + // macOS profile name conflicts with existing declaration + assertAppleProfile("apple-declaration.mobileconfig", "apple-declaration", "test-declaration-ident", 0, nil, http.StatusConflict, "Couldn't upload. A configuration profile with this name already exists.") + // not an xml nor mobileconfig file - assertWindowsProfile("foo.txt", "./Test", 0, nil, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.") - assertAppleProfile("foo.txt", "foo", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.") + assertWindowsProfile("foo.txt", "./Test", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.") + assertAppleProfile("foo.txt", "foo", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.") + assertAppleDeclaration("foo.txt", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.") // Windows-reserved LocURI assertWindowsProfile("bitlocker.xml", syncml.FleetBitLockerTargetLocURI, 0, nil, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include BitLocker settings.") @@ -8842,11 +8905,13 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // Fleet-reserved profiles for name := range servermdm.FleetReservedProfileNames() { assertAppleProfile(name+".mobileconfig", name, name+"-ident", 0, nil, http.StatusBadRequest, fmt.Sprintf(`name %s is not allowed`, name)) + assertAppleDeclaration(name+".json", name+"-ident", 0, nil, http.StatusBadRequest, fmt.Sprintf(`name %q is not allowed`, name)) assertWindowsProfile(name+".xml", "./Test", 0, nil, http.StatusBadRequest, fmt.Sprintf(`Couldn't upload. Profile name %q is not allowed.`, name)) } // profiles with non-existent labels assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist") + assertAppleDeclaration("apple-declaration-with-labels.json", "ident-with-labels", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist") assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist") // create a couple of labels @@ -8859,26 +8924,33 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // profiles mixing existent and non-existent labels assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"does-not-exist", "foo"}, http.StatusBadRequest, "some or all the labels provided don't exist") + assertAppleDeclaration("apple-declaration-with-labels.json", "ident-with-labels", 0, []string{"does-not-exist", "foo"}, http.StatusBadRequest, "some or all the labels provided don't exist") assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, "some or all the labels provided don't exist") // profiles with valid labels uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"foo"}, http.StatusOK, "") + uuidAppleDDMWithLabel := createAppleDeclaration("apple-decl-with-labels", "ident-decl-with-labels", 0, []string{"foo"}) uuidWindowsWithLabel := assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"foo", "bar"}, http.StatusOK, "") // verify that the label associations have been created // TODO: update when we have datastore methods to get this data var profileLabels []fleet.ConfigurationProfileLabel mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - stmt := `SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, label_id FROM mdm_configuration_profile_labels` + stmt := ` + SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, COALESCE(label_id, 0) as label_id + FROM mdm_configuration_profile_labels + UNION SELECT apple_declaration_uuid as profile_uuid, label_name, COALESCE(label_id, 0) as label_id + FROM mdm_declaration_labels ORDER BY profile_uuid, label_name;` return sqlx.SelectContext(context.Background(), q, &profileLabels, stmt) }) require.NotEmpty(t, profileLabels) - require.Len(t, profileLabels, 3) + require.Len(t, profileLabels, 4) require.ElementsMatch( t, []fleet.ConfigurationProfileLabel{ {ProfileUUID: uuidAppleWithLabel, LabelName: labelFoo.Name, LabelID: labelFoo.ID}, + {ProfileUUID: uuidAppleDDMWithLabel, LabelName: labelFoo.Name, LabelID: labelFoo.ID}, {ProfileUUID: uuidWindowsWithLabel, LabelName: labelFoo.Name, LabelID: labelFoo.ID}, {ProfileUUID: uuidWindowsWithLabel, LabelName: labelBar.Name, LabelID: labelBar.ID}, }, @@ -8891,19 +8963,27 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { errMsg := extractServerErrorText(res.Body) require.Contains(t, errMsg, "Couldn't upload. The file should include valid XML:") - // Apple invalid content + // Apple invalid mobileconfig content body, headers = generateNewProfileMultipartRequest(t, "apple.mobileconfig", []byte("\x00\x01\x02"), s.token, nil) res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers) errMsg = extractServerErrorText(res.Body) require.Contains(t, errMsg, "mobileconfig is not XML nor PKCS7 parseable") + // Apple invalid json declaration + body, headers = generateNewProfileMultipartRequest(t, + "apple.json", []byte("{"), s.token, nil) + res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't upload. The file should include valid JSON:") + // get the existing profiles work expectedProfiles := []fleet.MDMConfigProfilePayload{ {ProfileUUID: noTeamAppleProfUUID, Platform: "darwin", Name: "apple-global-profile", Identifier: "test-global-ident", TeamID: nil}, {ProfileUUID: teamAppleProfUUID, Platform: "darwin", Name: "apple-team-profile", Identifier: "test-team-ident", TeamID: &testTeam.ID}, {ProfileUUID: noTeamWinProfUUID, Platform: "windows", Name: "win-global-profile", TeamID: nil}, {ProfileUUID: teamWinProfUUID, Platform: "windows", Name: "win-team-profile", TeamID: &testTeam.ID}, + {ProfileUUID: uuidAppleDDMWithLabel, Platform: "darwin", Name: "apple-decl-with-labels", Identifier: "ident-decl-with-labels", TeamID: nil, Labels: []fleet.ConfigurationProfileLabel{{LabelID: labelFoo.ID, LabelName: labelFoo.Name}}}, } for _, prof := range expectedProfiles { var getResp getMDMConfigProfileResponse @@ -8922,8 +9002,10 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { resp := s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", prof.ProfileUUID), nil, http.StatusOK, "alt", "media") require.NotZero(t, resp.ContentLength) require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;") - if getResp.Platform == "darwin" { + if strings.HasPrefix(prof.ProfileUUID, "a") { require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config") + } else if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) { + require.Contains(t, resp.Header.Get("Content-Type"), "application/json") } else { require.Contains(t, resp.Header.Get("Content-Type"), "application/octet-stream") } @@ -8938,6 +9020,9 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // get an unknown Apple profile s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, &getResp) s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, "alt", "media") + // get an unknown Apple declaration + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, &getResp) + s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, "alt", "media") // get an unknown Windows profile s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, &getResp) s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, "alt", "media") @@ -8948,6 +9033,16 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamAppleProfUUID), nil, http.StatusOK, &deleteResp) // delete non-existing Apple profile s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, &deleteResp) + + // delete existing Apple declaration + s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", uuidAppleDDMWithLabel), nil, http.StatusOK, &deleteResp) + s.lastActivityOfTypeMatches( + fleet.ActivityTypeDeletedDeclarationProfile{}.ActivityName(), + `{"profile_name": "apple-decl-with-labels", "identifier": "ident-decl-with-labels", "team_id": null, "team_name": null}`, + 0, + ) + // delete non-existing Apple declaration + s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, &deleteResp) // delete existing Windows profiles s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", noTeamWinProfUUID), nil, http.StatusOK, &deleteResp) s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamWinProfUUID), nil, http.StatusOK, &deleteResp) @@ -8978,6 +9073,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { return err }) } + // TODO: Add tests for create/delete forbidden declaration types? // make fleet add a FileVault profile acResp := appConfigResponse{} @@ -8999,6 +9095,8 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { // try to delete the profile s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profUUID), nil, http.StatusBadRequest, &deleteResp) + + // TODO: Add tests for OS updates declaration when implemented. } func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { @@ -11029,10 +11127,12 @@ func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() { cmd, err := mdmDevice.Idle() require.NoError(t, err) for cmd != nil { - if manifest := cmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil { + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + if manifest := fullCmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil { foundInstallFleetdCommand = true require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType) - require.Contains(t, *cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) + require.Contains(t, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) } cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) require.NoError(t, err) @@ -11706,7 +11806,10 @@ func (s *integrationMDMTestSuite) TestDontIgnoreAnyProfileErrors() { for cmd != nil { if cmd.Command.RequestType == "RemoveProfile" { var errChain []mdm.ErrorChain - if cmd.Command.RemoveProfile.Identifier == "I1" { + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + + if fullCmd.Command.RemoveProfile.Identifier == "I1" { errChain = append(errChain, mdm.ErrorChain{ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'I1' not found."}) } else { errChain = append(errChain, mdm.ErrorChain{ErrorCode: 96, ErrorDomain: "MDMClientError", USEnglishDescription: "Cannot replace profile 'I2' because it was not installed by the MDM server."}) @@ -11836,7 +11939,7 @@ func (s *integrationMDMTestSuite) TestSCEPCertExpiration() { require.NoError(t, err) checkRenewCertCommand := func(device *mdmtest.TestAppleMDMClient, enrollRef string) { - var renewCmd *micromdm.CommandPayload + var renewCmd *mdm.Command cmd, err := device.Idle() require.NoError(t, err) for cmd != nil { @@ -11847,7 +11950,9 @@ func (s *integrationMDMTestSuite) TestSCEPCertExpiration() { require.NoError(t, err) } require.NotNil(t, renewCmd) - s.verifyEnrollmentProfile(renewCmd.Command.InstallProfile.Payload, enrollRef) + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(renewCmd.Raw, &fullCmd)) + s.verifyEnrollmentProfile(fullCmd.Command.InstallProfile.Payload, enrollRef) } checkRenewCertCommand(manualEnrolledDevice, "") diff --git a/server/service/mdm.go b/server/service/mdm.go index 82c23aa710..63c71ba286 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1019,6 +1019,25 @@ func getMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f }, nil } + if isAppleDeclarationUUID(req.ProfileUUID) { + // TODO: we could potentially combined with the other service methods + decl, err := svc.GetMDMAppleDeclaration(ctx, req.ProfileUUID) + if err != nil { + return &getMDMConfigProfileResponse{Err: err}, nil + } + + if downloadRequested { + return downloadFileResponse{ + content: decl.RawJSON, + contentType: "application/json", + filename: fmt.Sprintf("%s_%s.json", time.Now().Format("2006-01-02"), strings.ReplaceAll(decl.Name, " ", "_")), + }, nil + } + return &getMDMConfigProfileResponse{ + MDMConfigProfilePayload: fleet.NewMDMConfigProfilePayloadFromAppleDDM(decl), + }, nil + } + // Windows config profile cp, err := svc.GetMDMWindowsConfigProfile(ctx, req.ProfileUUID) if err != nil { @@ -1077,6 +1096,9 @@ func deleteMDMConfigProfileEndpoint(ctx context.Context, request interface{}, sv var err error if isAppleProfileUUID(req.ProfileUUID) { err = svc.DeleteMDMAppleConfigProfile(ctx, req.ProfileUUID) + } else if isAppleDeclarationUUID(req.ProfileUUID) { + // TODO: we could potentially combined with the other service methods + err = svc.DeleteMDMAppleDeclaration(ctx, req.ProfileUUID) } else { err = svc.DeleteMDMWindowsConfigProfile(ctx, req.ProfileUUID) } @@ -1157,6 +1179,10 @@ func isAppleProfileUUID(profileUUID string) bool { return strings.HasPrefix(profileUUID, "a") } +func isAppleDeclarationUUID(profileUUID string) bool { + return strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix) +} + //////////////////////////////////////////////////////////////////////////////// // POST /mdm/profiles (Create Apple or Windows MDM Config Profile) //////////////////////////////////////////////////////////////////////////////// @@ -1221,7 +1247,23 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f defer ff.Close() fileExt := filepath.Ext(req.Profile.Filename) - if isApple := strings.EqualFold(fileExt, ".mobileconfig"); isApple { + profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt) + isMobileConfig := strings.EqualFold(fileExt, ".mobileconfig") + isJSON := strings.EqualFold(fileExt, ".json") + if isMobileConfig || isJSON { + // Then it's an Apple configuration file + if isJSON { + decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, ff, req.Labels, profileName) + if err != nil { + return &newMDMConfigProfileResponse{Err: err}, nil + } + + return &newMDMConfigProfileResponse{ + ProfileUUID: decl.DeclarationUUID, + }, nil + + } + cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, ff, req.Labels) if err != nil { return &newMDMConfigProfileResponse{Err: err}, nil @@ -1232,7 +1274,6 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f } if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows { - profileName := strings.TrimSuffix(filepath.Base(req.Profile.Filename), fileExt) cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, ff, req.Labels) if err != nil { return &newMDMConfigProfileResponse{Err: err}, nil @@ -1254,7 +1295,7 @@ func (svc *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID u // this is required because we need authorize to return the error, and // svc.authz is only available on the concrete Service struct, not on the // Service interface so it cannot be done in the endpoint itself. - return &fleet.BadRequestError{Message: "Couldn't upload. The file should be a .mobileconfig or .xml file."} + return &fleet.BadRequestError{Message: "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file."} } func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, r io.Reader, labels []string) (*fleet.MDMWindowsConfigProfile, error) { @@ -1476,7 +1517,7 @@ func (svc *Service) BatchSetMDMProfiles( return ctxerr.Wrap(ctx, err, "validating labels") } - appleProfiles, err := getAppleProfiles(ctx, tmID, appCfg, profiles, labelMap) + appleProfiles, appleDecls, err := getAppleProfiles(ctx, tmID, appCfg, profiles, labelMap) if err != nil { return ctxerr.Wrap(ctx, err, "validating macOS profiles") } @@ -1490,7 +1531,7 @@ func (svc *Service) BatchSetMDMProfiles( return nil } - if err := svc.ds.BatchSetMDMProfiles(ctx, tmID, appleProfiles, windowsProfiles); err != nil { + if err := svc.ds.BatchSetMDMProfiles(ctx, tmID, appleProfiles, windowsProfiles, appleDecls); err != nil { return ctxerr.Wrap(ctx, err, "setting config profiles") } @@ -1576,17 +1617,94 @@ func getAppleProfiles( appCfg *fleet.AppConfig, profiles []fleet.MDMProfileBatchPayload, labelMap map[string]fleet.ConfigurationProfileLabel, -) ([]*fleet.MDMAppleConfigProfile, error) { +) ([]*fleet.MDMAppleConfigProfile, []*fleet.MDMAppleDeclaration, error) { // any duplicate identifier or name in the provided set results in an error profs := make([]*fleet.MDMAppleConfigProfile, 0, len(profiles)) - byName, byIdent := make(map[string]bool, len(profiles)), make(map[string]bool, len(profiles)) + decls := make([]*fleet.MDMAppleDeclaration, 0, len(profiles)) + // we need to keep track of the names and identifiers to check for duplicates so we will use + // a map where the key is the name oridentifier and the value is either "mobileconfig" or + // "declaration" to differentiate between the two types of profiles + byName, byIdent := make(map[string]string, len(profiles)), make(map[string]string, len(profiles)) for _, prof := range profiles { if mdm.GetRawProfilePlatform(prof.Contents) != "darwin" { continue } + + // Check for DDM files + + isJSON := func(b []byte) bool { + var js json.RawMessage + return json.Unmarshal(b, &js) == nil + } + + // TODO(roberto): As a mini optimization, GetRawDeclarationValues could replace isJSON. + if isJSON(prof.Contents) { + rawDecl, err := fleet.GetRawDeclarationValues(prof.Contents) + if err != nil { + return nil, nil, err + } + + if err := rawDecl.ValidateUserProvided(); err != nil { + return nil, nil, err + } + + mdmDecl := fleet.NewMDMAppleDeclaration(prof.Contents, tmID, prof.Name, rawDecl.Type, rawDecl.Identifier) + for _, labelName := range prof.Labels { + if lbl, ok := labelMap[labelName]; ok { + declLabel := fleet.ConfigurationProfileLabel{ + LabelName: lbl.LabelName, + LabelID: lbl.LabelID, + } + mdmDecl.Labels = append(mdmDecl.Labels, declLabel) + } + } + + v, ok := byName[mdmDecl.Name] + switch { + case !ok: + byName[mdmDecl.Name] = "declaration" + case v == "mobileconfig": + return nil, nil, ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(mdmDecl.Name, "A configuration profile with this name already exists."), + "duplicate mobileconfig profile by name") + case v == "declaration": + return nil, nil, ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(mdmDecl.Name, "A declaration profile with this name already exists."), + "duplicate declaration profile by name") + default: + // this should never happen but just in case + return nil, nil, ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(mdmDecl.Name, "A profile with this name already exists."), + "duplicate profile by name") + } + + v, ok = byIdent[mdmDecl.Identifier] + switch { + case !ok: + byIdent[mdmDecl.Identifier] = "declaration" + case v == "mobileconfig": + return nil, nil, ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(mdmDecl.Identifier, "A configuration profile with this identifier already exists."), + "duplicate mobileconfig profile by identifier") + case v == "declaration": + return nil, nil, ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(mdmDecl.Identifier, "A declaration profile with this identifier already exists."), + "duplicate declaration profile by identifier") + default: + // this should never happen but just in case + return nil, nil, ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(mdmDecl.Identifier, "A profile with this identifier already exists."), + "duplicate identifier by identifier") + } + + decls = append(decls, mdmDecl) + + continue + } + mdmProf, err := fleet.NewMDMAppleConfigProfile(prof.Contents, tmID) if err != nil { - return nil, ctxerr.Wrap(ctx, + return nil, nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError(prof.Name, err.Error()), "invalid mobileconfig profile") } @@ -1598,29 +1716,31 @@ func getAppleProfiles( } if err := mdmProf.ValidateUserProvided(); err != nil { - return nil, ctxerr.Wrap(ctx, + return nil, nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError(prof.Name, err.Error())) } if mdmProf.Name != prof.Name { - return nil, ctxerr.Wrap(ctx, + return nil, nil, ctxerr.Wrap(ctx, 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") } - if byName[mdmProf.Name] { - return nil, ctxerr.Wrap(ctx, + // TODO: confirm error messages + if _, ok := byName[mdmProf.Name]; ok { + return nil, nil, ctxerr.Wrap(ctx, 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") } - byName[mdmProf.Name] = true + byName[mdmProf.Name] = "mobileconfig" - if byIdent[mdmProf.Identifier] { - return nil, ctxerr.Wrap(ctx, + // TODO: confirm error messages + if _, ok := byIdent[mdmProf.Identifier]; ok { + return nil, nil, ctxerr.Wrap(ctx, 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") } - byIdent[mdmProf.Identifier] = true + byIdent[mdmProf.Identifier] = "mobileconfig" profs = append(profs, mdmProf) } @@ -1632,13 +1752,13 @@ func getAppleProfiles( // custom_settings key, we just return a success response in this // situation. if len(profs) == 0 { - return []*fleet.MDMAppleConfigProfile{}, nil + return []*fleet.MDMAppleConfigProfile{}, []*fleet.MDMAppleDeclaration{}, nil } - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", "cannot set custom settings: Fleet MDM is not configured")) + return nil, nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", "cannot set custom settings: Fleet MDM is not configured")) } - return profs, nil + return profs, decls, nil } func getWindowsProfiles( diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 04bcb4e8d2..472981e518 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -753,6 +753,8 @@ func TestGetMDMDiskEncryptionSummary(t *testing.T) { }) } +// TODO: Add tests for Apple DDM authz? + func TestMDMWindowsConfigProfileAuthz(t *testing.T) { ds := new(mock.Store) // while the config profiles are not premium-only, teams are and we want to test with teams. @@ -1093,7 +1095,7 @@ func TestMDMBatchSetProfiles(t *testing.T) { ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id, Name: "team"}, nil } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile) error { + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { return nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { @@ -1300,6 +1302,7 @@ func TestMDMBatchSetProfiles(t *testing.T) { {Name: "N1", Contents: mobileconfigForTest("N1", "I1")}, {Name: "N2", Contents: mobileconfigForTest("N2", "I2")}, {Name: "N3", Contents: mobileconfigForTest("N3", "I3")}, + {Name: "N4", Contents: declBytesForTest("D1", "d1content")}, }, ``, }, diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 0ceadc49aa..f6895dc4de 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -339,6 +339,10 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher), logger: kitlog.NewNopLogger(), }, + &MDMAppleDDMService{ + ds: ds, + logger: logger, + }, ) require.NoError(t, err) } diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md index 902b942aa2..27920c1819 100644 --- a/terraform/byo-vpc/README.md +++ b/terraform/byo-vpc/README.md @@ -33,7 +33,7 @@ No requirements. |------|-------------|------|---------|:--------:| | [alb\_config](#input\_alb\_config) | n/a |
object({
name = optional(string, "fleet")
subnets = list(string)
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
certificate_arn = string
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
})
| n/a | yes | | [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module |
object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
})
|
{
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
}
| no | -| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.47.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | +| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.47.3")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | | [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. |
object({
mem = number
cpu = number
})
|
{
"cpu": 1024,
"mem": 2048
}
| no | | [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module |
object({
name = optional(string, "fleet")
engine_version = optional(string, "8.0.mysql_aurora.3.02.2")
instance_class = optional(string, "db.t4g.large")
subnets = optional(list(string), [])
allowed_security_groups = optional(list(string), [])
allowed_cidr_blocks = optional(list(string), [])
apply_immediately = optional(bool, true)
monitoring_interval = optional(number, 10)
db_parameter_group_name = optional(string)
db_parameters = optional(map(string), {})
db_cluster_parameter_group_name = optional(string)
db_cluster_parameters = optional(map(string), {})
enabled_cloudwatch_logs_exports = optional(list(string), [])
master_username = optional(string, "fleet")
snapshot_identifier = optional(string)
cluster_tags = optional(map(string), {})
preferred_maintenance_window = optional(string, "thu:23:00-fri:00:00")
})
|
{
"allowed_cidr_blocks": [],
"allowed_security_groups": [],
"apply_immediately": true,
"cluster_tags": {},
"db_cluster_parameter_group_name": null,
"db_cluster_parameters": {},
"db_parameter_group_name": null,
"db_parameters": {},
"enabled_cloudwatch_logs_exports": [],
"engine_version": "8.0.mysql_aurora.3.02.2",
"instance_class": "db.t4g.large",
"master_username": "fleet",
"monitoring_interval": 10,
"name": "fleet",
"preferred_maintenance_window": "thu:23:00-fri:00:00",
"snapshot_identifier": null,
"subnets": []
}
| no | | [redis\_config](#input\_redis\_config) | n/a |
object({
name = optional(string, "fleet")
replication_group_id = optional(string)
elasticache_subnet_group_name = optional(string, "")
allowed_security_group_ids = optional(list(string), [])
subnets = list(string)
allowed_cidrs = list(string)
availability_zones = optional(list(string), [])
cluster_size = optional(number, 3)
instance_type = optional(string, "cache.m5.large")
apply_immediately = optional(bool, true)
automatic_failover_enabled = optional(bool, false)
engine_version = optional(string, "6.x")
family = optional(string, "redis6.x")
at_rest_encryption_enabled = optional(bool, true)
transit_encryption_enabled = optional(bool, true)
parameter = optional(list(object({
name = string
value = string
})), [])
log_delivery_configuration = optional(list(map(any)), [])
tags = optional(map(string), {})
})
|
{
"allowed_cidrs": null,
"allowed_security_group_ids": [],
"apply_immediately": true,
"at_rest_encryption_enabled": true,
"automatic_failover_enabled": false,
"availability_zones": [],
"cluster_size": 3,
"elasticache_subnet_group_name": "",
"engine_version": "6.x",
"family": "redis6.x",
"instance_type": "cache.m5.large",
"log_delivery_configuration": [],
"name": "fleet",
"parameter": [],
"replication_group_id": null,
"subnets": null,
"tags": {},
"transit_encryption_enabled": true
}
| no | diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 0fac721d46..367832f457 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -13,7 +13,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.47.2") + image = optional(string, "fleetdm/fleet:v4.47.3") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index 090ba32812..7c8ff1738f 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -74,7 +74,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.47.2") + image = optional(string, "fleetdm/fleet:v4.47.3") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 51c04fed72..daefdcbc54 100644 --- a/terraform/byo-vpc/example/main.tf +++ b/terraform/byo-vpc/example/main.tf @@ -17,7 +17,7 @@ provider "aws" { } locals { - fleet_image = "fleetdm/fleet:v4.47.2" + fleet_image = "fleetdm/fleet:v4.47.3" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index e886db4212..f74561183f 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -167,7 +167,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.47.2") + image = optional(string, "fleetdm/fleet:v4.47.3") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/example/main.tf b/terraform/example/main.tf index 9339221dcb..5a2fcf5491 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -59,8 +59,8 @@ module "fleet" { fleet_config = { # To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror - # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.47.2" - image = "fleetdm/fleet:v4.47.2" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.47.3" + image = "fleetdm/fleet:v4.47.3" # override default to deploy the image you desire # See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling # memory and cpu. autoscaling = { diff --git a/terraform/variables.tf b/terraform/variables.tf index b77887ddb2..e8b6e29b67 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -215,7 +215,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.47.2") + image = optional(string, "fleetdm/fleet:v4.47.3") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index af247c6058..19de34bf6c 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.47.2", + "version": "v4.47.3", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" diff --git a/website/config/routes.js b/website/config/routes.js index 4a69f469ae..6a486f1db6 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -511,6 +511,7 @@ module.exports.routes = { 'GET /learn-more-about/enrolling-hosts': '/docs/using-fleet/adding-hosts', 'GET /learn-more-about/setup-assistant': '/docs/using-fleet/mdm-macos-setup-experience#macos-setup-assistant', 'GET /learn-more-about/policy-automations': '/docs/using-fleet/automations', + 'GET /install-wine': 'https://github.com/fleetdm/fleet/blob/main/scripts/macos-install-wine.sh', // Sitemap // ============================================================================================================= diff --git a/website/views/pages/vulnerability-management.ejs b/website/views/pages/vulnerability-management.ejs index 27c0205659..6971d60fe4 100644 --- a/website/views/pages/vulnerability-management.ejs +++ b/website/views/pages/vulnerability-management.ejs @@ -115,9 +115,10 @@

Up-to-date data without scans

-

Traditional network vulnerability scans can clog networks. Fleet does things differently.

+

Traditional network vulnerability scans can clog your network and even haunt your printers with pages full of wingdings. Fleet does things differently.

-

Lightweight enough for the most brittle environments (OT, data centers, embedded/BTS, low-latency gaming servers).

+

Eliminate the risk of side effects from scanning the network.

+

Lightweight enough for the most brittle environments (OT, data centers, embedded/BTS, low-latency gaming servers).

Quickly pull data about important CVEs and zero days during an incident or audit.