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 (?, ?, '