package mysql import ( "bytes" "context" "crypto/md5" // nolint:gosec // used only to hash for efficient comparisons "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "errors" "fmt" "sort" "strings" "testing" "time" "github.com/VividCortex/mysqlerr" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql" "github.com/fleetdm/fleet/v4/server/datastore/s3" "github.com/fleetdm/fleet/v4/server/fleet" fleetmdm "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/go-sql-driver/mysql" "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMDMApple(t *testing.T) { ds := CreateMySQLDS(t) cases := []struct { name string fn func(t *testing.T, ds *Datastore) }{ {"TestNewMDMAppleConfigProfileDuplicateName", testNewMDMAppleConfigProfileDuplicateName}, {"TestNewMDMAppleConfigProfileLabels", testNewMDMAppleConfigProfileLabels}, {"TestNewMDMAppleConfigProfileDuplicateIdentifier", testNewMDMAppleConfigProfileDuplicateIdentifier}, {"TestDeleteMDMAppleConfigProfile", testDeleteMDMAppleConfigProfile}, {"TestDeleteMDMAppleConfigProfileWithPendingInstalls", testDeleteMDMAppleConfigProfileWithPendingInstalls}, {"TestDeleteMDMAppleConfigProfileByTeamAndIdentifier", testDeleteMDMAppleConfigProfileByTeamAndIdentifier}, {"TestListMDMAppleConfigProfiles", testListMDMAppleConfigProfiles}, {"TestHostDetailsMDMProfiles", testHostDetailsMDMProfiles}, {"TestHostDetailsMDMProfilesIOSIPadOS", testHostDetailsMDMProfilesIOSIPadOS}, {"TestBatchSetMDMAppleProfiles", testBatchSetMDMAppleProfiles}, {"TestMDMAppleProfileManagement", testMDMAppleProfileManagement}, {"TestMDMAppleProfileManagementBatch2", testMDMAppleProfileManagementBatch2}, {"TestMDMAppleProfileManagementBatch3", testMDMAppleProfileManagementBatch3}, {"TestGetMDMAppleProfilesContents", testGetMDMAppleProfilesContents}, {"TestAggregateMacOSSettingsStatusWithFileVault", testAggregateMacOSSettingsStatusWithFileVault}, {"TestMDMAppleHostsProfilesStatus", testMDMAppleHostsProfilesStatus}, {"TestMDMAppleIdPAccount", testMDMAppleIdPAccount}, {"TestIgnoreMDMClientError", testDoNotIgnoreMDMClientError}, {"TestDeleteMDMAppleProfilesForHost", testDeleteMDMAppleProfilesForHost}, {"TestGetMDMAppleCommandResults", testGetMDMAppleCommandResults}, {"TestBulkUpsertMDMAppleConfigProfiles", testBulkUpsertMDMAppleConfigProfile}, {"TestMDMAppleBootstrapPackageCRUD", testMDMAppleBootstrapPackageCRUD}, {"TestListMDMAppleCommands", testListMDMAppleCommands}, {"TestMDMAppleSetupAssistant", testMDMAppleSetupAssistant}, {"TestMDMAppleEnrollmentProfile", testMDMAppleEnrollmentProfile}, {"TestListMDMAppleSerials", testListMDMAppleSerials}, {"TestMDMAppleDefaultSetupAssistant", testMDMAppleDefaultSetupAssistant}, {"TestSetVerifiedMacOSProfiles", testSetVerifiedMacOSProfiles}, {"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash}, {"TestMDMAppleResetEnrollment", testMDMAppleResetEnrollment}, {"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments}, {"LockUnlockWipeMacOS", testLockUnlockWipeMacOS}, {"ScreenDEPAssignProfileSerialsForCooldown", testScreenDEPAssignProfileSerialsForCooldown}, {"MDMAppleDDMDeclarationsToken", testMDMAppleDDMDeclarationsToken}, {"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs}, {"SetOrUpdateMDMAppleDeclaration", testSetOrUpdateMDMAppleDDMDeclaration}, {"DEPAssignmentUpdates", testMDMAppleDEPAssignmentUpdates}, {"TestMDMConfigAsset", testMDMConfigAsset}, {"ListIOSAndIPadOSToRefetch", testListIOSAndIPadOSToRefetch}, {"MDMAppleUpsertHostIOSiPadOS", testMDMAppleUpsertHostIOSIPadOS}, {"IngestMDMAppleDevicesFromDEPSyncIOSIPadOS", testIngestMDMAppleDevicesFromDEPSyncIOSIPadOS}, {"MDMAppleProfilesOnIOSIPadOS", testMDMAppleProfilesOnIOSIPadOS}, {"GetEnrollmentIDsWithPendingMDMAppleCommands", testGetEnrollmentIDsWithPendingMDMAppleCommands}, {"MDMAppleBootstrapPackageWithS3", testMDMAppleBootstrapPackageWithS3}, {"GetAndUpdateABMToken", testMDMAppleGetAndUpdateABMToken}, {"ABMTokensTermsExpired", testMDMAppleABMTokensTermsExpired}, {"TestMDMGetABMTokenOrgNamesAssociatedWithTeam", testMDMGetABMTokenOrgNamesAssociatedWithTeam}, {"HostMDMCommands", testHostMDMCommands}, {"IngestMDMAppleDeviceFromOTAEnrollment", testIngestMDMAppleDeviceFromOTAEnrollment}, {"MDMManagedSCEPCertificates", testMDMManagedSCEPCertificates}, {"MDMManagedDigicertCertificates", testMDMManagedDigicertCertificates}, {"AppleMDMSetBatchAsyncLastSeenAt", testAppleMDMSetBatchAsyncLastSeenAt}, {"TestMDMAppleProfileLabels", testMDMAppleProfileLabels}, {"AggregateMacOSSettingsAllPlatforms", testAggregateMacOSSettingsAllPlatforms}, {"GetMDMAppleEnrolledDeviceDeletedFromFleet", testGetMDMAppleEnrolledDeviceDeletedFromFleet}, {"SetMDMAppleProfilesWithVariables", testSetMDMAppleProfilesWithVariables}, {"GetNanoMDMEnrollmentTimes", testGetNanoMDMEnrollmentTimes}, {"GetNanoMDMUserEnrollment", testGetNanoMDMUserEnrollment}, {"TestDeleteMDMAppleDeclarationWithPendingInstalls", testDeleteMDMAppleDeclarationWithPendingInstalls}, {"TestUpdateNanoMDMUserEnrollmentUsername", testUpdateNanoMDMUserEnrollmentUsername}, {"TestLockUnlockWipeIphone", testLockUnlockWipeIphone}, {"TestGetLatestAppleMDMCommandOfType", testGetLatestAppleMDMCommandOfType}, {"TestSetLockCommandForLostModeCheckin", testSetLockCommandForLostModeCheckin}, } for _, c := range cases { t.Helper() t.Run(c.name, func(t *testing.T) { defer TruncateTables(t, ds) c.fn(t, ds) }) } } func testNewMDMAppleConfigProfileDuplicateName(t *testing.T, ds *Datastore) { ctx := t.Context() // create a couple Apple profiles for no-team profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 0), nil) require.NoError(t, err) require.NotZero(t, profA.ProfileID) require.NotEmpty(t, profA.ProfileUUID) require.Equal(t, "a", string(profA.ProfileUUID[0])) profB, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 0), nil) require.NoError(t, err) require.NotZero(t, profB.ProfileID) require.NotEmpty(t, profB.ProfileUUID) require.Equal(t, "a", string(profB.ProfileUUID[0])) // create a Windows profile for no-team profC, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "c", TeamID: nil, SyncML: []byte("")}, nil) require.NoError(t, err) require.NotEmpty(t, profC.ProfileUUID) require.Equal(t, "w", string(profC.ProfileUUID[0])) // create the same name for team 1 as Apple profile profATm, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 1), nil) require.NoError(t, err) require.NotZero(t, profATm.ProfileID) require.NotEmpty(t, profATm.ProfileUUID) require.Equal(t, "a", string(profATm.ProfileUUID[0])) require.NotNil(t, profATm.TeamID) require.Equal(t, uint(1), *profATm.TeamID) // create the same B profile for team 1 as Windows profile profBTm, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: ptr.Uint(1), SyncML: []byte("")}, nil) require.NoError(t, err) require.NotEmpty(t, profBTm.ProfileUUID) var existsErr *existsError // create a duplicate of Apple for no-team _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 0), nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate of Windows for no-team _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("c", "c", 0), nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate of Apple for team _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 1), nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate of Windows for team _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 1), nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate name with a Windows profile for no-team _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: nil, SyncML: []byte("")}, nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate name with a Windows profile for team _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: ptr.Uint(1), SyncML: []byte("")}, nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) } func testNewMDMAppleConfigProfileLabels(t *testing.T, ds *Datastore) { ctx := t.Context() dummyMC := mobileconfig.Mobileconfig([]byte("DummyTestMobileconfigBytes")) cp := fleet.MDMAppleConfigProfile{ Name: "DummyTestName", Identifier: "DummyTestIdentifier", Mobileconfig: dummyMC, TeamID: nil, LabelsIncludeAll: []fleet.ConfigurationProfileLabel{ {LabelName: "foo", LabelID: 1}, }, } _, err := ds.NewMDMAppleConfigProfile(ctx, cp, nil) require.NotNil(t, err) require.True(t, fleet.IsForeignKey(err)) label := &fleet.Label{ Name: "my label", Description: "a label", Query: "select 1 from processes;", Platform: "darwin", } label, err = ds.NewLabel(ctx, label) require.NoError(t, err) cp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{ {LabelName: label.Name, LabelID: label.ID}, } prof, err := ds.NewMDMAppleConfigProfile(ctx, cp, nil) require.NoError(t, err) require.NotEmpty(t, prof.ProfileUUID) } func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore) { ctx := t.Context() initialCP := storeDummyConfigProfilesForTest(t, ds, 1)[0] // cannot create another profile with the same identifier if it is on the same team duplicateCP := fleet.MDMAppleConfigProfile{ Name: "DifferentNameDoesNotMatter", Identifier: initialCP.Identifier, TeamID: initialCP.TeamID, Mobileconfig: initialCP.Mobileconfig, } _, err := ds.NewMDMAppleConfigProfile(ctx, duplicateCP, nil) expectedErr := &existsError{ResourceType: "MDMAppleConfigProfile.PayloadIdentifier", Identifier: initialCP.Identifier, TeamID: initialCP.TeamID} require.ErrorContains(t, err, expectedErr.Error()) // can create another profile with the same name if it is on a different team duplicateCP.TeamID = ptr.Uint(*duplicateCP.TeamID + 1) newCP, err := ds.NewMDMAppleConfigProfile(ctx, duplicateCP, nil) require.NoError(t, err) checkConfigProfile(t, duplicateCP, *newCP) // get it back from both the deprecated ID and the uuid methods storedCP, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, newCP.ProfileID) require.NoError(t, err) checkConfigProfile(t, *newCP, *storedCP) require.Nil(t, storedCP.LabelsIncludeAll) storedCP, err = ds.GetMDMAppleConfigProfile(ctx, newCP.ProfileUUID) require.NoError(t, err) checkConfigProfile(t, *newCP, *storedCP) require.Nil(t, storedCP.LabelsIncludeAll) // create a label-based profile lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "lbl", Query: "select 1"}) require.NoError(t, err) labelCP := fleet.MDMAppleConfigProfile{ Name: "label-based", Identifier: "label-based", Mobileconfig: mobileconfig.Mobileconfig([]byte("LabelTestMobileconfigBytes")), LabelsIncludeAll: []fleet.ConfigurationProfileLabel{ {LabelName: lbl.Name, LabelID: lbl.ID}, }, } labelProf, err := ds.NewMDMAppleConfigProfile(ctx, labelCP, nil) require.NoError(t, err) // get it back from both the deprecated ID and the uuid methods, labels are // only included in the uuid one prof, err := ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, labelProf.ProfileID) require.NoError(t, err) require.Nil(t, prof.LabelsIncludeAll) prof, err = ds.GetMDMAppleConfigProfile(ctx, labelProf.ProfileUUID) require.NoError(t, err) require.Len(t, prof.LabelsIncludeAll, 1) require.Equal(t, lbl.Name, prof.LabelsIncludeAll[0].LabelName) require.False(t, prof.LabelsIncludeAll[0].Broken) // break the profile by deleting the label require.NoError(t, ds.DeleteLabel(ctx, lbl.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})) prof, err = ds.GetMDMAppleConfigProfile(ctx, labelProf.ProfileUUID) require.NoError(t, err) require.Len(t, prof.LabelsIncludeAll, 1) require.Equal(t, lbl.Name, prof.LabelsIncludeAll[0].LabelName) require.True(t, prof.LabelsIncludeAll[0].Broken) } func generateAppleCP(name string, identifier string, teamID uint) *fleet.MDMAppleConfigProfile { mc := mobileconfig.Mobileconfig([]byte(name + identifier)) return &fleet.MDMAppleConfigProfile{ Name: name, Identifier: identifier, TeamID: &teamID, Mobileconfig: mc, Scope: fleet.PayloadScopeSystem, } } func generateWindowsCP(name string, identifier string, teamID uint) *fleet.MDMWindowsConfigProfile { mc := syncml.ForTestWithData([]syncml.TestCommand{ { Verb: "Add", LocURI: "Test/Loc/URI", Data: (name + identifier), }, }) return &fleet.MDMWindowsConfigProfile{ Name: name, TeamID: &teamID, SyncML: mc, } } func testListMDMAppleConfigProfiles(t *testing.T, ds *Datastore) { ctx := t.Context() expectedTeam0 := []*fleet.MDMAppleConfigProfile{} expectedTeam1 := []*fleet.MDMAppleConfigProfile{} // add profile with team id zero (i.e. profile is not associated with any team) cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name0", "identifier0", 0), nil) require.NoError(t, err) expectedTeam0 = append(expectedTeam0, cp) cps, err := ds.ListMDMAppleConfigProfiles(ctx, nil) require.NoError(t, err) require.Len(t, cps, 1) checkConfigProfileWithChecksum(t, *expectedTeam0[0], *cps[0]) // add fleet-managed profiles for the team and globally for idf := range mobileconfig.FleetPayloadIdentifiers() { _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name_"+idf, idf, 1), nil) require.NoError(t, err) _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name_"+idf, idf, 0), nil) require.NoError(t, err) } // add profile with team id 1 cp, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name1", "identifier1", 1), nil) require.NoError(t, err) expectedTeam1 = append(expectedTeam1, cp) // list profiles for team id 1 cps, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(1)) require.NoError(t, err) require.Len(t, cps, 1) checkConfigProfileWithChecksum(t, *expectedTeam1[0], *cps[0]) // add another profile with team id 1 cp, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("another_name1", "another_identifier1", 1), nil) require.NoError(t, err) expectedTeam1 = append(expectedTeam1, cp) // list profiles for team id 1 cps, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(1)) require.NoError(t, err) require.Len(t, cps, 2) for _, cp := range cps { switch cp.Name { case "name1": checkConfigProfileWithChecksum(t, *expectedTeam1[0], *cp) case "another_name1": checkConfigProfileWithChecksum(t, *expectedTeam1[1], *cp) default: t.FailNow() } } // try to list profiles for non-existent team id cps, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(42)) require.NoError(t, err) require.Len(t, cps, 0) } func testDeleteMDMAppleConfigProfile(t *testing.T, ds *Datastore) { ctx := t.Context() // first via the deprecated ID initialCP := storeDummyConfigProfilesForTest(t, ds, 1)[0] err := ds.DeleteMDMAppleConfigProfileByDeprecatedID(ctx, initialCP.ProfileID) require.NoError(t, err) _, err = ds.GetMDMAppleConfigProfileByDeprecatedID(ctx, initialCP.ProfileID) require.ErrorIs(t, err, sql.ErrNoRows) err = ds.DeleteMDMAppleConfigProfileByDeprecatedID(ctx, initialCP.ProfileID) require.ErrorIs(t, err, sql.ErrNoRows) // next via the uuid initialCP = storeDummyConfigProfilesForTest(t, ds, 1)[0] err = ds.DeleteMDMAppleConfigProfile(ctx, initialCP.ProfileUUID) require.NoError(t, err) _, err = ds.GetMDMAppleConfigProfile(ctx, initialCP.ProfileUUID) require.ErrorIs(t, err, sql.ErrNoRows) err = ds.DeleteMDMAppleConfigProfile(ctx, initialCP.ProfileUUID) require.ErrorIs(t, err, sql.ErrNoRows) // delete by name via a non-existing name is not an error err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, "test") require.NoError(t, err) testDecl := declForTest("D1", "D1", "{}") dbDecl, err := ds.NewMDMAppleDeclaration(ctx, testDecl) require.NoError(t, err) // delete for a non-existing team does nothing err = ds.DeleteMDMAppleDeclarationByName(ctx, ptr.Uint(1), dbDecl.Name) require.NoError(t, err) // ddm still exists _, err = ds.GetMDMAppleDeclaration(ctx, dbDecl.DeclarationUUID) require.NoError(t, err) // properly delete err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, dbDecl.Name) require.NoError(t, err) _, err = ds.GetMDMAppleDeclaration(ctx, dbDecl.DeclarationUUID) require.ErrorIs(t, err, sql.ErrNoRows) } func testDeleteMDMAppleConfigProfileWithPendingInstalls(t *testing.T, ds *Datastore) { ctx := t.Context() var hosts []*fleet.Host var userEnrollmentIDs []string var deviceProfiles []*fleet.MDMAppleConfigProfile var userProfiles []*fleet.MDMAppleConfigProfile numHosts := 2 profiles := storeDummyConfigProfilesForTest(t, ds, numHosts*2) for i := 0; i < 2; i++ { h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now()) hosts = append(hosts, h) nanoEnroll(t, ds, h, true) userEnrollment, err := ds.GetNanoMDMUserEnrollment(ctx, h.UUID) require.NoError(t, err) require.NotNil(t, userEnrollment) require.Equal(t, h.UUID, userEnrollment.DeviceID) userEnrollmentIDs = append(userEnrollmentIDs, userEnrollment.ID) deviceProfiles = append(deviceProfiles, profiles[i*2]) userProfiles = append(userProfiles, profiles[(i*2)+1]) } ids, err := ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.Empty(t, ids) commander, _ := createMDMAppleCommanderAndStorage(t, ds) for i := 0; i < numHosts; i++ { // insert a device channel profile install and user channel profile install for each host uuid1 := uuid.New().String() rawCmd1 := createRawAppleCmd("InstallProfile", uuid1) err = commander.EnqueueCommand(ctx, []string{hosts[i].UUID}, rawCmd1) require.NoError(t, err) uuid2 := uuid.New().String() rawCmd2 := createRawAppleCmd("InstallProfile", uuid2) err = commander.EnqueueCommand(ctx, []string{userEnrollmentIDs[i]}, rawCmd2) require.NoError(t, err) err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { ProfileUUID: deviceProfiles[i].ProfileUUID, ProfileIdentifier: deviceProfiles[i].Identifier, ProfileName: deviceProfiles[i].Name, HostUUID: hosts[i].UUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: uuid1, Checksum: []byte("csum"), Scope: fleet.PayloadScopeSystem, }, { ProfileUUID: userProfiles[i].ProfileUUID, ProfileIdentifier: userProfiles[i].Identifier, ProfileName: userProfiles[i].Name, HostUUID: hosts[i].UUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: uuid2, Checksum: []byte("csum-user"), Scope: fleet.PayloadScopeUser, }, }, ) require.NoError(t, err) } ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[0].UUID, hosts[1].UUID, userEnrollmentIDs[0], userEnrollmentIDs[1]}, ids) err = ds.DeleteMDMAppleConfigProfile(ctx, deviceProfiles[0].ProfileUUID) require.NoError(t, err) _, err = ds.GetMDMAppleConfigProfile(ctx, deviceProfiles[0].ProfileUUID) require.ErrorIs(t, err, sql.ErrNoRows) // Device ID for host 0 should be no longer in the list ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[1].UUID, userEnrollmentIDs[0], userEnrollmentIDs[1]}, ids) err = ds.DeleteMDMAppleConfigProfile(ctx, userProfiles[0].ProfileUUID) require.NoError(t, err) _, err = ds.GetMDMAppleConfigProfile(ctx, userProfiles[0].ProfileUUID) require.ErrorIs(t, err, sql.ErrNoRows) // User enrollment ID for host 0 should be no longer in the list ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[1].UUID, userEnrollmentIDs[1]}, ids) } func testDeleteMDMAppleConfigProfileByTeamAndIdentifier(t *testing.T, ds *Datastore) { ctx := t.Context() initialCP := storeDummyConfigProfilesForTest(t, ds, 1)[0] err := ds.DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx, initialCP.TeamID, initialCP.Identifier) require.NoError(t, err) _, err = ds.GetMDMAppleConfigProfile(ctx, initialCP.ProfileUUID) require.ErrorIs(t, err, sql.ErrNoRows) err = ds.DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx, initialCP.TeamID, initialCP.Identifier) require.ErrorIs(t, err, sql.ErrNoRows) } func storeDummyConfigProfilesForTest(t *testing.T, ds *Datastore, howMany int) []*fleet.MDMAppleConfigProfile { storedCPs := make([]*fleet.MDMAppleConfigProfile, howMany) for i := range howMany { dummyMC := mobileconfig.Mobileconfig([]byte(fmt.Sprintf("DummyTestMobileconfigBytes-%d", i))) dummyCP := fleet.MDMAppleConfigProfile{ Name: fmt.Sprintf("DummyTestName-%d", i), Identifier: fmt.Sprintf("DummyTestIdentifier-%d", i), Mobileconfig: dummyMC, TeamID: nil, } ctx := t.Context() newCP, err := ds.NewMDMAppleConfigProfile(ctx, dummyCP, nil) require.NoError(t, err) checkConfigProfile(t, dummyCP, *newCP) storedCP, err := ds.GetMDMAppleConfigProfile(ctx, newCP.ProfileUUID) require.NoError(t, err) checkConfigProfile(t, *newCP, *storedCP) storedCPs[i] = storedCP } return storedCPs } func checkConfigProfile(t *testing.T, expected, actual fleet.MDMAppleConfigProfile) { require.Equal(t, expected.Name, actual.Name) require.Equal(t, expected.Identifier, actual.Identifier) require.Equal(t, expected.Mobileconfig, actual.Mobileconfig) if !expected.UploadedAt.IsZero() { require.True(t, expected.UploadedAt.Equal(actual.UploadedAt)) } } func checkConfigProfileWithChecksum(t *testing.T, expected, actual fleet.MDMAppleConfigProfile) { checkConfigProfile(t, expected, actual) require.ElementsMatch(t, md5.Sum(expected.Mobileconfig), actual.Checksum) // nolint:gosec // used only to hash for efficient comparisons } func testHostDetailsMDMProfiles(t *testing.T, ds *Datastore) { ctx := t.Context() p0, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name0", Identifier: "Identifier0", Mobileconfig: []byte("profile0-bytes"), Scope: fleet.PayloadScopeSystem}, nil) require.NoError(t, err) p1, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name1", Identifier: "Identifier1", Mobileconfig: []byte("profile1-bytes"), Scope: fleet.PayloadScopeSystem}, nil) require.NoError(t, err) p2, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name2", Identifier: "Identifier2", Mobileconfig: []byte("profile2-bytes"), Scope: fleet.PayloadScopeSystem}, nil) require.NoError(t, err) p3, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name3", Identifier: "Identifier3", Mobileconfig: []byte("profile3-bytes"), Scope: fleet.PayloadScopeUser}, nil) require.NoError(t, err) profiles, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) require.NoError(t, err) require.Len(t, profiles, 4) h0, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id"), NodeKey: ptr.String("host0-node-key"), UUID: "host0-test-mdm-profiles", Hostname: "hostname0", }) require.NoError(t, err) nanoEnroll(t, ds, h0, true) gotHost, err := ds.Host(ctx, h0.ID) require.NoError(t, err) require.Nil(t, gotHost.MDM.Profiles) gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, h0.UUID) require.NoError(t, err) require.Nil(t, gotProfs) h1, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host1-osquery-id"), NodeKey: ptr.String("host1-node-key"), UUID: "host1-test-mdm-profiles", Hostname: "hostname1", }) require.NoError(t, err) nanoEnroll(t, ds, h1, false) gotHost, err = ds.Host(ctx, h1.ID) require.NoError(t, err) require.Nil(t, gotHost.MDM.Profiles) gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h1.UUID) require.NoError(t, err) require.Nil(t, gotProfs) expectedProfiles0 := map[string]fleet.HostMDMAppleProfile{ p0.ProfileUUID: {HostUUID: h0.UUID, Name: p0.Name, ProfileUUID: p0.ProfileUUID, CommandUUID: "cmd0-uuid", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: "", Scope: fleet.PayloadScopeSystem}, p1.ProfileUUID: {HostUUID: h0.UUID, Name: p1.Name, ProfileUUID: p1.ProfileUUID, CommandUUID: "cmd1-uuid", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, Detail: "", Scope: fleet.PayloadScopeSystem}, p2.ProfileUUID: {HostUUID: h0.UUID, Name: p2.Name, ProfileUUID: p2.ProfileUUID, CommandUUID: "cmd2-uuid", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove, Detail: "Error removing profile", Scope: fleet.PayloadScopeSystem}, p3.ProfileUUID: {HostUUID: h0.UUID, Name: p3.Name, ProfileUUID: p3.ProfileUUID, CommandUUID: "cmd3-uuid", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: "", Scope: fleet.PayloadScopeUser, ManagedLocalAccount: nanoenroll_username}, } expectedProfiles1 := map[string]fleet.HostMDMAppleProfile{ p0.ProfileUUID: {HostUUID: h1.UUID, Name: p0.Name, ProfileUUID: p0.ProfileUUID, CommandUUID: "cmd0-uuid", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall, Detail: "Error installing profile", Scope: fleet.PayloadScopeSystem}, p1.ProfileUUID: {HostUUID: h1.UUID, Name: p1.Name, ProfileUUID: p1.ProfileUUID, CommandUUID: "cmd1-uuid", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, Detail: "", Scope: fleet.PayloadScopeSystem}, p2.ProfileUUID: {HostUUID: h1.UUID, Name: p2.Name, ProfileUUID: p2.ProfileUUID, CommandUUID: "cmd2-uuid", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove, Detail: "Error removing profile", Scope: fleet.PayloadScopeSystem}, p3.ProfileUUID: {HostUUID: h1.UUID, Name: p3.Name, ProfileUUID: p3.ProfileUUID, CommandUUID: "cmd3-uuid", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: "", Scope: fleet.PayloadScopeUser}, } // Add profile_identifier and checksum for each profile var args []interface{} i := 0 for _, p := range expectedProfiles0 { args = append(args, p.HostUUID, p.ProfileUUID, p.CommandUUID, *p.Status, p.OperationType, p.Detail, p.Name, "com.test.profile."+p.ProfileUUID, // profile_identifier test.MakeTestChecksum(byte(i)), // checksum (16 bytes) p.Scope, ) i++ } for _, p := range expectedProfiles1 { args = append(args, p.HostUUID, p.ProfileUUID, p.CommandUUID, *p.Status, p.OperationType, p.Detail, p.Name, "com.test.profile."+p.ProfileUUID, // profile_identifier test.MakeTestChecksum(byte(i)), // checksum (16 bytes) p.Scope, ) i++ } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` INSERT INTO host_mdm_apple_profiles ( host_uuid, profile_uuid, command_uuid, status, operation_type, detail, profile_name, profile_identifier, checksum, scope) VALUES (?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?,?) `, args..., ) if err != nil { return err } return nil }) gotHost, err = ds.Host(ctx, h0.ID) require.NoError(t, err) require.Nil(t, gotHost.MDM.Profiles) // ds.Host never returns MDM profiles gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h0.UUID) require.NoError(t, err) require.Len(t, gotProfs, 4) for _, gp := range gotProfs { ep, ok := expectedProfiles0[gp.ProfileUUID] require.True(t, ok) require.Equal(t, ep.Name, gp.Name) require.Equal(t, *ep.Status, *gp.Status) require.Equal(t, ep.OperationType, gp.OperationType) require.Equal(t, ep.Detail, gp.Detail) require.Equal(t, ep.Scope, gp.Scope) require.Equal(t, ep.ManagedLocalAccount, gp.ManagedLocalAccount) } gotHost, err = ds.Host(ctx, h1.ID) require.NoError(t, err) require.Nil(t, gotHost.MDM.Profiles) // ds.Host never returns MDM profiles gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h1.UUID) require.NoError(t, err) require.Len(t, gotProfs, 4) for _, gp := range gotProfs { ep, ok := expectedProfiles1[gp.ProfileUUID] require.True(t, ok) require.Equal(t, ep.Name, gp.Name) require.Equal(t, *ep.Status, *gp.Status) require.Equal(t, ep.OperationType, gp.OperationType) require.Equal(t, ep.Detail, gp.Detail) require.Equal(t, ep.Scope, gp.Scope) require.Equal(t, ep.ManagedLocalAccount, gp.ManagedLocalAccount) } // mark h1's install+failed profile as install+pending h1InstallFailed := expectedProfiles1[p0.ProfileUUID] err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ HostUUID: h1InstallFailed.HostUUID, CommandUUID: h1InstallFailed.CommandUUID, ProfileUUID: h1InstallFailed.ProfileUUID, Name: h1InstallFailed.Name, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: "", }) require.NoError(t, err) // mark h1's remove+failed profile as remove+verifying, deletes the host profile row h1RemoveFailed := expectedProfiles1[p2.ProfileUUID] err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ HostUUID: h1RemoveFailed.HostUUID, CommandUUID: h1RemoveFailed.CommandUUID, ProfileUUID: h1RemoveFailed.ProfileUUID, Name: h1RemoveFailed.Name, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeRemove, Detail: "", }) require.NoError(t, err) // The pending profile will be NOT be cleaned up because it was updated too recently. err = ds.CleanupHostMDMAppleProfiles(ctx) require.NoError(t, err) gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h1.UUID) require.NoError(t, err) require.Len(t, gotProfs, 3) // remove+verifying is not there anymore h1InstallPending := h1InstallFailed h1InstallPending.Status = &fleet.MDMDeliveryPending h1InstallPending.Detail = "" expectedProfiles1[p0.ProfileUUID] = h1InstallPending delete(expectedProfiles1, p2.ProfileUUID) for _, gp := range gotProfs { ep, ok := expectedProfiles1[gp.ProfileUUID] require.True(t, ok) require.Equal(t, ep.Name, gp.Name) require.Equal(t, *ep.Status, *gp.Status) require.Equal(t, ep.OperationType, gp.OperationType) require.Equal(t, ep.Detail, gp.Detail) require.Equal(t, ep.Scope, gp.Scope) require.Equal(t, ep.ManagedLocalAccount, gp.ManagedLocalAccount) } // Update the timestamps of the profiles ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET updated_at = updated_at - INTERVAL 2 HOUR`) return err }) // The pending profile will be cleaned up because we did not populate the corresponding nano table in this test. err = ds.CleanupHostMDMAppleProfiles(ctx) require.NoError(t, err) gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h1.UUID) require.NoError(t, err) require.Len(t, gotProfs, 1) assert.Equal(t, &fleet.MDMDeliveryVerifying, gotProfs[0].Status) } func TestIngestMDMAppleDevicesFromDEPSync(t *testing.T) { ds := CreateMySQLDS(t) ctx := t.Context() createBuiltinLabels(t, ds) for i := 0; i < 10; i++ { _, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("hostname_%d", i), DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now().Add(-time.Duration(i) * time.Minute), OsqueryHostID: ptr.String(fmt.Sprintf("osquery-host-id_%d", i)), NodeKey: ptr.String(fmt.Sprintf("node-key_%d", i)), UUID: fmt.Sprintf("uuid_%d", i), HardwareSerial: fmt.Sprintf("serial_%d", i), }) require.NoError(t, err) } hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 10) wantSerials := []string{} for _, h := range hosts { wantSerials = append(wantSerials, h.HardwareSerial) } // mock results incoming from depsync.Syncer depDevices := []godep.Device{ {SerialNumber: "abc", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // ingested; new serial, macOS, "added" op type {SerialNumber: "abc", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // not ingested; duplicate serial {SerialNumber: hosts[0].HardwareSerial, Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // not ingested; existing serial {SerialNumber: "ijk", Model: "MacBook Pro", OS: "", OpType: "added"}, // ingested; empty OS {SerialNumber: "tuv", Model: "MacBook Pro", OS: "OSX", OpType: "modified"}, // ingested; op type "modified", but new serial {SerialNumber: hosts[1].HardwareSerial, Model: "MacBook Pro", OS: "OSX", OpType: "modified"}, // not ingested; op type "modified", existing serial {SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "updated"}, // not ingested; op type "updated" {SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "deleted"}, // not ingested; op type "deleted" {SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // ingested; new serial, macOS, "added" op type } wantSerials = append(wantSerials, "abc", "xyz", "ijk", "tuv") encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.EqualValues(t, 4, n) // 4 new hosts ("abc", "xyz", "ijk", "tuv") hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, len(wantSerials)) gotSerials := []string{} for _, h := range hosts { gotSerials = append(gotSerials, h.HardwareSerial) if hs := h.HardwareSerial; hs == "abc" || hs == "xyz" { checkMDMHostRelatedTables(t, ds, h.ID, hs, "MacBook Pro") } } require.ElementsMatch(t, wantSerials, gotSerials) } func TestDEPSyncTeamAssignment(t *testing.T) { ds := CreateMySQLDS(t) ctx := t.Context() createBuiltinLabels(t, ds) depDevices := []godep.Device{ {SerialNumber: "abc", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, {SerialNumber: "def", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, } encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.Equal(t, int64(2), n) hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 2) for _, h := range hosts { require.Nil(t, h.TeamID) } // create a team team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"}) require.NoError(t, err) // assign the team as the default team for DEP devices ac, err := ds.AppConfig(ctx) require.NoError(t, err) ac.MDM.DeprecatedAppleBMDefaultTeam = team.Name err = ds.SaveAppConfig(ctx, ac) require.NoError(t, err) depDevices = []godep.Device{ {SerialNumber: "abc", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, {SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, } n, err = ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, team, team, team) require.NoError(t, err) require.Equal(t, int64(1), n) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 3) for _, h := range hosts { if h.HardwareSerial == "xyz" { require.EqualValues(t, team.ID, *h.TeamID) } else { require.Nil(t, h.TeamID) } } nonExistentTeam := &fleet.Team{ID: 8888} depDevices = []godep.Device{ {SerialNumber: "jqk", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, } n, err = ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, nonExistentTeam, nonExistentTeam, nonExistentTeam) require.NoError(t, err) require.EqualValues(t, n, 1) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 4) for _, h := range hosts { if h.HardwareSerial == "jqk" { require.Nil(t, h.TeamID) } } } func TestMDMEnrollment(t *testing.T) { ds := CreateMySQLDS(t) cases := []struct { name string fn func(t *testing.T, ds *Datastore) }{ {"TestHostAlreadyExistsInFleet", testIngestMDMAppleHostAlreadyExistsInFleet}, {"TestIngestAfterDEPSync", testIngestMDMAppleIngestAfterDEPSync}, {"TestBeforeDEPSync", testIngestMDMAppleCheckinBeforeDEPSync}, {"TestMultipleIngest", testIngestMDMAppleCheckinMultipleIngest}, {"TestCheckOut", testUpdateHostTablesOnMDMUnenroll}, {"TestNonDarwinHostAlreadyExistsInFleet", testIngestMDMNonDarwinHostAlreadyExistsInFleet}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { defer TruncateTables(t, ds) createBuiltinLabels(t, ds) c.fn(t, ds) }) } } func testIngestMDMAppleHostAlreadyExistsInFleet(t *testing.T, ds *Datastore) { ctx := t.Context() testSerial := "test-serial" testUUID := "test-uuid" host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host-name", DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: testUUID, HardwareSerial: testSerial, Platform: "darwin", }) require.NoError(t, err) err = ds.SetOrUpdateMDMData(ctx, host.ID, false, false, "https://fleetdm.com", true, fleet.WellKnownMDMFleet, "", false) require.NoError(t, err) hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, }, false) require.NoError(t, err) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) } func testIngestMDMNonDarwinHostAlreadyExistsInFleet(t *testing.T, ds *Datastore) { ctx := t.Context() testSerial := "test-serial" testUUID := "test-uuid" // this cannot happen for real, but it tests the host-matching logic in that // even if the host does match on serial number, it is not used as matching // host because it is not a macOS (darwin) platform host. host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host-name", DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: testUUID, HardwareSerial: testSerial, Platform: "linux", }) require.NoError(t, err) err = ds.SetOrUpdateMDMData(ctx, host.ID, false, false, "https://fleetdm.com", true, "Fleet MDM", "", false) require.NoError(t, err) hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, Platform: "darwin", }, false) require.NoError(t, err) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 2) // a new host was created with the provided uuid/serial and darwin as platform require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) require.Equal(t, testSerial, hosts[1].HardwareSerial) require.Equal(t, testUUID, hosts[1].UUID) id0, id1 := hosts[0].ID, hosts[1].ID platform0, platform1 := hosts[0].Platform, hosts[1].Platform require.NotEqual(t, id0, id1) require.NotEqual(t, platform0, platform1) require.ElementsMatch(t, []string{"darwin", "linux"}, []string{platform0, platform1}) } func testIngestMDMAppleIngestAfterDEPSync(t *testing.T, ds *Datastore) { ctx := t.Context() testSerial := "test-serial" testUUID := "test-uuid" testModel := "MacBook Pro" encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) // simulate a host that is first ingested via DEP (e.g., the device was added via Apple Business Manager) n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{ {SerialNumber: testSerial, Model: testModel, OS: "OSX", OpType: "added"}, }, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.Equal(t, int64(1), n) hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) // hosts that are first ingested via DEP will have a serial number but not a UUID because UUID // is not available from the DEP sync endpoint require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, "", hosts[0].UUID) checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, testModel) // now simulate the initial MDM checkin by that same host err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, }, false) require.NoError(t, err) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, testModel) } func testIngestMDMAppleCheckinBeforeDEPSync(t *testing.T, ds *Datastore) { ctx := t.Context() testSerial := "test-serial" testUUID := "test-uuid" testModel := "MacBook Pro" // ingest host on initial mdm checkin err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, HardwareModel: testModel, }, false) require.NoError(t, err) hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, testModel) encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) // no effect if same host appears in DEP sync n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{ {SerialNumber: testSerial, Model: testModel, OS: "OSX", OpType: "added"}, }, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.Equal(t, int64(0), n) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, testModel) } func testIngestMDMAppleCheckinMultipleIngest(t *testing.T, ds *Datastore) { ctx := t.Context() testSerial := "test-serial" testUUID := "test-uuid" err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, }, false) require.NoError(t, err) hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) // duplicate Authenticate request has no effect err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, }, false) require.NoError(t, err) hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1) require.Equal(t, testSerial, hosts[0].HardwareSerial) require.Equal(t, testUUID, hosts[0].UUID) } func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) { ctx := t.Context() testSerial := "test-serial" testUUID := "test-uuid" err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, Platform: "darwin", }, false) require.NoError(t, err) profiles := []*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "z"), } err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { ProfileUUID: profiles[0].ProfileUUID, ProfileIdentifier: profiles[0].Identifier, ProfileName: profiles[0].Name, HostUUID: testUUID, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte("csum"), Scope: fleet.PayloadScopeSystem, }, }, ) require.NoError(t, err) hostProfs, err := ds.GetHostMDMAppleProfiles(ctx, testUUID) require.NoError(t, err) require.Len(t, hostProfs, len(profiles)) var hostID uint err = sqlx.GetContext(ctx, ds.reader(ctx), &hostID, `SELECT id FROM hosts WHERE uuid = ?`, testUUID) require.NoError(t, err) _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, &fleet.Host{ID: hostID}, "asdf", "", nil) require.NoError(t, err) key, err := ds.GetHostDiskEncryptionKey(ctx, hostID) require.NoError(t, err) require.NotNil(t, key) // check that an entry in host_mdm exists var count int err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(*) FROM host_mdm WHERE host_id = (SELECT id FROM hosts WHERE uuid = ?)`, testUUID) require.NoError(t, err) require.Equal(t, 1, count) _, _, err = ds.MDMTurnOff(ctx, testUUID) require.NoError(t, err) err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(*) FROM host_mdm WHERE host_id = ?`, testUUID) require.NoError(t, err) require.Equal(t, 0, count) hostProfs, err = ds.GetHostMDMAppleProfiles(ctx, testUUID) require.NoError(t, err) require.Empty(t, hostProfs) key, err = ds.GetHostDiskEncryptionKey(ctx, hostID) require.NoError(t, err) require.NotNil(t, key) } func expectAppleProfiles( t *testing.T, ds *Datastore, tmID *uint, want []*fleet.MDMAppleConfigProfile, ) map[string]string { if tmID == nil { tmID = ptr.Uint(0) } // don't use ds.ListMDMAppleConfigProfiles as it leaves out // fleet-managed profiles. var got []*fleet.MDMAppleConfigProfile ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { ctx := t.Context() return sqlx.SelectContext(ctx, q, &got, `SELECT * FROM mdm_apple_configuration_profiles WHERE team_id = ?`, tmID) }) // create map of expected profiles keyed by identifier wantMap := make(map[string]*fleet.MDMAppleConfigProfile, len(want)) for _, cp := range want { wantMap[cp.Identifier] = cp } // compare only the fields we care about, and build the resulting map of // profile identifier as key to profile UUID as value m := make(map[string]string) for _, gotp := range got { m[gotp.Identifier] = gotp.ProfileUUID if gotp.TeamID != nil && *gotp.TeamID == 0 { gotp.TeamID = nil } // ProfileID is non-zero (auto-increment), but otherwise we don't care // about it for test assertions. require.NotZero(t, gotp.ProfileID) gotp.ProfileID = 0 // ProfileUUID is non-empty and starts with "a", but otherwise we don't // care about it for test assertions. require.NotEmpty(t, gotp.ProfileUUID) require.True(t, strings.HasPrefix(gotp.ProfileUUID, "a")) gotp.ProfileUUID = "" gotp.CreatedAt = time.Time{} gotp.SecretsUpdatedAt = nil // if an expected uploaded_at timestamp is provided for this profile, keep // its value, otherwise clear it as we don't care about asserting its // value. if wantp := wantMap[gotp.Identifier]; wantp == nil || wantp.UploadedAt.IsZero() { gotp.UploadedAt = time.Time{} } } // order is not guaranteed require.ElementsMatch(t, want, got) return m } func expectAppleDeclarations( t *testing.T, ds *Datastore, tmID *uint, want []*fleet.MDMAppleDeclaration, ) map[string]string { if tmID == nil { tmID = ptr.Uint(0) } ctx := t.Context() var gotUUIDs []string ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.SelectContext(ctx, q, &gotUUIDs, `SELECT declaration_uuid FROM mdm_apple_declarations WHERE team_id = ?`, tmID) }) // load each declaration, this will also load its labels var got []*fleet.MDMAppleDeclaration for _, declUUID := range gotUUIDs { decl, err := ds.GetMDMAppleDeclaration(ctx, declUUID) require.NoError(t, err) got = append(got, decl) } // 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) } jsonMustMarshal := func(v any) string { b, err := json.Marshal(v) require.NoError(t, err) return string(b) } // 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.Token = "" // 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.UploadedAt.IsZero() { gotD.UploadedAt = time.Time{} } require.Equal(t, wantD.Name, gotD.Name) require.Equal(t, wantD.Identifier, gotD.Identifier) // for labels, only care about ID and Name (the exclude, require all // fields, etc. are reflected by the field that contains the label) require.Equal(t, jsonMustMarshal(wantD.LabelsIncludeAll), jsonMustMarshal(gotD.LabelsIncludeAll)) require.Equal(t, jsonMustMarshal(wantD.LabelsIncludeAny), jsonMustMarshal(gotD.LabelsIncludeAny)) require.Equal(t, jsonMustMarshal(wantD.LabelsExcludeAny), jsonMustMarshal(gotD.LabelsExcludeAny)) } return m } func testBatchSetMDMAppleProfiles(t *testing.T, ds *Datastore) { ctx := t.Context() applyAndExpect := func(newSet []*fleet.MDMAppleConfigProfile, tmID *uint, want []*fleet.MDMAppleConfigProfile) map[string]string { err := ds.BatchSetMDMAppleProfiles(ctx, tmID, newSet) require.NoError(t, err) return expectAppleProfiles(t, ds, tmID, want) } getProfileByTeamAndIdentifier := func(tmID *uint, identifier string) *fleet.MDMAppleConfigProfile { var prof fleet.MDMAppleConfigProfile var teamID uint if tmID != nil { teamID = *tmID } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &prof, `SELECT * FROM mdm_apple_configuration_profiles WHERE team_id = ? AND identifier = ?`, teamID, identifier) }) return &prof } withTeamID := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile { p.TeamID = &tmID return p } withUploadedAt := func(p *fleet.MDMAppleConfigProfile, ua time.Time) *fleet.MDMAppleConfigProfile { p.UploadedAt = ua return p } // apply empty set for no-team applyAndExpect(nil, nil, nil) // apply single profile set for tm1 mTm1 := applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "a"), }, ptr.Uint(1), []*fleet.MDMAppleConfigProfile{ withTeamID(configProfileForTest(t, "N1", "I1", "a"), 1), }) profTm1I1 := getProfileByTeamAndIdentifier(ptr.Uint(1), "I1") // apply single profile set for no-team mNoTm := applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "b"), }, nil, []*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "b"), }) profNoTmI1 := getProfileByTeamAndIdentifier(nil, "I1") // wait a second to ensure timestamps in the DB change time.Sleep(time.Second) // apply new profile set for tm1 mTm1b := applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "a"), // unchanged configProfileForTest(t, "N2", "I2", "b"), }, ptr.Uint(1), []*fleet.MDMAppleConfigProfile{ withUploadedAt(withTeamID(configProfileForTest(t, "N1", "I1", "a"), 1), profTm1I1.UploadedAt), withTeamID(configProfileForTest(t, "N2", "I2", "b"), 1), }) // identifier for N1-I1 is unchanged require.Equal(t, mTm1["I1"], mTm1b["I1"]) profTm1I2 := getProfileByTeamAndIdentifier(ptr.Uint(1), "I2") // apply edited (by name only) profile set for no-team mNoTmb := applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N2", "I1", "b"), }, nil, []*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N2", "I1", "b"), // name change implies uploaded_at change }) require.Equal(t, mNoTm["I1"], mNoTmb["I1"]) profNoTmI1b := getProfileByTeamAndIdentifier(nil, "I1") require.False(t, profNoTmI1.UploadedAt.Equal(profNoTmI1b.UploadedAt)) // wait a second to ensure timestamps in the DB change time.Sleep(time.Second) // apply edited profile (by content only), unchanged profile and new profile // for tm1 mTm1c := applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "z"), // content updated configProfileForTest(t, "N2", "I2", "b"), // unchanged configProfileForTest(t, "N3", "I3", "c"), // new }, ptr.Uint(1), []*fleet.MDMAppleConfigProfile{ withTeamID(configProfileForTest(t, "N1", "I1", "z"), 1), withUploadedAt(withTeamID(configProfileForTest(t, "N2", "I2", "b"), 1), profTm1I2.UploadedAt), withTeamID(configProfileForTest(t, "N3", "I3", "c"), 1), }) // identifier for N1-I1 is unchanged require.Equal(t, mTm1b["I1"], mTm1c["I1"]) // identifier for N2-I2 is unchanged require.Equal(t, mTm1b["I2"], mTm1c["I2"]) profTm1I1c := getProfileByTeamAndIdentifier(ptr.Uint(1), "I1") // uploaded-at was modified because the content changed require.False(t, profTm1I1.UploadedAt.Equal(profTm1I1c.UploadedAt)) // apply only new profiles to no-team applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N4", "I4", "d"), configProfileForTest(t, "N5", "I5", "e"), }, nil, []*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N4", "I4", "d"), configProfileForTest(t, "N5", "I5", "e"), }) // clear profiles for tm1 applyAndExpect(nil, ptr.Uint(1), nil) // simulate profiles being added by fleet fleetProfiles := []*fleet.MDMAppleConfigProfile{} expectFleetProfiles := []*fleet.MDMAppleConfigProfile{} for fp := range mobileconfig.FleetPayloadIdentifiers() { fleetProfiles = append(fleetProfiles, configProfileForTest(t, fp, fp, fp)) expectFleetProfiles = append(expectFleetProfiles, withTeamID(configProfileForTest(t, fp, fp, fp), 1)) } applyAndExpect(fleetProfiles, nil, fleetProfiles) applyAndExpect(fleetProfiles, ptr.Uint(1), expectFleetProfiles) // add no-team profiles applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "b"), }, nil, append([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "b"), }, fleetProfiles...)) // add team profiles applyAndExpect([]*fleet.MDMAppleConfigProfile{ configProfileForTest(t, "N1", "I1", "a"), configProfileForTest(t, "N2", "I2", "b"), }, ptr.Uint(1), append([]*fleet.MDMAppleConfigProfile{ withTeamID(configProfileForTest(t, "N1", "I1", "a"), 1), withTeamID(configProfileForTest(t, "N2", "I2", "b"), 1), }, expectFleetProfiles...)) // cleaning profiles still leaves the profile managed by Fleet applyAndExpect(nil, nil, fleetProfiles) applyAndExpect(nil, ptr.Uint(1), expectFleetProfiles) } func configProfileBytesForTest(name, identifier, uuid string) []byte { return []byte(fmt.Sprintf(` PayloadContent PayloadDisplayName %s PayloadIdentifier %s PayloadType Configuration PayloadUUID %s PayloadVersion 1 `, name, identifier, uuid)) } // If the label name starts with "exclude-", the label is considered an "exclude-any". If it starts // with "include-any", it is considered an "include-any". Otherwise it is an "include-all". func configProfileForTest(t *testing.T, name, identifier, uuid string, labels ...*fleet.Label) *fleet.MDMAppleConfigProfile { return scopedConfigProfileForTest(t, name, identifier, uuid, fleet.PayloadScopeSystem, labels...) } func scopedConfigProfileForTest(t *testing.T, name, identifier, uuid string, scope fleet.PayloadScope, labels ...*fleet.Label) *fleet.MDMAppleConfigProfile { prof := configProfileBytesForTest(name, identifier, uuid) cp, err := fleet.NewMDMAppleConfigProfile(prof, nil) cp.Identifier = identifier cp.Name = name cp.Scope = scope require.NoError(t, err) sum := md5.Sum(prof) // nolint:gosec // used only to hash for efficient comparisons cp.Checksum = sum[:] for _, lbl := range labels { switch { case strings.HasPrefix(lbl.Name, "exclude-"): cp.LabelsExcludeAny = append(cp.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) case strings.HasPrefix(lbl.Name, "include-any-"): cp.LabelsIncludeAny = append(cp.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) default: cp.LabelsIncludeAll = append(cp.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) } } return cp } // if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise // it is an "include-all". 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 { switch { case strings.HasPrefix(l.Name, "exclude-"): decl.LabelsExcludeAny = append(decl.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID}) case strings.HasPrefix(l.Name, "inclany-"): decl.LabelsIncludeAny = append(decl.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID}) default: decl.LabelsIncludeAll = append(decl.LabelsIncludeAll, 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) require.NoError(t, err) sum := md5.Sum(prof) // nolint:gosec // used only to hash for efficient comparisons cp.Checksum = sum[:] return cp } func testMDMAppleProfileManagementBatch2(t *testing.T, ds *Datastore) { ds.testSelectMDMProfilesBatchSize = 2 ds.testUpsertMDMDesiredProfilesBatchSize = 2 t.Cleanup(func() { ds.testSelectMDMProfilesBatchSize = 0 ds.testUpsertMDMDesiredProfilesBatchSize = 0 }) testMDMAppleProfileManagement(t, ds) } func testMDMAppleProfileManagementBatch3(t *testing.T, ds *Datastore) { ds.testSelectMDMProfilesBatchSize = 3 ds.testUpsertMDMDesiredProfilesBatchSize = 3 t.Cleanup(func() { ds.testSelectMDMProfilesBatchSize = 0 ds.testUpsertMDMDesiredProfilesBatchSize = 0 }) testMDMAppleProfileManagement(t, ds) } func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { ctx := t.Context() matchProfiles := func(want, got []*fleet.MDMAppleProfilePayload) { // match only the fields we care about for _, p := range got { require.NotEmpty(t, p.Checksum) p.Checksum = nil p.SecretsUpdatedAt = nil p.DeviceEnrolledAt = nil } require.ElementsMatch(t, want, got) } // Helper function to ensure the "combined" ToInstallAndRemove function matches the output // of the individual functions matchCombinedProfiles := func(toInstall, toRemove []*fleet.MDMAppleProfilePayload) { combinedToInstall, combinedToRemove, err := ds.ListMDMAppleProfilesToInstallAndRemove(ctx) require.NoError(t, err) matchProfiles(toInstall, combinedToInstall) matchProfiles(toRemove, combinedToRemove) } globalProfiles := []*fleet.MDMAppleConfigProfile{ scopedConfigProfileForTest(t, "N1", "I1", "z", fleet.PayloadScopeSystem), scopedConfigProfileForTest(t, "N2", "I2", "b", fleet.PayloadScopeSystem), scopedConfigProfileForTest(t, "N3", "I3", "c", fleet.PayloadScopeUser), } err := ds.BatchSetMDMAppleProfiles(ctx, nil, globalProfiles) require.NoError(t, err) globalPfs, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) require.NoError(t, err) require.Len(t, globalPfs, len(globalProfiles)) _, err = ds.writer(ctx).Exec(` INSERT INTO nano_commands (command_uuid, request_type, command) VALUES ('command-uuid', 'foo', ' %s %s` cases := []struct { prefix, content, suffix string }{ {"", "", ""}, {" ", "", ""}, {"", "", " "}, {"\t\n ", "", "\t\n "}, {"", ` PayloadVersion 1 PayloadUUID Ignored PayloadType Configuration PayloadIdentifier Ignored `, ""}, {" ", ` PayloadVersion 1 PayloadUUID Ignored PayloadType Configuration PayloadIdentifier Ignored `, "\r\n"}, } for i, c := range cases { t.Run(fmt.Sprintf("%q %q %q", c.prefix, c.content, c.suffix), func(t *testing.T) { mc := mobileconfig.Mobileconfig(fmt.Sprintf(base, c.prefix, c.content, c.suffix)) prof, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{ Name: fmt.Sprintf("profile-%d", i), Identifier: fmt.Sprintf("profile-%d", i), TeamID: nil, Mobileconfig: mc, }, nil) require.NoError(t, err) t.Cleanup(func() { err := ds.DeleteMDMAppleConfigProfile(ctx, prof.ProfileUUID) require.NoError(t, err) }) goProf := fleet.MDMApplePreassignProfilePayload{Profile: mc} goHash := goProf.HexMD5Hash() require.NotEmpty(t, goHash) var uid string ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE checksum = UNHEX(?)`, goHash) }) require.Equal(t, prof.ProfileUUID, uid) }) } } func testMDMAppleResetEnrollment(t *testing.T, ds *Datastore) { ctx := t.Context() host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-uuid-1", TeamID: nil, Platform: "darwin", }) require.NoError(t, err) // try with a host that doesn't have a matching entry // in nano_enrollments err = ds.MDMResetEnrollment(ctx, host.UUID, false) require.NoError(t, err) // add a matching entry in the nano table nanoEnroll(t, ds, host, false) enrollment, err := ds.GetNanoMDMEnrollment(ctx, host.UUID) require.NoError(t, err) require.Equal(t, enrollment.TokenUpdateTally, 1) // add configuration profiles cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name0", "identifier0", 0), nil) require.NoError(t, err) upsertHostCPs([]*fleet.Host{host}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerified, ctx, ds, t) gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, host.UUID) require.NoError(t, err) require.Len(t, gotProfs, 1) // add a record of the bootstrap package being installed _, err = ds.writer(ctx).Exec(` INSERT INTO nano_commands (command_uuid, request_type, command) VALUES ('command-uuid', 'foo', ' Command ManagedOnly RequestType %s CommandUUID %s `, reqType, cmdUUID) } func testMDMConfigAsset(t *testing.T, ds *Datastore) { ctx := t.Context() assets := []fleet.MDMConfigAsset{ { Name: fleet.MDMAssetCACert, Value: []byte("a"), }, { Name: fleet.MDMAssetCAKey, Value: []byte("b"), }, } wantAssets := map[fleet.MDMAssetName]fleet.MDMConfigAsset{} for _, a := range assets { wantAssets[a.Name] = a } err := ds.InsertMDMConfigAssets(ctx, assets, nil) require.NoError(t, err) a, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) require.NoError(t, err) require.Equal(t, wantAssets, a) h, err := ds.GetAllMDMConfigAssetsHashes(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) require.NoError(t, err) require.Len(t, h, 2) require.NotEmpty(t, h[fleet.MDMAssetCACert]) require.NotEmpty(t, h[fleet.MDMAssetCAKey]) // try to fetch an asset that doesn't exist var nfe fleet.NotFoundError a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetABMCert}, ds.writer(ctx)) require.ErrorAs(t, err, &nfe) require.Nil(t, a) h, err = ds.GetAllMDMConfigAssetsHashes(ctx, []fleet.MDMAssetName{fleet.MDMAssetABMCert}) require.ErrorAs(t, err, &nfe) require.Nil(t, h) // try to fetch a mix of assets that exist and doesn't exist a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetABMCert}, nil) require.ErrorIs(t, err, ErrPartialResult) require.Len(t, a, 1) h, err = ds.GetAllMDMConfigAssetsHashes(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetABMCert}) require.ErrorIs(t, err, ErrPartialResult) require.Len(t, h, 1) require.NotEmpty(t, h[fleet.MDMAssetCACert]) // Replace the assets newAssets := []fleet.MDMConfigAsset{ { Name: fleet.MDMAssetCACert, Value: []byte("c"), }, { Name: fleet.MDMAssetCAKey, Value: []byte("d"), }, } wantNewAssets := map[fleet.MDMAssetName]fleet.MDMConfigAsset{} for _, a := range newAssets { wantNewAssets[a.Name] = a } err = ds.ReplaceMDMConfigAssets(ctx, newAssets, nil) require.NoError(t, err) a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, ds.reader(ctx)) require.NoError(t, err) require.Equal(t, wantNewAssets, a) h, err = ds.GetAllMDMConfigAssetsHashes(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) require.NoError(t, err) require.Len(t, h, 2) require.NotEmpty(t, h[fleet.MDMAssetCACert]) require.NotEmpty(t, h[fleet.MDMAssetCAKey]) // Soft delete the assets err = ds.DeleteMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) require.NoError(t, err) a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) require.ErrorAs(t, err, &nfe) require.Nil(t, a) h, err = ds.GetAllMDMConfigAssetsHashes(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) require.ErrorAs(t, err, &nfe) require.Nil(t, h) // Verify that they're still in the DB. Values should be encrypted. type assetRow struct { Name string `db:"name"` Value []byte `db:"value"` DeletionUUID string `db:"deletion_uuid"` DeletedAt time.Time `db:"deleted_at"` } var ar []assetRow err = sqlx.SelectContext(ctx, ds.reader(ctx), &ar, "SELECT name, value, deletion_uuid, deleted_at FROM mdm_config_assets") require.NoError(t, err) require.Len(t, ar, 4) expected := make(map[string]fleet.MDMConfigAsset) for _, a := range append(assets, newAssets...) { expected[string(a.Value)] = a } for _, got := range ar { d, err := decrypt(got.Value, ds.serverPrivateKey) require.NoError(t, err) require.Equal(t, expected[string(d)].Name, fleet.MDMAssetName(got.Name)) require.NotEmpty(t, got.Value) require.Equal(t, expected[string(d)].Value, d) require.NotEmpty(t, got.DeletionUUID) require.NotEmpty(t, got.DeletedAt) } // Hard delete err = ds.HardDeleteMDMConfigAsset(ctx, fleet.MDMAssetCACert) require.NoError(t, err) a, err = ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) require.ErrorAs(t, err, &nfe) require.Nil(t, a) var result bool err = sqlx.GetContext(ctx, ds.reader(ctx), &result, "SELECT 1 FROM mdm_config_assets WHERE name = ?", fleet.MDMAssetCACert) assert.ErrorIs(t, err, sql.ErrNoRows) // other (non-hard deleted asset still present) err = sqlx.GetContext(ctx, ds.reader(ctx), &result, "SELECT 1 FROM mdm_config_assets WHERE name = ?", fleet.MDMAssetCAKey) assert.NoError(t, err) assert.True(t, result) } func testListIOSAndIPadOSToRefetch(t *testing.T, ds *Datastore) { ctx := t.Context() refetchInterval := 1 * time.Hour hostCount := 0 newHost := func(platform string) *fleet.Host { h, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("foobar%d", hostCount), OsqueryHostID: ptr.String(fmt.Sprintf("foobar-%d", hostCount)), NodeKey: ptr.String(fmt.Sprintf("foobar-%d", hostCount)), UUID: fmt.Sprintf("foobar-%d", hostCount), Platform: platform, HardwareSerial: fmt.Sprintf("foobar-%d", hostCount), }) require.NoError(t, err) hostCount++ return h } encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) // Test with no hosts. devices, err := ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Empty(t, devices) // Create a placeholder macOS host. _ = newHost("darwin") // Mock results incoming from depsync.Syncer depDevices := []godep.Device{ {SerialNumber: "iOS0_SERIAL", DeviceFamily: "iPhone", OpType: "added"}, {SerialNumber: "iPadOS0_SERIAL", DeviceFamily: "iPad", OpType: "added"}, } n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.Equal(t, int64(2), n) // Hosts are not enrolled yet (e.g. DEP enrolled) devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Empty(t, devices) // Now simulate the initial MDM checkin of the devices. err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: "iOS0_UUID", HardwareSerial: "iOS0_SERIAL", HardwareModel: "iPhone14,6", Platform: "ios", OsqueryHostID: ptr.String("iOS0_OSQUERY_HOST_ID"), }, false) require.NoError(t, err) iOS0, err := ds.HostByIdentifier(ctx, "iOS0_SERIAL") require.NoError(t, err) nanoEnroll(t, ds, iOS0, false) err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: "iPadOS0_UUID", HardwareSerial: "iPadOS0_SERIAL", HardwareModel: "iPad13,18", Platform: "ipados", OsqueryHostID: ptr.String("iPadOS0_OSQUERY_HOST_ID"), }, false) require.NoError(t, err) iPadOS0, err := ds.HostByIdentifier(ctx, "iPadOS0_SERIAL") require.NoError(t, err) nanoEnroll(t, ds, iPadOS0, false) // Test with hosts but empty state in nanomdm command tables. devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Len(t, devices, 2) uuids := []string{devices[0].UUID, devices[1].UUID} sort.Slice(uuids, func(i, j int) bool { return uuids[i] < uuids[j] }) assert.Equal(t, uuids, []string{"iOS0_UUID", "iPadOS0_UUID"}) assert.Empty(t, devices[0].CommandsAlreadySent) assert.Empty(t, devices[1].CommandsAlreadySent) // Set iOS detail_updated_at as 30 minutes in the past. ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 30 MINUTE) WHERE id = ?`, iOS0.ID) return err }) // iOS device should not be returned because it was refetched recently devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Len(t, devices, 1) require.Equal(t, devices[0].UUID, "iPadOS0_UUID") // Set iPadOS detail_updated_at as 30 minutes in the past. ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 30 MINUTE) WHERE id = ?`, iPadOS0.ID) return err }) // Both devices are up-to-date thus none should be returned. devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Empty(t, devices) // Set iOS detail_updated_at as 2 hours in the past. ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, iOS0.ID) return err }) // iOS device be returned because it is out of date. devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Len(t, devices, 1) require.Equal(t, devices[0].UUID, "iOS0_UUID") assert.Empty(t, devices[0].CommandsAlreadySent) // Update commands already sent to the devices and check that they are returned. require.NoError(t, ds.AddHostMDMCommands(ctx, []fleet.HostMDMCommand{{ HostID: iOS0.ID, CommandType: fleet.RefetchAppsCommandUUIDPrefix, }})) devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Len(t, devices, 1) require.Equal(t, devices[0].UUID, "iOS0_UUID") require.Len(t, devices[0].CommandsAlreadySent, 1) assert.Equal(t, fleet.RefetchAppsCommandUUIDPrefix, devices[0].CommandsAlreadySent[0]) // set iOS device to not be enabled in fleet MDM. No devices should be returned. ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE nano_enrollments SET enabled = 0 WHERE id = ?`, iOS0.UUID) return err }) devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) require.NoError(t, err) require.Empty(t, devices) } func testMDMAppleUpsertHostIOSIPadOS(t *testing.T, ds *Datastore) { ctx := t.Context() createBuiltinLabels(t, ds) for i, platform := range []string{"ios", "ipados"} { // Upsert first to test insertMDMAppleHostDB. err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: fmt.Sprintf("test-uuid-%d", i), HardwareSerial: fmt.Sprintf("test-serial-%d", i), HardwareModel: "test-hw-model", Platform: platform, }, false) require.NoError(t, err) h, err := ds.HostByIdentifier(ctx, fmt.Sprintf("test-uuid-%d", i)) require.NoError(t, err) require.Equal(t, false, h.RefetchRequested) require.Less(t, time.Since(h.LastEnrolledAt), 1*time.Hour) // check it's not in the date in the 2000 we use as "Never". require.Equal(t, "test-hw-model", h.HardwareModel) labels, err := ds.ListLabelsForHost(ctx, h.ID) require.NoError(t, err) require.Len(t, labels, 2) sort.Slice(labels, func(i, j int) bool { return labels[i].ID < labels[j].ID }) require.Equal(t, "All Hosts", labels[0].Name) if i == 0 { require.Equal(t, "iOS", labels[1].Name) } else { require.Equal(t, "iPadOS", labels[1].Name) } // Insert again to test updateMDMAppleHostDB. err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: fmt.Sprintf("test-uuid-%d", i), HardwareSerial: fmt.Sprintf("test-serial-%d", i), HardwareModel: "test-hw-model-2", Platform: platform, }, false) require.NoError(t, err) h, err = ds.HostByIdentifier(ctx, fmt.Sprintf("test-uuid-%d", i)) require.NoError(t, err) require.Equal(t, false, h.RefetchRequested) require.Less(t, time.Since(h.LastEnrolledAt), 1*time.Hour) // check it's not in the date in the 2000 we use as "Never". require.Equal(t, "test-hw-model-2", h.HardwareModel) labels, err = ds.ListLabelsForHost(ctx, h.ID) require.NoError(t, err) require.Len(t, labels, 2) sort.Slice(labels, func(i, j int) bool { return labels[i].ID < labels[j].ID }) require.Equal(t, "All Hosts", labels[0].Name) if i == 0 { require.Equal(t, "iOS", labels[1].Name) } else { require.Equal(t, "iPadOS", labels[1].Name) } } err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: "test-uuid-2", HardwareSerial: "test-serial-2", HardwareModel: "test-hw-model", Platform: "darwin", }, false) require.NoError(t, err) h, err := ds.HostByIdentifier(ctx, "test-uuid-2") require.NoError(t, err) require.Equal(t, true, h.RefetchRequested) require.Less(t, 1*time.Hour, time.Since(h.LastEnrolledAt)) // check it's in the date in the 2000 we use as "Never". labels, err := ds.ListLabelsForHost(ctx, h.ID) require.NoError(t, err) require.Len(t, labels, 2) require.Equal(t, "All Hosts", labels[0].Name) require.Equal(t, "macOS", labels[1].Name) } func testIngestMDMAppleDevicesFromDEPSyncIOSIPadOS(t *testing.T, ds *Datastore) { ctx := t.Context() // Mock results incoming from depsync.Syncer depDevices := []godep.Device{ {SerialNumber: "iOS0_SERIAL", DeviceFamily: "iPhone", OpType: "added"}, {SerialNumber: "iPadOS0_SERIAL", DeviceFamily: "iPad", OpType: "added"}, } encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.Equal(t, int64(2), n) hosts, err := ds.ListHosts(ctx, fleet.TeamFilter{ User: &fleet.User{ GlobalRole: ptr.String(fleet.RoleAdmin), }, }, fleet.HostListOptions{}) require.NoError(t, err) require.Len(t, hosts, 2) require.Equal(t, "ios", hosts[0].Platform) require.Equal(t, false, hosts[0].RefetchRequested) require.Equal(t, "ipados", hosts[1].Platform) require.Equal(t, false, hosts[1].RefetchRequested) } func testMDMAppleProfilesOnIOSIPadOS(t *testing.T, ds *Datastore) { ctx := t.Context() // Add the Fleetd configuration and profile that are only for macOS. params := mobileconfig.FleetdProfileOptions{ EnrollSecret: t.Name(), ServerURL: "https://example.com", PayloadType: mobileconfig.FleetdConfigPayloadIdentifier, PayloadName: fleetmdm.FleetdConfigProfileName, } var contents bytes.Buffer err := mobileconfig.FleetdProfileTemplate.Execute(&contents, params) require.NoError(t, err) fleetdConfigProfile, err := fleet.NewMDMAppleConfigProfile(contents.Bytes(), nil) require.NoError(t, err) _, err = ds.NewMDMAppleConfigProfile(ctx, *fleetdConfigProfile, nil) require.NoError(t, err) // For the FileVault profile we re-use the FleetdProfileTemplate // (because fileVaultProfileTemplate is not exported) var contents2 bytes.Buffer params.PayloadName = fleetmdm.FleetFileVaultProfileName params.PayloadType = mobileconfig.FleetFileVaultPayloadIdentifier err = mobileconfig.FleetdProfileTemplate.Execute(&contents2, params) require.NoError(t, err) fileVaultProfile, err := fleet.NewMDMAppleConfigProfile(contents2.Bytes(), nil) require.NoError(t, err) _, err = ds.NewMDMAppleConfigProfile(ctx, *fileVaultProfile, nil) require.NoError(t, err) err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: "iOS0_UUID", HardwareSerial: "iOS0_SERIAL", HardwareModel: "iPhone14,6", Platform: "ios", OsqueryHostID: ptr.String("iOS0_OSQUERY_HOST_ID"), }, false) require.NoError(t, err) iOS0, err := ds.HostByIdentifier(ctx, "iOS0_UUID") require.NoError(t, err) nanoEnroll(t, ds, iOS0, false) err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: "iPadOS0_UUID", HardwareSerial: "iPadOS0_SERIAL", HardwareModel: "iPad13,18", Platform: "ipados", OsqueryHostID: ptr.String("iPadOS0_OSQUERY_HOST_ID"), }, false) require.NoError(t, err) iPadOS0, err := ds.HostByIdentifier(ctx, "iPadOS0_UUID") require.NoError(t, err) nanoEnroll(t, ds, iPadOS0, false) someProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 0), nil) require.NoError(t, err) updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil) require.NoError(t, err) assert.True(t, updates.AppleConfigProfile) assert.False(t, updates.AppleDeclaration) assert.False(t, updates.WindowsConfigProfile) profiles, err := ds.GetHostMDMAppleProfiles(ctx, "iOS0_UUID") require.NoError(t, err) require.Len(t, profiles, 1) require.Equal(t, someProfile.Name, profiles[0].Name) profiles, err = ds.GetHostMDMAppleProfiles(ctx, "iPadOS0_UUID") require.NoError(t, err) require.Len(t, profiles, 1) require.Equal(t, someProfile.Name, profiles[0].Name) } func testGetEnrollmentIDsWithPendingMDMAppleCommands(t *testing.T, ds *Datastore) { ctx := t.Context() ids, err := ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.Empty(t, ids) var hosts []*fleet.Host hostUUIDToUserEnrollmentID := make(map[string]string) for i := 0; i < 10; i++ { h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now()) hosts = append(hosts, h) nanoEnroll(t, ds, h, true) userEnrollment, err := ds.GetNanoMDMUserEnrollment(ctx, h.UUID) require.NoError(t, err) require.NotNil(t, userEnrollment) require.Equal(t, h.UUID, userEnrollment.DeviceID) hostUUIDToUserEnrollmentID[h.UUID] = userEnrollment.ID } ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.Empty(t, ids) commander, storage := createMDMAppleCommanderAndStorage(t, ds) // insert a command for three hosts uuid1 := uuid.New().String() rawCmd1 := createRawAppleCmd("ListApps", uuid1) err = commander.EnqueueCommand(ctx, []string{hosts[0].UUID, hosts[1].UUID, hosts[2].UUID}, rawCmd1) require.NoError(t, err) ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[0].UUID, hosts[1].UUID, hosts[2].UUID}, ids) err = storage.StoreCommandReport(&mdm.Request{ EnrollID: &mdm.EnrollID{ID: hosts[0].UUID}, Context: ctx, }, &mdm.CommandResults{ CommandUUID: uuid1, Status: "Acknowledged", Raw: []byte(rawCmd1), }) require.NoError(t, err) // only hosts[1] and hosts[2] are returned now ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[1].UUID, hosts[2].UUID}, ids) // Enqueue some user channel commands // insert a command for three hosts uuid2 := uuid.New().String() rawCmd2 := createRawAppleCmd("InstallProfile", uuid2) err = commander.EnqueueCommand(ctx, []string{hostUUIDToUserEnrollmentID[hosts[3].UUID], hostUUIDToUserEnrollmentID[hosts[4].UUID], hostUUIDToUserEnrollmentID[hosts[5].UUID]}, rawCmd2) require.NoError(t, err) ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[1].UUID, hosts[2].UUID, hostUUIDToUserEnrollmentID[hosts[3].UUID], hostUUIDToUserEnrollmentID[hosts[4].UUID], hostUUIDToUserEnrollmentID[hosts[5].UUID]}, ids) err = storage.StoreCommandReport(&mdm.Request{ EnrollID: &mdm.EnrollID{ID: hostUUIDToUserEnrollmentID[hosts[3].UUID]}, Context: ctx, }, &mdm.CommandResults{ CommandUUID: uuid2, Status: "Acknowledged", Raw: []byte(rawCmd2), }) require.NoError(t, err) ids, err = ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{hosts[1].UUID, hosts[2].UUID, hostUUIDToUserEnrollmentID[hosts[4].UUID], hostUUIDToUserEnrollmentID[hosts[5].UUID]}, ids) } func testHostDetailsMDMProfilesIOSIPadOS(t *testing.T, ds *Datastore) { ctx := t.Context() p0, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{ Name: "Name0", Identifier: "Identifier0", Mobileconfig: []byte("profile0-bytes"), }, nil) require.NoError(t, err) profiles, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) require.NoError(t, err) require.Len(t, profiles, 1) iOS, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id"), NodeKey: ptr.String("host0-node-key"), UUID: "host0-test-mdm-profiles", Hostname: "hostname0", Platform: "ios", }) require.NoError(t, err) iPadOS, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id-2"), NodeKey: ptr.String("host0-node-key-2"), UUID: "host0-test-mdm-profiles-2", Hostname: "hostname0-2", Platform: "ipados", }) require.NoError(t, err) gotHost, err := ds.Host(ctx, iOS.ID) require.NoError(t, err) require.Nil(t, gotHost.MDM.Profiles) gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, iOS.UUID) require.NoError(t, err) require.Nil(t, gotProfs) gotHost, err = ds.Host(ctx, iPadOS.ID) require.NoError(t, err) require.Nil(t, gotHost.MDM.Profiles) gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, iPadOS.UUID) require.NoError(t, err) require.Nil(t, gotProfs) expectedProfilesIOS := map[string]fleet.HostMDMAppleProfile{ p0.ProfileUUID: { HostUUID: iOS.UUID, Name: p0.Name, ProfileUUID: p0.ProfileUUID, CommandUUID: "cmd0-uuid", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: "", }, } expectedProfilesIPadOS := map[string]fleet.HostMDMAppleProfile{ p0.ProfileUUID: { HostUUID: iPadOS.UUID, Name: p0.Name, ProfileUUID: p0.ProfileUUID, CommandUUID: "cmd0-uuid", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: "", }, } var args []interface{} i := 0 for _, p := range expectedProfilesIOS { args = append(args, p.HostUUID, p.ProfileUUID, p.CommandUUID, *p.Status, p.OperationType, p.Detail, p.Name, "com.test.profile."+p.ProfileUUID, // profile_identifier test.MakeTestChecksum(byte(i)), // checksum (16 bytes) ) i++ } for _, p := range expectedProfilesIPadOS { args = append(args, p.HostUUID, p.ProfileUUID, p.CommandUUID, *p.Status, p.OperationType, p.Detail, p.Name, "com.test.profile."+p.ProfileUUID, // profile_identifier test.MakeTestChecksum(byte(i)), // checksum (16 bytes) ) i++ } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` INSERT INTO host_mdm_apple_profiles ( host_uuid, profile_uuid, command_uuid, status, operation_type, detail, profile_name, profile_identifier, checksum) VALUES (?,?,?,?,?,?,?,?,?),(?,?,?,?,?,?,?,?,?) `, args..., ) if err != nil { return err } return nil }) for _, tc := range []struct { host *fleet.Host expectedProfiles map[string]fleet.HostMDMAppleProfile }{ { host: iOS, expectedProfiles: expectedProfilesIOS, }, { host: iPadOS, expectedProfiles: expectedProfilesIPadOS, }, } { gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, tc.host.UUID) require.NoError(t, err) require.Len(t, gotProfs, 1) for _, gp := range gotProfs { ep, ok := expectedProfilesIOS[gp.ProfileUUID] require.True(t, ok) require.Equal(t, ep.Name, gp.Name) require.Equal(t, *ep.Status, *gp.Status) require.Equal(t, ep.OperationType, gp.OperationType) require.Equal(t, ep.Detail, gp.Detail) } // mark pending profile to 'verifying', which should instead set it as 'verified'. installPendingProfile := expectedProfilesIOS[p0.ProfileUUID] err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ HostUUID: installPendingProfile.HostUUID, CommandUUID: installPendingProfile.CommandUUID, ProfileUUID: installPendingProfile.ProfileUUID, Name: installPendingProfile.Name, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, Detail: "", }) require.NoError(t, err) // Check that the profile is the 'verified' state. gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, iOS.UUID) require.NoError(t, err) require.Len(t, gotProfs, 1) require.NotNil(t, gotProfs[0].Status) require.Equal(t, fleet.MDMDeliveryVerified, *gotProfs[0].Status) } } func testMDMAppleBootstrapPackageWithS3(t *testing.T, ds *Datastore) { ctx := t.Context() var nfe fleet.NotFoundError var aerr fleet.AlreadyExistsError hashContent := func(content string) []byte { h := sha256.New() _, err := h.Write([]byte(content)) require.NoError(t, err) return h.Sum(nil) } bpMatchesWithoutContent := func(want, got *fleet.MDMAppleBootstrapPackage) { // make local copies so we don't alter the caller's structs w, g := *want, *got w.Bytes, g.Bytes = nil, nil w.CreatedAt, g.CreatedAt = time.Time{}, time.Time{} w.UpdatedAt, g.UpdatedAt = time.Time{}, time.Time{} require.Equal(t, w, g) } pkgStore := s3.SetupTestBootstrapPackageStore(t, "mdm-apple-bootstrap-package-test", "") err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{}, pkgStore) require.Error(t, err) // associate bp1 with no team bp1 := &fleet.MDMAppleBootstrapPackage{ TeamID: uint(0), Name: "bp1", Sha256: hashContent("bp1"), Bytes: []byte("bp1"), Token: uuid.New().String(), } err = ds.InsertMDMAppleBootstrapPackage(ctx, bp1, pkgStore) require.NoError(t, err) // try to store bp1 again, fails as it already exists err = ds.InsertMDMAppleBootstrapPackage(ctx, bp1, pkgStore) require.ErrorAs(t, err, &aerr) // associate bp2 with team id 2 bp2 := &fleet.MDMAppleBootstrapPackage{ TeamID: uint(2), Name: "bp2", Sha256: hashContent("bp2"), Bytes: []byte("bp2"), Token: uuid.New().String(), } err = ds.InsertMDMAppleBootstrapPackage(ctx, bp2, pkgStore) require.NoError(t, err) // associate the same content as bp1 with team id 1, via a copy err = ds.CopyDefaultMDMAppleBootstrapPackage(ctx, &fleet.AppConfig{}, 1) require.NoError(t, err) // get bp for no team meta, err := ds.GetMDMAppleBootstrapPackageMeta(ctx, 0) require.NoError(t, err) bpMatchesWithoutContent(bp1, meta) // get for team 1, token differs due to the copy, rest is the same meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 1) require.NoError(t, err) require.NotEqual(t, bp1.Token, meta.Token) bp1b := *bp1 bp1b.Token = meta.Token bp1b.TeamID = 1 bpMatchesWithoutContent(&bp1b, meta) // get for team 2 meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 2) require.NoError(t, err) bpMatchesWithoutContent(bp2, meta) // get for team 3, does not exist meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 3) require.ErrorAs(t, err, &nfe) require.Nil(t, meta) // get content for no team bpContent, err := ds.GetMDMAppleBootstrapPackageBytes(ctx, bp1.Token, pkgStore) require.NoError(t, err) require.Equal(t, bp1.Bytes, bpContent.Bytes) // get content for team 1 (copy of no team) bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, bp1b.Token, pkgStore) require.NoError(t, err) require.Equal(t, bp1b.Bytes, bpContent.Bytes) require.Equal(t, bp1.Bytes, bpContent.Bytes) // get content for team 2 bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, bp2.Token, pkgStore) require.NoError(t, err) require.Equal(t, bp2.Bytes, bpContent.Bytes) // get content with invalid token bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, "no-such-token", pkgStore) require.ErrorAs(t, err, &nfe) require.Nil(t, bpContent) // delete bp for no team and team 2 err = ds.DeleteMDMAppleBootstrapPackage(ctx, 0) require.NoError(t, err) err = ds.DeleteMDMAppleBootstrapPackage(ctx, 2) require.NoError(t, err) // run the cleanup job err = ds.CleanupUnusedBootstrapPackages(ctx, pkgStore, time.Now()) require.NoError(t, err) // team 1 can still be retrieved (it shares the same contents) bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, bp1b.Token, pkgStore) require.NoError(t, err) require.Equal(t, bp1b.Bytes, bpContent.Bytes) // team 0 and 2 don't exist anymore meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 0) require.ErrorAs(t, err, &nfe) require.Nil(t, meta) meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 2) require.ErrorAs(t, err, &nfe) require.Nil(t, meta) ok, err := pkgStore.Exists(ctx, hex.EncodeToString(bp1.Sha256)) require.NoError(t, err) require.True(t, ok) ok, err = pkgStore.Exists(ctx, hex.EncodeToString(bp2.Sha256)) require.NoError(t, err) require.False(t, ok) // delete team 1 err = ds.DeleteMDMAppleBootstrapPackage(ctx, 1) require.NoError(t, err) // force a team 3 bp to be saved in the DB (simulates upgrading to the new // S3-based storage with already-saved bps in the DB) bp3 := &fleet.MDMAppleBootstrapPackage{ TeamID: uint(3), Name: "bp3", Sha256: hashContent("bp3"), Bytes: []byte("bp3"), Token: uuid.New().String(), } err = ds.InsertMDMAppleBootstrapPackage(ctx, bp3, nil) // passing a nil pkgStore to force save in the DB require.NoError(t, err) // metadata can be read meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 3) require.NoError(t, err) bpMatchesWithoutContent(bp3, meta) // content will be retrieved correctly from the DB even if we pass a pkgStore bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, bp3.Token, pkgStore) require.NoError(t, err) require.Equal(t, bp3.Bytes, bpContent.Bytes) // run the cleanup job err = ds.CleanupUnusedBootstrapPackages(ctx, pkgStore, time.Now()) require.NoError(t, err) ok, err = pkgStore.Exists(ctx, hex.EncodeToString(bp1.Sha256)) require.NoError(t, err) require.False(t, ok) ok, err = pkgStore.Exists(ctx, hex.EncodeToString(bp2.Sha256)) require.NoError(t, err) require.False(t, ok) // bp3 does not exist in the S3 store ok, err = pkgStore.Exists(ctx, hex.EncodeToString(bp3.Sha256)) require.NoError(t, err) require.False(t, ok) // so it can still be retrieved from the DB bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, bp3.Token, pkgStore) require.NoError(t, err) require.Equal(t, bp3.Bytes, bpContent.Bytes) // it can be deleted without problem err = ds.DeleteMDMAppleBootstrapPackage(ctx, 3) require.NoError(t, err) bpContent, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, bp3.Token, pkgStore) require.ErrorAs(t, err, &nfe) require.Nil(t, bpContent) } func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) { ctx := t.Context() // get a non-existing token tok, err := ds.GetABMTokenByOrgName(ctx, "no-such-token") var nfe fleet.NotFoundError require.ErrorAs(t, err, &nfe) require.Nil(t, tok) // create some teams tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) require.NoError(t, err) tm2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) require.NoError(t, err) tm3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team3"}) require.NoError(t, err) toks, err := ds.ListABMTokens(ctx) require.NoError(t, err) require.Empty(t, toks) tokCount, err := ds.GetABMTokenCount(ctx) require.NoError(t, err) assert.EqualValues(t, 0, tokCount) // create a token with an empty name and no team set, and another that will be unused encTok := uuid.NewString() t1, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, t1.ID) t2, err := ds.InsertABMToken(ctx, &fleet.ABMToken{EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, t2.ID) toks, err = ds.ListABMTokens(ctx) require.NoError(t, err) require.Len(t, toks, 2) tokCount, err = ds.GetABMTokenCount(ctx) require.NoError(t, err) assert.EqualValues(t, 2, tokCount) // get that token tok, err = ds.GetABMTokenByOrgName(ctx, "") require.NoError(t, err) require.NotZero(t, tok.ID) require.Equal(t, encTok, string(tok.EncryptedToken)) require.Empty(t, tok.OrganizationName) require.Empty(t, tok.AppleID) require.Equal(t, fleet.TeamNameNoTeam, tok.MacOSTeamName) require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeamName) require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeamName) // update the token with a name and teams tok.OrganizationName = "org-name" tok.AppleID = "name@example.com" tok.MacOSDefaultTeamID = &tm1.ID tok.IOSDefaultTeamID = &tm2.ID err = ds.SaveABMToken(ctx, tok) require.NoError(t, err) // reload that token tokReload, err := ds.GetABMTokenByOrgName(ctx, "org-name") require.NoError(t, err) require.Equal(t, tok.ID, tokReload.ID) require.Equal(t, encTok, string(tokReload.EncryptedToken)) require.Equal(t, "org-name", tokReload.OrganizationName) require.Equal(t, "name@example.com", tokReload.AppleID) require.Equal(t, tm1.Name, tokReload.MacOSTeamName) require.Equal(t, tm1.Name, tokReload.MacOSTeam.Name) require.Equal(t, tm1.ID, tokReload.MacOSTeam.ID) require.Equal(t, tm2.Name, tokReload.IOSTeamName) require.Equal(t, tm2.Name, tokReload.IOSTeam.Name) require.Equal(t, tm2.ID, tokReload.IOSTeam.ID) require.Equal(t, fleet.TeamNameNoTeam, tokReload.IPadOSTeamName) require.Equal(t, fleet.TeamNameNoTeam, tokReload.IPadOSTeam.Name) require.Equal(t, uint(0), tokReload.IPadOSTeam.ID) // empty name token now doesn't exist _, err = ds.GetABMTokenByOrgName(ctx, "") require.ErrorAs(t, err, &nfe) // update some teams tok.MacOSDefaultTeamID = nil tok.IPadOSDefaultTeamID = &tm3.ID err = ds.SaveABMToken(ctx, tok) require.NoError(t, err) // reload that token tokReload, err = ds.GetABMTokenByOrgName(ctx, "org-name") require.NoError(t, err) require.Equal(t, tok.ID, tokReload.ID) require.Equal(t, encTok, string(tokReload.EncryptedToken)) require.Equal(t, "org-name", tokReload.OrganizationName) require.Equal(t, "name@example.com", tokReload.AppleID) require.Equal(t, fleet.TeamNameNoTeam, tokReload.MacOSTeamName) require.Equal(t, fleet.TeamNameNoTeam, tokReload.MacOSTeam.Name) require.Equal(t, uint(0), tokReload.MacOSTeam.ID) require.Equal(t, tm2.Name, tokReload.IOSTeamName) require.Equal(t, tm3.Name, tokReload.IPadOSTeamName) // change just the encrypted token encTok2 := uuid.NewString() tok.EncryptedToken = []byte(encTok2) err = ds.SaveABMToken(ctx, tok) require.NoError(t, err) tokReload, err = ds.GetABMTokenByOrgName(ctx, "org-name") require.NoError(t, err) require.Equal(t, tok.ID, tokReload.ID) require.Equal(t, encTok2, string(tokReload.EncryptedToken)) require.Equal(t, "org-name", tokReload.OrganizationName) require.Equal(t, "name@example.com", tokReload.AppleID) require.Equal(t, fleet.TeamNameNoTeam, tokReload.MacOSTeamName) require.Equal(t, fleet.TeamNameNoTeam, tokReload.MacOSTeam.Name) require.Equal(t, uint(0), tokReload.MacOSTeam.ID) require.Equal(t, tm2.Name, tokReload.IOSTeamName) require.Equal(t, tm2.Name, tokReload.IOSTeam.Name) require.Equal(t, tm2.ID, tokReload.IOSTeam.ID) require.Equal(t, tm3.Name, tokReload.IPadOSTeamName) require.Equal(t, tm3.Name, tokReload.IPadOSTeam.Name) require.Equal(t, tm3.ID, tokReload.IPadOSTeam.ID) // Remove unused token require.NoError(t, ds.DeleteABMToken(ctx, t1.ID)) toks, err = ds.ListABMTokens(ctx) require.NoError(t, err) require.Len(t, toks, 1) expTok := toks[0] require.Equal(t, "org-name", expTok.OrganizationName) require.Equal(t, "name@example.com", expTok.AppleID) require.Equal(t, fleet.TeamNameNoTeam, expTok.MacOSTeamName) require.Equal(t, fleet.TeamNameNoTeam, expTok.MacOSTeam.Name) require.Equal(t, uint(0), expTok.MacOSTeam.ID) require.Equal(t, tm2.Name, expTok.IOSTeamName) require.Equal(t, tm3.Name, expTok.IPadOSTeamName) tokCount, err = ds.GetABMTokenCount(ctx) require.NoError(t, err) assert.EqualValues(t, 1, tokCount) } func testMDMAppleABMTokensTermsExpired(t *testing.T, ds *Datastore) { ctx := t.Context() // count works with no token count, err := ds.CountABMTokensWithTermsExpired(ctx) require.NoError(t, err) require.Zero(t, count) // create a few tokens encTok1 := uuid.NewString() t1, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "abm1", EncryptedToken: []byte(encTok1), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, t1.ID) encTok2 := uuid.NewString() t2, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "abm2", EncryptedToken: []byte(encTok2), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, t2.ID) // this one simulates a mirated token - empty name encTok3 := uuid.NewString() t3, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "", EncryptedToken: []byte(encTok3), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, t3.ID) // none have terms expired yet count, err = ds.CountABMTokensWithTermsExpired(ctx) require.NoError(t, err) require.Zero(t, count) // set t1 terms expired was, err := ds.SetABMTokenTermsExpiredForOrgName(ctx, t1.OrganizationName, true) require.NoError(t, err) require.False(t, was) // set t2 terms not expired, no-op was, err = ds.SetABMTokenTermsExpiredForOrgName(ctx, t2.OrganizationName, false) require.NoError(t, err) require.False(t, was) // set t3 terms expired was, err = ds.SetABMTokenTermsExpiredForOrgName(ctx, t3.OrganizationName, true) require.NoError(t, err) require.False(t, was) // count is now 2 count, err = ds.CountABMTokensWithTermsExpired(ctx) require.NoError(t, err) require.EqualValues(t, 2, count) // set t1 terms not expired was, err = ds.SetABMTokenTermsExpiredForOrgName(ctx, t1.OrganizationName, false) require.NoError(t, err) require.True(t, was) // set t3 terms still expired was, err = ds.SetABMTokenTermsExpiredForOrgName(ctx, t3.OrganizationName, true) require.NoError(t, err) require.True(t, was) // count is now 1 count, err = ds.CountABMTokensWithTermsExpired(ctx) require.NoError(t, err) require.EqualValues(t, 1, count) // setting the expired flag of a non-existing token always returns as if it // did not update (which is fine, it will only be called after a DEP API call // that used this token, so if the token does not exist it would fail the // call). was, err = ds.SetABMTokenTermsExpiredForOrgName(ctx, "no-such-token", false) require.NoError(t, err) require.False(t, was) was, err = ds.SetABMTokenTermsExpiredForOrgName(ctx, "no-such-token", true) require.NoError(t, err) require.True(t, was) // count is unaffected count, err = ds.CountABMTokensWithTermsExpired(ctx) require.NoError(t, err) require.EqualValues(t, 1, count) } func testMDMGetABMTokenOrgNamesAssociatedWithTeam(t *testing.T, ds *Datastore) { ctx := t.Context() // Create some teams tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) require.NoError(t, err) tm2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) require.NoError(t, err) encTok := uuid.NewString() tok1, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "org1", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, tok1.ID) tok2, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "org2", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, tok1.ID) tok3, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "org3", EncryptedToken: []byte(encTok), MacOSDefaultTeamID: &tm2.ID, RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, tok1.ID) // Create some hosts and add to teams (and one for no team) h1, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", OsqueryHostID: ptr.String("1"), NodeKey: ptr.String("1"), UUID: "test-uuid-1", TeamID: &tm1.ID, Platform: "darwin", }) require.NoError(t, err) h2, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host2-name", OsqueryHostID: ptr.String("2"), NodeKey: ptr.String("2"), UUID: "test-uuid-2", TeamID: &tm1.ID, Platform: "darwin", }) require.NoError(t, err) h3, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host3-name", OsqueryHostID: ptr.String("3"), NodeKey: ptr.String("3"), UUID: "test-uuid-3", TeamID: nil, Platform: "darwin", }) require.NoError(t, err) h4, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host4-name", OsqueryHostID: ptr.String("4"), NodeKey: ptr.String("4"), UUID: "test-uuid-4", TeamID: &tm1.ID, Platform: "darwin", }) require.NoError(t, err) // Insert host DEP assignment require.NoError(t, ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h1, *h4}, tok1.ID, make(map[uint]time.Time))) require.NoError(t, ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h2}, tok3.ID, make(map[uint]time.Time))) require.NoError(t, ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h3}, tok2.ID, make(map[uint]time.Time))) // Should return the 2 unique org names [org1, org3] orgNames, err := ds.GetABMTokenOrgNamesAssociatedWithTeam(ctx, &tm1.ID) require.NoError(t, err) sort.Strings(orgNames) require.Len(t, orgNames, 2) require.Equal(t, orgNames[0], "org1") require.Equal(t, orgNames[1], "org3") // all tokens default to no team in one way or another orgNames, err = ds.GetABMTokenOrgNamesAssociatedWithTeam(ctx, nil) require.NoError(t, err) sort.Strings(orgNames) require.Len(t, orgNames, 3) require.Equal(t, orgNames[0], "org1") require.Equal(t, orgNames[1], "org2") require.Equal(t, orgNames[2], "org3") // No orgs for this team except org3 which uses it as a default team orgNames, err = ds.GetABMTokenOrgNamesAssociatedWithTeam(ctx, &tm2.ID) require.NoError(t, err) sort.Strings(orgNames) require.Len(t, orgNames, 1) require.Equal(t, orgNames[0], "org3") } func testHostMDMCommands(t *testing.T, ds *Datastore) { ctx := t.Context() addHostMDMCommandsBatchSizeOrig := addHostMDMCommandsBatchSize addHostMDMCommandsBatchSize = 2 t.Cleanup(func() { addHostMDMCommandsBatchSize = addHostMDMCommandsBatchSizeOrig }) // create a host h, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id"), NodeKey: ptr.String("host0-node-key"), UUID: "host0-test-mdm-profiles", Hostname: "hostname0", }) require.NoError(t, err) hostCommands := []fleet.HostMDMCommand{ { HostID: h.ID, CommandType: "command-1", }, { HostID: h.ID, CommandType: "command-2", }, { HostID: h.ID, CommandType: "command-3", }, } badHostID := h.ID + 1 allCommands := hostCommands allCommands = append(allCommands, fleet.HostMDMCommand{ HostID: badHostID, CommandType: "command-1", }) err = ds.AddHostMDMCommands(ctx, allCommands) require.NoError(t, err) commands, err := ds.GetHostMDMCommands(ctx, h.ID) require.NoError(t, err) assert.ElementsMatch(t, hostCommands, commands) // Remove a command require.NoError(t, ds.RemoveHostMDMCommand(ctx, hostCommands[0])) commands, err = ds.GetHostMDMCommands(ctx, h.ID) require.NoError(t, err) assert.ElementsMatch(t, hostCommands[1:], commands) // Clean up commands, and make sure badHost commands have been removed, but others remain. commands, err = ds.GetHostMDMCommands(ctx, badHostID) require.NoError(t, err) assert.Len(t, commands, 1) require.NoError(t, ds.CleanupHostMDMCommands(ctx)) commands, err = ds.GetHostMDMCommands(ctx, badHostID) require.NoError(t, err) assert.Empty(t, commands) commands, err = ds.GetHostMDMCommands(ctx, h.ID) require.NoError(t, err) assert.ElementsMatch(t, hostCommands[1:], commands) } func testIngestMDMAppleDeviceFromOTAEnrollment(t *testing.T, ds *Datastore) { ctx := t.Context() createBuiltinLabels(t, ds) for i := 0; i < 10; i++ { _, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("hostname_%d", i), DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now().Add(-time.Duration(i) * time.Minute), OsqueryHostID: ptr.String(fmt.Sprintf("osquery-host-id_%d", i)), NodeKey: ptr.String(fmt.Sprintf("node-key_%d", i)), UUID: fmt.Sprintf("uuid_%d", i), HardwareSerial: fmt.Sprintf("serial_%d", i), }) require.NoError(t, err) } hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 10) wantSerials := []string{} for _, h := range hosts { wantSerials = append(wantSerials, h.HardwareSerial) } // mock results incoming from OTA enrollments otaDevices := []fleet.MDMAppleMachineInfo{ {Serial: "abc", Product: "MacBook Pro"}, {Serial: "abc", Product: "MacBook Pro"}, {Serial: hosts[0].HardwareSerial, Product: "MacBook Pro"}, {Serial: "ijk", Product: "iPad13,16"}, {Serial: "tuv", Product: "iPhone14,6"}, {Serial: hosts[1].HardwareSerial, Product: "MacBook Pro"}, {Serial: "xyz", Product: "MacBook Pro"}, {Serial: "xyz", Product: "MacBook Pro"}, {Serial: "xyz", Product: "MacBook Pro"}, } wantSerials = append(wantSerials, "abc", "xyz", "ijk", "tuv") for _, d := range otaDevices { err := ds.IngestMDMAppleDeviceFromOTAEnrollment(ctx, nil, "", d) require.NoError(t, err) } hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, len(wantSerials)) gotSerials := []string{} for _, h := range hosts { gotSerials = append(gotSerials, h.HardwareSerial) switch h.HardwareSerial { case "abc", "xyz": checkMDMHostRelatedTables(t, ds, h.ID, h.HardwareSerial, "MacBook Pro") case "ijk": checkMDMHostRelatedTables(t, ds, h.ID, h.HardwareSerial, "iPad13,16") case "tuv": checkMDMHostRelatedTables(t, ds, h.ID, h.HardwareSerial, "iPhone14,6") } } require.ElementsMatch(t, wantSerials, gotSerials) } func TestGetMDMAppleOSUpdatesSettingsByHostSerial(t *testing.T) { ds := CreateMySQLDS(t) defer ds.Close() keys := []string{"ios", "ipados", "macos"} devicesByKey := map[string]godep.Device{ "ios": {SerialNumber: "dep-serial-ios-updates", DeviceFamily: "iPhone"}, "ipados": {SerialNumber: "dep-serial-ipados-updates", DeviceFamily: "iPad"}, "macos": {SerialNumber: "dep-serial-macos-updates", DeviceFamily: "Mac"}, } getConfigSettings := func(teamID uint, key string) *fleet.AppleOSUpdateSettings { var settings fleet.AppleOSUpdateSettings ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { stmt := fmt.Sprintf(`SELECT json_value->'$.mdm.%s_updates' FROM app_config_json`, key) if teamID > 0 { stmt = fmt.Sprintf(`SELECT config->'$.mdm.%s_updates' FROM teams WHERE id = %d`, key, teamID) } var raw json.RawMessage if err := sqlx.GetContext(context.Background(), q, &raw, stmt); err != nil { return err } if err := json.Unmarshal(raw, &settings); err != nil { return err } return nil }) return &settings } setConfigSettings := func(teamID uint, key string, minVersion string) { var mv *string if minVersion != "" { mv = &minVersion } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { stmt := fmt.Sprintf(`UPDATE app_config_json SET json_value = JSON_SET(json_value, '$.mdm.%s_updates.minimum_version', ?)`, key) if teamID > 0 { stmt = fmt.Sprintf(`UPDATE teams SET config = JSON_SET(config, '$.mdm.%s_updates.minimum_version', ?) WHERE id = %d`, key, teamID) } if _, err := q.ExecContext(context.Background(), stmt, mv); err != nil { return err } return nil }) } checkExpectedVersion := func(t *testing.T, gotSettings *fleet.AppleOSUpdateSettings, expectedVersion string) { if expectedVersion == "" { require.True(t, gotSettings.MinimumVersion.Set) require.False(t, gotSettings.MinimumVersion.Valid) require.Empty(t, gotSettings.MinimumVersion.Value) } else { require.True(t, gotSettings.MinimumVersion.Set) require.True(t, gotSettings.MinimumVersion.Valid) require.Equal(t, expectedVersion, gotSettings.MinimumVersion.Value) } } checkDevice := func(t *testing.T, teamID uint, key string, wantVersion string) { checkExpectedVersion(t, getConfigSettings(teamID, key), wantVersion) platform, gotSettings, err := ds.GetMDMAppleOSUpdatesSettingsByHostSerial(context.Background(), devicesByKey[key].SerialNumber) require.NoError(t, err) checkExpectedVersion(t, gotSettings, wantVersion) if key == "macos" { require.Equal(t, "darwin", platform) } else { require.Equal(t, platform, key) } } // empty global settings to start for _, key := range keys { checkExpectedVersion(t, getConfigSettings(0, key), "") } encTok := uuid.NewString() abmToken, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{OrganizationName: "unused", EncryptedToken: []byte(encTok), RenewAt: time.Now().Add(365 * 24 * time.Hour)}) require.NoError(t, err) require.NotEmpty(t, abmToken.ID) // ingest some test devices n, err := ds.IngestMDMAppleDevicesFromDEPSync(context.Background(), []godep.Device{devicesByKey["ios"], devicesByKey["ipados"], devicesByKey["macos"]}, abmToken.ID, nil, nil, nil) require.NoError(t, err) require.Equal(t, int64(3), n) hostIDsByKey := map[string]uint{} for key, device := range devicesByKey { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { var hid uint err = sqlx.GetContext(context.Background(), q, &hid, "SELECT id FROM hosts WHERE hardware_serial = ?", device.SerialNumber) require.NoError(t, err) hostIDsByKey[key] = hid return nil }) } // not set in global config, so devics should return empty checkDevice(t, 0, "ios", "") checkDevice(t, 0, "ipados", "") checkDevice(t, 0, "macos", "") // set the minimum version for ios setConfigSettings(0, "ios", "17.1") checkDevice(t, 0, "ios", "17.1") checkDevice(t, 0, "ipados", "") // no change checkDevice(t, 0, "macos", "") // no change // set the minimum version for ipados setConfigSettings(0, "ipados", "17.2") checkDevice(t, 0, "ios", "17.1") // no change checkDevice(t, 0, "ipados", "17.2") checkDevice(t, 0, "macos", "") // no change // set the minimum version for macos setConfigSettings(0, "macos", "14.5") checkDevice(t, 0, "ios", "17.1") // no change checkDevice(t, 0, "ipados", "17.2") // no change checkDevice(t, 0, "macos", "14.5") // create a team team, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) require.NoError(t, err) // empty team settings to start for _, key := range keys { checkExpectedVersion(t, getConfigSettings(team.ID, key), "") } // transfer ios and ipados to the team err = ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&team.ID, []uint{hostIDsByKey["ios"], hostIDsByKey["ipados"]})) require.NoError(t, err) checkDevice(t, team.ID, "ios", "") // team settings are empty to start checkDevice(t, team.ID, "ipados", "") // team settings are empty to start checkDevice(t, 0, "macos", "14.5") // no change, still global setConfigSettings(team.ID, "ios", "17.3") checkDevice(t, team.ID, "ios", "17.3") // team settings are set for ios checkDevice(t, team.ID, "ipados", "") // team settings are empty for ipados checkDevice(t, 0, "macos", "14.5") // no change, still global setConfigSettings(team.ID, "ipados", "17.4") checkDevice(t, team.ID, "ios", "17.3") // no change in team settings for ios checkDevice(t, team.ID, "ipados", "17.4") // team settings are set for ipados checkDevice(t, 0, "macos", "14.5") // no change, still global // transfer macos to the team err = ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&team.ID, []uint{hostIDsByKey["macos"]})) require.NoError(t, err) checkDevice(t, team.ID, "macos", "") // team settings are empty for macos setConfigSettings(team.ID, "macos", "14.6") checkDevice(t, team.ID, "macos", "14.6") // team settings are set for macos // create a non-DEP host _, err = ds.NewHost(context.Background(), &fleet.Host{ OsqueryHostID: ptr.String("non-dep-osquery-id"), NodeKey: ptr.String("non-dep-node-key"), UUID: "non-dep-uuid", Hostname: "non-dep-hostname", Platform: "macos", HardwareSerial: "non-dep-serial", }) // non-DEP host should return not found _, _, err = ds.GetMDMAppleOSUpdatesSettingsByHostSerial(context.Background(), "non-dep-serial") require.ErrorIs(t, err, sql.ErrNoRows) // deleted DEP host should return not found ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(context.Background(), "UPDATE host_dep_assignments SET deleted_at = NOW() WHERE host_id = ?", hostIDsByKey["macos"]) return err }) _, _, err = ds.GetMDMAppleOSUpdatesSettingsByHostSerial(context.Background(), devicesByKey["macos"].SerialNumber) require.ErrorIs(t, err, sql.ErrNoRows) } func testMDMManagedSCEPCertificates(t *testing.T, ds *Datastore) { ctx := context.Background() testCases := []struct { name string caName string caType fleet.CAConfigAssetType challengeRetrievedAt *time.Time }{ { name: "NDES", caName: "ndes", caType: fleet.CAConfigNDES, challengeRetrievedAt: ptr.Time(time.Now().Add(-time.Hour).UTC().Round(time.Microsecond)), }, { name: "Custom SCEP", caName: "test-ca", caType: fleet.CAConfigCustomSCEPProxy, challengeRetrievedAt: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { caName := tc.caName caType := tc.caType challengeRetrievedAt := tc.challengeRetrievedAt dummyMC := mobileconfig.Mobileconfig([]byte("DummyTestMobileconfigBytes")) dummyCP := fleet.MDMAppleConfigProfile{ Name: tc.caName, Identifier: tc.caName, Mobileconfig: dummyMC, TeamID: nil, } initialCP, err := ds.NewMDMAppleConfigProfile(ctx, dummyCP, nil) require.NoError(t, err) checkConfigProfile(t, dummyCP, *initialCP) host, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id" + tc.caName), NodeKey: ptr.String("host0-node-key" + tc.caName), UUID: "host0-test-mdm-profiles" + tc.caName, Hostname: "hostname0", }) require.NoError(t, err) // Host and profile are not linked profile, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) assert.Nil(t, profile) err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { ProfileUUID: initialCP.ProfileUUID, ProfileIdentifier: initialCP.Identifier, ProfileName: initialCP.Name, HostUUID: host.UUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte("checksum"), Scope: fleet.PayloadScopeSystem, }, }, ) require.NoError(t, err) // Host and profile do not have certificate metadata profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) assert.Nil(t, profile) // Initial certificate state where a host has been requested to install but we have no metadata err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, Type: caType, CAName: caName, }, }) require.NoError(t, err) // Check that the managed certificate was inserted correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, caType, profile.Type) assert.Nil(t, profile.Serial) assert.Nil(t, profile.NotValidBefore) assert.Nil(t, profile.NotValidAfter) assert.Equal(t, caName, profile.CAName) // Renew should not do anything yet err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryPending, *profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) serial := "8ABADCAFEF684D6348F5EC95AEFF468F237A9D75" t.Run("Non renewal scenario 1 - validity window > 30 days but not yet time to renew", func(t *testing.T) { // Set not_valid_before to 1 day in the past and not_valid_after to 31 days in the future so // teh validity window is 32 days of which there are 31 left which should not trigger renewal notValidAfter := time.Now().Add(31 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidBefore := time.Now().Add(-1 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should not change the MDM delivery status err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Non renewal scenario 2 - validity window < 30 days but not yet time to renew", func(t *testing.T) { // Set not_valid_before to 13 days in the past and not_valid_after to 15 days in the future so // the validity window is 28 days of which there are 15 left which should not trigger renewal notValidAfter := time.Now().Add(15 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidBefore := time.Now().Add(-13 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should not change the MDM delivery status err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Renew scenario 1 - validity window > 30 days", func(t *testing.T) { // Set not_valid_before to 31 days in the past the validity window becomes 60 days, of which there are // 29 left which should trigger the first renewal scenario(window > 30 days, renew when < 30 // days left) notValidAfter := time.Now().Add(29 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidBefore := time.Now().Add(-31 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.Nil(t, profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Renew scenario 2 - validity window < 30 days", func(t *testing.T) { // Set not_valid_before to 15 days in the past and not_valid_after to 14 days in the future so the // validity window becomes 29 days, of which there are 14 left which should trigger the second // renewal scenario(window < 30 days, renew when there is half that time left) notValidBefore := time.Now().Add(-15 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidAfter := time.Now().Add(14 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) require.NoError(t, err) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) require.NoError(t, err) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.Nil(t, profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) }) } } func testMDMManagedDigicertCertificates(t *testing.T, ds *Datastore) { ctx := t.Context() initialCP := storeDummyConfigProfilesForTest(t, ds, 1)[0] host, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id"), NodeKey: ptr.String("host0-node-key"), UUID: "host0-test-mdm-profiles", Hostname: "hostname0", }) require.NoError(t, err) // Host and profile are not linked profile, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) assert.Nil(t, profile) err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { ProfileUUID: initialCP.ProfileUUID, ProfileIdentifier: initialCP.Identifier, ProfileName: initialCP.Name, HostUUID: host.UUID, Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte("checksum"), Scope: fleet.PayloadScopeSystem, }, }, ) require.NoError(t, err) // Host and profile do not have certificate metadata profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) assert.Nil(t, profile) notValidBefore := time.Now().UTC().Round(time.Microsecond) notValidAfter := time.Now().Add(29 * 24 * time.Hour).UTC().Round(time.Microsecond) serial := "3ABADCAFEF684D6348F5EC95AEFF468F237A9D7E" err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: nil, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: fleet.CAConfigDigiCert, CAName: "test-ca", Serial: &serial, }, }) require.NoError(t, err) // Check that the managed certificate was inserted correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.NotNil(t, profile) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Nil(t, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, fleet.CAConfigDigiCert, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, "test-ca", profile.CAName) // Renew should not do anything yet so the MDM delivery status should stay "verified" err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) t.Run("Renew scenario 1 - validity window > 30 days", func(t *testing.T) { // Set not_valid_before to 31 days in the past the validity window becomes 60 days, of which there are // 29 left which should trigger the first renewal scenario(window > 30 days, renew when < 30 // days left) notValidBefore := time.Now().Add(-31 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: nil, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: fleet.CAConfigDigiCert, CAName: "test-ca", Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Nil(t, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, fleet.CAConfigDigiCert, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, "test-ca", profile.CAName) // Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.Nil(t, profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Renew scenario 2 - validity window < 30 days", func(t *testing.T) { // Set not_valid_before to 15 days in the past and not_valid_after to 14 days in the future so the // validity window becomes 29 days, of which there are 14 left which should trigger the second // renewal scenario(window < 30 days, renew when there is half that time left) notValidBefore := time.Now().Add(-15 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidAfter := time.Now().Add(14 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: nil, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: fleet.CAConfigDigiCert, CAName: "test-ca", Serial: &serial, }, }) require.NoError(t, err) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) require.NoError(t, err) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Nil(t, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, fleet.CAConfigDigiCert, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, "test-ca", profile.CAName) // Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.Nil(t, profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca") require.NoError(t, err) require.NotNil(t, profile) }) badProfileUUID := uuid.NewString() ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` INSERT INTO host_mdm_managed_certificates (host_uuid, profile_uuid) VALUES (?, ?) `, host.UUID, badProfileUUID) if err != nil { return err } return nil }) var uid string ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`, badProfileUUID) }) require.Equal(t, badProfileUUID, uid) // Cleanup should delete the above orphaned record err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) err = ExecAdhocSQLWithError(ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`, badProfileUUID) }) require.ErrorIs(t, err, sql.ErrNoRows) } func testAppleMDMSetBatchAsyncLastSeenAt(t *testing.T, ds *Datastore) { ctx := t.Context() // create some hosts, all enrolled enrolledHosts := make([]*fleet.Host, 2) for i := 0; i < len(enrolledHosts); i++ { h, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("test-host%d-name", i), OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)), NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)), UUID: fmt.Sprintf("test-uuid-%d", i), Platform: "darwin", }) require.NoError(t, err) nanoEnroll(t, ds, h, false) enrolledHosts[i] = h t.Logf("enrolled host [%d]: %s", i, h.UUID) } getHostLastSeenAt := func(h *fleet.Host) time.Time { var lastSeenAt time.Time ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &lastSeenAt, `SELECT last_seen_at FROM nano_enrollments WHERE device_id = ?`, h.UUID) }) return lastSeenAt } storage, err := ds.NewTestMDMAppleMDMStorage(2, 5*time.Second) require.NoError(t, err) commander := apple_mdm.NewMDMAppleCommander(storage, pusherFunc(okPusherFunc)) // enqueue a command for a couple of enrolled hosts uuid1 := uuid.NewString() rawCmd1 := createRawAppleCmd("ProfileList", uuid1) err = commander.EnqueueCommand(ctx, []string{enrolledHosts[0].UUID, enrolledHosts[1].UUID}, rawCmd1) require.NoError(t, err) // at this point, last_seen_at is still the original value ts1, ts2 := getHostLastSeenAt(enrolledHosts[0]), getHostLastSeenAt(enrolledHosts[1]) time.Sleep(time.Second + time.Millisecond) // ensure a distinct mysql timestamp // simulate a result for enrolledHosts[0] err = storage.StoreCommandReport(&mdm.Request{ EnrollID: &mdm.EnrollID{ID: enrolledHosts[0].UUID}, Context: ctx, }, &mdm.CommandResults{ CommandUUID: uuid1, Status: "Acknowledged", Raw: []byte(rawCmd1), }) require.NoError(t, err) // simulate a result for enrolledHosts[1] err = storage.StoreCommandReport(&mdm.Request{ EnrollID: &mdm.EnrollID{ID: enrolledHosts[1].UUID}, Context: ctx, }, &mdm.CommandResults{ CommandUUID: uuid1, Status: "Error", Raw: []byte(rawCmd1), }) require.NoError(t, err) // timestamps should've been updated ts1b, ts2b := getHostLastSeenAt(enrolledHosts[0]), getHostLastSeenAt(enrolledHosts[1]) require.True(t, ts1b.After(ts1)) require.True(t, ts2b.After(ts2)) } func testGetNanoMDMEnrollmentTimes(t *testing.T, ds *Datastore) { ctx := t.Context() host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-uuid-1", TeamID: nil, Platform: "darwin", }) require.NoError(t, err) lastMDMEnrolledAt, lastMDMSeenAt, err := ds.GetNanoMDMEnrollmentTimes(ctx, host.UUID) require.NoError(t, err) require.Nil(t, lastMDMEnrolledAt) require.Nil(t, lastMDMSeenAt) // add user and device enrollment for this device. Timestamps should not be updated so nothing // returned yet nanoEnroll(t, ds, host, true) lastMDMEnrolledAt, lastMDMSeenAt, err = ds.GetNanoMDMEnrollmentTimes(ctx, host.UUID) require.NoError(t, err) require.NotNil(t, lastMDMEnrolledAt) // defaults to current time on creation require.NotNil(t, lastMDMSeenAt) // defaults to time 0 value // Add a BYOD host byodHost, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-byod-host1-name", OsqueryHostID: ptr.String("1338"), NodeKey: ptr.String("1338"), UUID: "test-byod-uuid-2", TeamID: nil, Platform: "ios", }) require.NoError(t, err) nanoEnrollUserDevice(t, ds, byodHost) lastMDMEnrolledAt, lastMDMSeenAt, err = ds.GetNanoMDMEnrollmentTimes(ctx, byodHost.UUID) require.NoError(t, err) require.NotNil(t, lastMDMEnrolledAt) // defaults to current time on creation require.NotNil(t, lastMDMSeenAt) // defaults to time 0 value authenticateTime := time.Now().Add(-1 * time.Hour).UTC().Round(time.Second) deviceEnrollTime := time.Now().Add(-2 * time.Hour).UTC().Round(time.Second) userEnrollTime := time.Now().Add(-3 * time.Hour).UTC().Round(time.Second) byodDeviceAuthenticateTime := time.Now().Add(-4 * time.Hour).UTC().Round(time.Second) byodDeviceEnrollTime := time.Now().Add(-5 * time.Hour).UTC().Round(time.Second) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE nano_devices SET authenticate_at=? WHERE id = ?`, authenticateTime, host.UUID) if err != nil { return err } _, err = q.ExecContext(ctx, `UPDATE nano_enrollments SET last_seen_at=? WHERE type='Device' AND device_id = ?`, deviceEnrollTime, host.UUID) if err != nil { return err } _, err = q.ExecContext(ctx, `UPDATE nano_enrollments SET last_seen_at=? WHERE type='User' AND device_id = ?`, userEnrollTime, host.UUID) if err != nil { return err } _, err = q.ExecContext(ctx, `UPDATE nano_devices SET authenticate_at=? WHERE id = ?`, byodDeviceAuthenticateTime, byodHost.UUID) if err != nil { return err } _, err = q.ExecContext(ctx, `UPDATE nano_enrollments SET last_seen_at=? WHERE device_id = ?`, byodDeviceEnrollTime, byodHost.UUID) if err != nil { return err } return nil }) lastMDMEnrolledAt, lastMDMSeenAt, err = ds.GetNanoMDMEnrollmentTimes(ctx, host.UUID) require.NoError(t, err) require.NotNil(t, lastMDMEnrolledAt) assert.Equal(t, authenticateTime, *lastMDMEnrolledAt) require.NotNil(t, lastMDMSeenAt) assert.Equal(t, deviceEnrollTime, *lastMDMSeenAt) lastMDMEnrolledAt, lastMDMSeenAt, err = ds.GetNanoMDMEnrollmentTimes(ctx, byodHost.UUID) require.NoError(t, err) require.NotNil(t, lastMDMEnrolledAt) assert.Equal(t, byodDeviceAuthenticateTime, *lastMDMEnrolledAt) require.NotNil(t, lastMDMSeenAt) assert.Equal(t, byodDeviceEnrollTime, *lastMDMSeenAt) } func testGetNanoMDMUserEnrollment(t *testing.T, ds *Datastore) { ctx := t.Context() // unknown host uuid userEnrollment, err := ds.GetNanoMDMUserEnrollment(ctx, "no-such-host") require.NoError(t, err) require.Nil(t, userEnrollment) username, uuid, err := ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, "no-such-host") require.NoError(t, err) require.Empty(t, username) require.Empty(t, uuid) host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-uuid-1", TeamID: nil, Platform: "darwin", }) require.NoError(t, err) lastMDMEnrolledAt, lastMDMSeenAt, err := ds.GetNanoMDMEnrollmentTimes(ctx, host.UUID) require.NoError(t, err) require.Nil(t, lastMDMEnrolledAt) require.Nil(t, lastMDMSeenAt) userEnrollment, err = ds.GetNanoMDMUserEnrollment(ctx, host.UUID) require.NoError(t, err) require.Nil(t, userEnrollment) username, uuid, err = ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, host.UUID) require.NoError(t, err) require.Empty(t, username) require.Empty(t, uuid) // add user and device enrollment for this device. Timestamps should not be updated so nothing // returned yet nanoEnroll(t, ds, host, true) userEnrollment, err = ds.GetNanoMDMUserEnrollment(ctx, host.UUID) require.NoError(t, err) require.NotNil(t, userEnrollment) require.Equal(t, host.UUID, userEnrollment.DeviceID) require.True(t, userEnrollment.Enabled) require.Equal(t, "User", userEnrollment.Type) username, uuid, err = ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, host.UUID) require.NoError(t, err) require.Equal(t, nanoenroll_username, username) require.Equal(t, nanoenroll_useruuid_prefix+host.UUID, uuid) } func testMDMAppleProfileLabels(t *testing.T, ds *Datastore) { ctx := t.Context() // Explicitly set the labelUpdatedAt very slightly in the past for testing dynamic label logic fiveSecondsAgo := time.Now().Add(-5 * time.Second).UTC().Round(time.Microsecond) matchProfiles := func(want, got []*fleet.MDMAppleProfilePayload) { // match only the fields we care about for _, p := range got { assert.NotEmpty(t, p.Checksum) p.Checksum = nil p.SecretsUpdatedAt = nil p.DeviceEnrolledAt = nil } require.ElementsMatch(t, want, got) } globProf1, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "N1", "I1", "z"), nil) require.NoError(t, err) globProf2, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "N2", "I2", "x"), nil) require.NoError(t, err) globalPfs, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) require.NoError(t, err) require.Len(t, globalPfs, 2) // if there are no hosts, then no profilesToInstall need to be installed profilesToInstall, err := ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) require.Empty(t, profilesToInstall) host1, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-uuid-1", TeamID: nil, Platform: "darwin", LabelUpdatedAt: fiveSecondsAgo, }) require.NoError(t, err) // add a user enrollment for this device, nothing else should be modified nanoEnroll(t, ds, host1, true) // non-macOS hosts shouldn't modify any of the results below _, err = ds.NewHost(ctx, &fleet.Host{ Hostname: "test-windows-host", OsqueryHostID: ptr.String("4824"), NodeKey: ptr.String("4824"), UUID: "test-windows-host", TeamID: nil, Platform: "windows", LabelUpdatedAt: fiveSecondsAgo, }) require.NoError(t, err) // a macOS host that's not MDM enrolled into Fleet shouldn't // modify any of the results below _, err = ds.NewHost(ctx, &fleet.Host{ Hostname: "test-non-mdm-host", OsqueryHostID: ptr.String("4825"), NodeKey: ptr.String("4825"), UUID: "test-non-mdm-host", TeamID: nil, Platform: "darwin", LabelUpdatedAt: fiveSecondsAgo, }) require.NoError(t, err) // global profiles to install on the newly added host profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) hostLabel, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host-name-label", OsqueryHostID: ptr.String("1337_label"), NodeKey: ptr.String("1337_label"), UUID: "test-uuid-1-label", TeamID: nil, Platform: "darwin", LabelUpdatedAt: fiveSecondsAgo, }) require.NoError(t, err) // add a user enrollment for this device, nothing else should be modified nanoEnroll(t, ds, hostLabel, true) // include-any labels l1, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-1", Query: "select 1"}) require.NoError(t, err) l2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-2", Query: "select 1"}) require.NoError(t, err) l3, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-3", Query: "select 1"}) require.NoError(t, err) // include-all labels l4, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-4", Query: "select 1"}) require.NoError(t, err) l5, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-5", Query: "select 1"}) require.NoError(t, err) // exclude-any labels l6, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-any-6", Query: "select 1"}) require.NoError(t, err) l7, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-any-7", LabelMembershipType: fleet.LabelMembershipTypeManual}) require.NoError(t, err) profIncludeAny, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-include-any", "prof-include-any", "prof-include-any", l1, l2, l3), nil) require.NoError(t, err) profIncludeAll, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-include-all", "prof-include-all", "prof-include-all", l4, l5), nil) require.NoError(t, err) profExcludeAny, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-exclude-any", "prof-exclude-any", "prof-exclude-any", l6, l7), nil) require.NoError(t, err) profExcludeAnyManualOnly, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "prof-exclude-any-manual", "prof-exclude-any-manual", "prof-exclude-any-manual", l7), nil) require.NoError(t, err) // hostLabel is a member of l1, l4, l5 err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, hostLabel.ID}, {l4.ID, hostLabel.ID}, {l5.ID, hostLabel.ID}}) require.NoError(t, err) globalPfs, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) require.NoError(t, err) require.Len(t, globalPfs, 6) // still the same profiles to assign (plus the one for hostLabel) as there are no profiles for team 1 profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) // Update hosts' labels updated at timestamp so that the exclude any profile with a dynamic label shows up hostLabel.LabelUpdatedAt = time.Now().Add(1 * time.Second) err = ds.UpdateHost(ctx, hostLabel) require.NoError(t, err) host1.LabelUpdatedAt = time.Now().Add(1 * time.Second) err = ds.UpdateHost(ctx, host1) require.NoError(t, err) profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) // Remove the l1<->hostLabel relationship, but add l2<->hostLabel. The profile should still show // up since it's "include any" err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, hostLabel.ID}}) require.NoError(t, err) err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, hostLabel.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAny.ProfileUUID, ProfileIdentifier: profIncludeAny.Identifier, ProfileName: profIncludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) // Remove the l2<->hostLabel relationship. The profie should no longer show up since it's // include-any err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, hostLabel.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profIncludeAll.ProfileUUID, ProfileIdentifier: profIncludeAll.Identifier, ProfileName: profIncludeAll.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) // Remove the l4<->hostLabel relationship. Since the profile is "include-all", it should no longer show // up even though the l5<->hostLabel connection is still there. err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, hostLabel.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) // Add a l6<->host relationship. The dynamic exclude-any profile should no longer be assigned to hostLabel but manual still will err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, hostLabel.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "") require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAny.ProfileUUID, ProfileIdentifier: profExcludeAny.Identifier, ProfileName: profExcludeAny.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: host1.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf1.ProfileUUID, ProfileIdentifier: globProf1.Identifier, ProfileName: globProf1.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: globProf2.ProfileUUID, ProfileIdentifier: globProf2.Identifier, ProfileName: globProf2.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, {ProfileUUID: profExcludeAnyManualOnly.ProfileUUID, ProfileIdentifier: profExcludeAnyManualOnly.Identifier, ProfileName: profExcludeAnyManualOnly.Name, HostUUID: hostLabel.UUID, HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem}, }, profilesToInstall) } func testAggregateMacOSSettingsAllPlatforms(t *testing.T, ds *Datastore) { ctx := t.Context() // Create macOS/iOS/iPadOS devices on "No team". var hosts []*fleet.Host for i, platform := range []string{"darwin", "ios", "ipados"} { host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("hostname_%d", i), DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now().Add(-time.Duration(i) * time.Minute), OsqueryHostID: ptr.String(fmt.Sprintf("osquery-host-id_%d", i)), NodeKey: ptr.String(fmt.Sprintf("node-key_%d", i)), UUID: fmt.Sprintf("uuid_%d", i), HardwareSerial: fmt.Sprintf("serial_%d", i), Platform: platform, }) require.NoError(t, err) nanoEnrollAndSetHostMDMData(t, ds, host, false) hosts = append(hosts, host) } // Create a profile for "No team". cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("foobar", "barfoo", 0), nil) require.NoError(t, err) // Upsert the profile with nil status, should be counted as pending. upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMOperationTypeInstall, nil, ctx, ds, t) res, err := ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) require.NotNil(t, res) require.EqualValues(t, len(hosts), res.Pending) require.EqualValues(t, 0, res.Failed) require.EqualValues(t, 0, res.Verifying) require.EqualValues(t, 0, res.Verified) } func testGetMDMAppleEnrolledDeviceDeletedFromFleet(t *testing.T, ds *Datastore) { ctx := t.Context() // create macOS/iOS/iPadOS enrolled devices and an unenrolled one var hosts []*fleet.Host for i, platform := range []string{"darwin", "ios", "ipados", "linux"} { host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("hostname_%d", i), DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now().Add(-time.Duration(i) * time.Minute), OsqueryHostID: ptr.String(fmt.Sprintf("osquery-host-id_%d", i)), NodeKey: ptr.String(fmt.Sprintf("node-key_%d", i)), UUID: fmt.Sprintf("uuid_%d", i), HardwareSerial: fmt.Sprintf("serial_%d", i), Platform: platform, }) require.NoError(t, err) if platform != "linux" { nanoEnrollAndSetHostMDMData(t, ds, host, false) } hosts = append(hosts, host) } // Create BYOD Personal enrollments for iOS and iPadOS devices for i, platform := range []string{"ios", "ipados"} { host, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("byod_hostname_%d", i), DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now().Add(-time.Duration(i) * time.Minute), OsqueryHostID: ptr.String(fmt.Sprintf("osquery-host-id-byod_%d", i)), NodeKey: ptr.String(fmt.Sprintf("node-key-byod_%d", i)), UUID: fmt.Sprintf("byod_uuid_%d", i), HardwareSerial: fmt.Sprintf("byod_uuid_%d", i), Platform: platform, }) nanoEnrollUserDeviceAndSetHostMDMData(t, ds, host) require.NoError(t, err) hosts = append(hosts, host) } // get for any of those hosts returns not found because the host entry still exists for _, h := range hosts { _, err := ds.GetMDMAppleEnrolledDeviceDeletedFromFleet(ctx, h.UUID) require.Error(t, err) require.True(t, errors.Is(err, sql.ErrNoRows)) } ids, err := ds.ListMDMAppleEnrolledIPhoneIpadDeletedFromFleet(ctx, 10) require.NoError(t, err) require.Len(t, ids, 0) // delete the darwin and ios hosts err = ds.DeleteHost(ctx, hosts[0].ID) require.NoError(t, err) err = ds.DeleteHost(ctx, hosts[1].ID) require.NoError(t, err) // darwin device info can be retrieved info, err := ds.GetMDMAppleEnrolledDeviceDeletedFromFleet(ctx, hosts[0].UUID) require.NoError(t, err) require.Equal(t, hosts[0].UUID, info.ID) require.Equal(t, hosts[0].HardwareSerial, info.SerialNumber) require.Equal(t, hosts[0].Platform, info.Platform) require.NotEmpty(t, info.Authenticate) // ios device info can be retrieved info, err = ds.GetMDMAppleEnrolledDeviceDeletedFromFleet(ctx, hosts[1].UUID) require.NoError(t, err) require.Equal(t, hosts[1].UUID, info.ID) require.Equal(t, hosts[1].HardwareSerial, info.SerialNumber) require.Equal(t, hosts[1].Platform, info.Platform) require.NotEmpty(t, info.Authenticate) // the others still cannot be retrieved (add an invalid host uuid for good measure) for _, huuid := range []string{hosts[2].UUID, hosts[3].UUID, hosts[4].UUID, hosts[5].UUID, uuid.NewString()} { _, err := ds.GetMDMAppleEnrolledDeviceDeletedFromFleet(ctx, huuid) require.Error(t, err) require.True(t, errors.Is(err, sql.ErrNoRows)) } // list returns only 1 because it ignores macOS ids, err = ds.ListMDMAppleEnrolledIPhoneIpadDeletedFromFleet(ctx, 10) require.NoError(t, err) require.Len(t, ids, 1) require.ElementsMatch(t, []string{hosts[1].UUID}, ids) // delete the darwin and ios BYOD hosts err = ds.DeleteHost(ctx, hosts[4].ID) require.NoError(t, err) err = ds.DeleteHost(ctx, hosts[5].ID) require.NoError(t, err) // ios device info can be retrieved info, err = ds.GetMDMAppleEnrolledDeviceDeletedFromFleet(ctx, hosts[4].UUID) require.NoError(t, err) require.Equal(t, hosts[4].UUID, info.ID) require.Equal(t, hosts[4].HardwareSerial, info.SerialNumber) require.Equal(t, hosts[4].Platform, info.Platform) require.NotEmpty(t, info.Authenticate) // iPadOS device info can be retrieved info, err = ds.GetMDMAppleEnrolledDeviceDeletedFromFleet(ctx, hosts[5].UUID) require.NoError(t, err) require.Equal(t, hosts[5].UUID, info.ID) require.Equal(t, hosts[5].HardwareSerial, info.SerialNumber) require.Equal(t, hosts[5].Platform, info.Platform) require.NotEmpty(t, info.Authenticate) // list returns only 3 because it ignores macOS ids, err = ds.ListMDMAppleEnrolledIPhoneIpadDeletedFromFleet(ctx, 10) require.NoError(t, err) require.Len(t, ids, 3) require.ElementsMatch(t, []string{hosts[1].UUID, hosts[4].UUID, hosts[5].UUID}, ids) } func testSetMDMAppleProfilesWithVariables(t *testing.T, ds *Datastore) { ctx := t.Context() tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) require.NoError(t, err) checkProfileVariables := func(profIdent string, teamID uint, wantVars []fleet.FleetVarName) { var gotVars []string ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.SelectContext(ctx, q, &gotVars, ` SELECT fv.name FROM mdm_apple_configuration_profiles macp INNER JOIN mdm_configuration_profile_variables mcpv ON macp.profile_uuid = mcpv.apple_profile_uuid INNER JOIN fleet_variables fv ON mcpv.fleet_variable_id = fv.id WHERE macp.identifier = ? AND macp.team_id = ?`, profIdent, teamID) }) wantVarStrings := make([]string, len(wantVars)) for i := range wantVars { wantVarStrings[i] = "FLEET_VAR_" + string(wantVars[i]) } require.ElementsMatch(t, wantVarStrings, gotVars) } profA := *generateAppleCP("a", "a", 0) profB := *generateAppleCP("b", "b", 0) profC := *generateAppleCP("c", "c", tm1.ID) profD := *generateAppleCP("d", "d", 0) profE := *generateAppleCP("e", "e", tm1.ID) _, err = ds.NewMDMAppleConfigProfile(ctx, profA, nil) require.NoError(t, err) _, err = ds.NewMDMAppleConfigProfile(ctx, profB, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername}) require.NoError(t, err) _, err = ds.NewMDMAppleConfigProfile(ctx, profC, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarCustomSCEPChallengePrefix + "ZZZ"}) require.NoError(t, err) checkProfileVariables(profA.Identifier, 0, []fleet.FleetVarName{}) checkProfileVariables(profB.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername}) checkProfileVariables(profC.Identifier, tm1.ID, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarCustomSCEPChallengePrefix}) // batch-set no team, add a variable to profA (need to change its contents to // force it to be updated), leave profB unchanged profA.Mobileconfig = append(profA.Mobileconfig, '-') updates, err := ds.BatchSetMDMProfiles(ctx, nil, []*fleet.MDMAppleConfigProfile{ &profA, &profB, }, nil, nil, nil, []fleet.MDMProfileIdentifierFleetVariables{ {Identifier: profA.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsernameLocalPart}}, {Identifier: profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername}}, }) require.NoError(t, err) require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true}, updates) checkProfileVariables(profA.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsernameLocalPart}) checkProfileVariables(profB.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername}) // batch-set no team, remove variable from profA, update variable of profB // and add profD. profA.Mobileconfig = append(profA.Mobileconfig, '-') profB.Mobileconfig = append(profB.Mobileconfig, '-') updates, err = ds.BatchSetMDMProfiles(ctx, nil, []*fleet.MDMAppleConfigProfile{ &profA, &profB, &profD, }, nil, nil, nil, []fleet.MDMProfileIdentifierFleetVariables{ {Identifier: profA.Identifier, FleetVariables: nil}, {Identifier: profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}}, {Identifier: profD.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}}, }) require.NoError(t, err) require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true}, updates) checkProfileVariables(profA.Identifier, 0, nil) checkProfileVariables(profB.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}) checkProfileVariables(profD.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarDigiCertDataPrefix}) // batch-set with no changes to no team, just adding Windows profile and // Apple declaration, does also affect variables. updates, err = ds.BatchSetMDMProfiles(ctx, nil, []*fleet.MDMAppleConfigProfile{ &profA, &profB, &profD, }, []*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "W1", "W1"), }, []*fleet.MDMAppleDeclaration{ declForTest("D1", "D1", "foo"), }, nil, []fleet.MDMProfileIdentifierFleetVariables{ {Identifier: profA.Identifier, FleetVariables: nil}, {Identifier: profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}}, {Identifier: profD.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}}, }) require.NoError(t, err) require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true, AppleDeclaration: true, WindowsConfigProfile: true}, updates) checkProfileVariables(profA.Identifier, 0, nil) checkProfileVariables(profB.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}) checkProfileVariables(profD.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarDigiCertDataPrefix}) // batch-set team 1, replace C with profile E. updates, err = ds.BatchSetMDMProfiles(ctx, &tm1.ID, []*fleet.MDMAppleConfigProfile{ &profE, }, nil, nil, nil, []fleet.MDMProfileIdentifierFleetVariables{ {Identifier: profE.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}}, }) require.NoError(t, err) require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true}, updates) checkProfileVariables(profC.Identifier, tm1.ID, nil) checkProfileVariables(profE.Identifier, tm1.ID, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarDigiCertDataPrefix}) // no-team profiles are not affected checkProfileVariables(profA.Identifier, 0, nil) checkProfileVariables(profB.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}) checkProfileVariables(profD.Identifier, 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarDigiCertDataPrefix}) } func testUpdateNanoMDMUserEnrollmentUsername(t *testing.T, ds *Datastore) { ctx := context.Background() host0, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id"), NodeKey: ptr.String("host0-node-key"), UUID: "host0-test-uuid", Hostname: "hostname0", }) require.NoError(t, err) // Another user-enrolled host to verify isolation host1, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host1-osquery-id"), NodeKey: ptr.String("host1-node-key"), UUID: "host1-test-uuid", Hostname: "hostname1", }) require.NoError(t, err) nanoEnroll(t, ds, host0, true) nanoEnroll(t, ds, host1, true) user0, userUUID0, err := ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, host0.UUID) require.NoError(t, err) require.Equal(t, nanoenroll_username, user0) user1, userUUID1, err := ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, host1.UUID) require.NoError(t, err) require.Equal(t, nanoenroll_username, user1) err = ds.UpdateNanoMDMUserEnrollmentUsername(ctx, host0.UUID, userUUID0, "newfleetie") require.NoError(t, err) user0, fetchedUserUUID0, err := ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, host0.UUID) require.NoError(t, err) require.Equal(t, "newfleetie", user0) require.Equal(t, userUUID0, fetchedUserUUID0) user1, fetchedUserUUID1, err := ds.GetNanoMDMUserEnrollmentUsernameAndUUID(ctx, host1.UUID) require.NoError(t, err) require.Equal(t, nanoenroll_username, user1) require.Equal(t, userUUID1, fetchedUserUUID1) } func testGetLatestAppleMDMCommandOfType(t *testing.T, ds *Datastore) { ctx := t.Context() // Fails if host does not have a record _, err := ds.GetLatestAppleMDMCommandOfType(ctx, "non-existing-uuid", "DeviceLock") require.Error(t, err) require.True(t, errors.Is(err, sql.ErrNoRows)) // Nano enroll a single device realHostUUID := uuid.NewString() host := &fleet.Host{ UUID: realHostUUID, HardwareSerial: "serial", Platform: "darwin", TeamID: nil, } nanoEnroll(t, ds, host, false) // Insert one record deviceLockCommandUUID := uuid.NewString() requestType := "DeviceLock" insertIntoNanoViewQueue(t, ds, host.UUID, deviceLockCommandUUID, requestType) // Fails if host does exist but not request type _, err = ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, "EnableLostMode") require.Error(t, err) require.True(t, errors.Is(err, sql.ErrNoRows)) // Succeeds if host and request type exist cmd, err := ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, requestType) require.NoError(t, err) require.Equal(t, deviceLockCommandUUID, cmd.CommandUUID) require.Equal(t, requestType, cmd.RequestType) } // insertIntoNanoViewQueue is a helper function that populates the entries that nano_view_queue is made up of. func insertIntoNanoViewQueue(t *testing.T, ds *Datastore, hostUUID, commandUUID, requestType string) { ctx := t.Context() ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { // Insert into nano_commands _, err := q.ExecContext(ctx, `INSERT INTO nano_commands (command_uuid, request_type, command, subtype) VALUES (?, ?, '