mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42327 We're not doing Windows because we're missing the failed activity for Windows profiles, which we do have for Apple. The actual code change is small. This PR is mostly test changes. ## Demo video and docs https://www.youtube.com/watch?v=YKNguaQQs_E https://github.com/fleetdm/fleet/pull/42332/changes # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Apple device configuration profiles (macOS, iOS, iPadOS) now automatically retry failed deliveries up to 3 times instead of once. * Windows configuration profiles maintain their existing single retry limit. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
11392 lines
448 KiB
Go
11392 lines
448 KiB
Go
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"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/VividCortex/mysqlerr"
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"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/platform/logging"
|
|
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
|
"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},
|
|
{"TestMDMAppleHostsDiskEncryption", testMDMAppleHostsDiskEncryption},
|
|
{"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},
|
|
{"TestOrphanMDMCommandRef", testOrphanMDMCommandRef},
|
|
{"TestGetLatestAppleMDMCommandOfType", testGetLatestAppleMDMCommandOfType},
|
|
{"TestSetLockCommandForLostModeCheckin", testSetLockCommandForLostModeCheckin},
|
|
{"DeviceLocation", testDeviceLocation},
|
|
{"TestGetDEPAssignProfileExpiredCooldowns", testGetDEPAssignProfileExpiredCooldowns},
|
|
{"DeleteMDMAppleDeclarationByNameCancelsInstalls", testDeleteMDMAppleDeclarationByNameCancelsInstalls},
|
|
{"RecoveryLockPasswordSetAndGet", testRecoveryLockPasswordSetAndGet},
|
|
{"RecoveryLockPasswordBulkSet", testRecoveryLockPasswordBulkSet},
|
|
{"RecoveryLockPasswordGetNotFound", testRecoveryLockPasswordGetNotFound},
|
|
{"RecoveryLockPasswordSetOverwrite", testRecoveryLockPasswordSetOverwrite},
|
|
{"RecoveryLockPasswordUpdatedAtChanges", testRecoveryLockPasswordUpdatedAtChanges},
|
|
{"RecoveryLockStatusMethods", testRecoveryLockStatusMethods},
|
|
{"GetHostsForRecoveryLockAction", testGetHostsForRecoveryLockAction},
|
|
{"GetHostRecoveryLockPasswordStatus", testGetHostRecoveryLockPasswordStatus},
|
|
{"ClaimHostsForRecoveryLockClear", testClaimHostsForRecoveryLockClear},
|
|
{"RecoveryLockRotation", testRecoveryLockRotation},
|
|
}
|
|
|
|
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("<Replace></Replace>")}, 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("<Replace></Replace>")}, 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("<Replace></Replace>")}, 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("<Replace></Replace>")}, 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(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array/>
|
|
<key>PayloadDisplayName</key>
|
|
<string>%s</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>%s</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>%s</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</plist>
|
|
`, 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', '<?xml')
|
|
`)
|
|
require.NoError(t, err)
|
|
|
|
// 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)
|
|
profilesToRemove, err := ds.ListMDMAppleProfilesToRemove(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, profilesToRemove)
|
|
// Both should return empty
|
|
matchCombinedProfiles(profilesToInstall, profilesToRemove)
|
|
|
|
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",
|
|
})
|
|
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",
|
|
})
|
|
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",
|
|
})
|
|
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: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
matchCombinedProfiles(profilesToInstall, profilesToRemove)
|
|
|
|
// add another host, it belongs to a team
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
|
|
require.NoError(t, err)
|
|
host2, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "test-host2-name",
|
|
OsqueryHostID: ptr.String("1338"),
|
|
NodeKey: ptr.String("1338"),
|
|
UUID: "test-uuid-2",
|
|
TeamID: &team.ID,
|
|
Platform: "darwin",
|
|
})
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, host2, false) // no user-channel enrollment for this host yet
|
|
|
|
profiles, err := ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
|
|
// all profiles of host1 for now, host2 has no profiles yet
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profiles)
|
|
|
|
matchCombinedProfiles(profiles, profilesToRemove)
|
|
|
|
// assign profiles to team 1
|
|
teamProfiles := []*fleet.MDMAppleConfigProfile{
|
|
scopedConfigProfileForTest(t, "N4", "I4", "x", fleet.PayloadScopeSystem),
|
|
scopedConfigProfileForTest(t, "N5", "I5", "y", fleet.PayloadScopeUser),
|
|
}
|
|
err = ds.BatchSetMDMAppleProfiles(ctx, &team.ID, teamProfiles)
|
|
require.NoError(t, err)
|
|
|
|
globalPfs, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0))
|
|
require.NoError(t, err)
|
|
require.Len(t, globalPfs, 3)
|
|
teamPfs, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(1))
|
|
require.NoError(t, err)
|
|
require.Len(t, teamPfs, 2)
|
|
|
|
// new profiles, this time for host2 belonging to team 1
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
matchCombinedProfiles(profilesToInstall, profilesToRemove)
|
|
|
|
// create the user enrollment for host2
|
|
nanoEnrollUserOnly(t, ds, host2)
|
|
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
matchCombinedProfiles(profilesToInstall, profilesToRemove)
|
|
|
|
// add another global host
|
|
host3, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "test-host3-name",
|
|
OsqueryHostID: ptr.String("1339"),
|
|
NodeKey: ptr.String("1339"),
|
|
UUID: "test-uuid-3",
|
|
TeamID: nil,
|
|
Platform: "darwin",
|
|
})
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, host3, false) // no user enrollment
|
|
|
|
// global profiles to install on host3
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
matchCombinedProfiles(profilesToInstall, profilesToRemove)
|
|
|
|
// cron runs and updates the status
|
|
err = ds.BulkUpsertMDMAppleHostProfiles(
|
|
ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
{
|
|
ProfileUUID: globalPfs[0].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[0].Identifier,
|
|
ProfileName: globalPfs[0].Name,
|
|
Checksum: globalProfiles[0].Checksum,
|
|
HostUUID: "test-uuid-1",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[0].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[0].Identifier,
|
|
ProfileName: globalPfs[0].Name,
|
|
Checksum: globalProfiles[0].Checksum,
|
|
HostUUID: "test-uuid-3",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[1].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[1].Identifier,
|
|
ProfileName: globalPfs[1].Name,
|
|
Checksum: globalProfiles[1].Checksum,
|
|
HostUUID: "test-uuid-1",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[1].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[1].Identifier,
|
|
ProfileName: globalPfs[1].Name,
|
|
Checksum: globalProfiles[1].Checksum,
|
|
HostUUID: "test-uuid-3",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[2].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[2].Identifier,
|
|
ProfileName: globalPfs[2].Name,
|
|
Checksum: globalProfiles[2].Checksum,
|
|
HostUUID: "test-uuid-1",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
{
|
|
ProfileUUID: teamPfs[0].ProfileUUID,
|
|
ProfileIdentifier: teamPfs[0].Identifier,
|
|
ProfileName: teamPfs[0].Name,
|
|
Checksum: teamProfiles[0].Checksum,
|
|
HostUUID: "test-uuid-2",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: teamPfs[1].ProfileUUID,
|
|
ProfileIdentifier: teamPfs[1].Identifier,
|
|
ProfileName: teamPfs[1].Name,
|
|
Checksum: teamProfiles[1].Checksum,
|
|
HostUUID: "test-uuid-2",
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// user profile N3 on host3 is still pending install (no user enrollment)
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
// no profiles to remove yet
|
|
toRemove, err := ds.ListMDMAppleProfilesToRemove(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, toRemove)
|
|
|
|
matchCombinedProfiles(profilesToInstall, toRemove)
|
|
|
|
// set host1 and host 3 to verified status, leave host2 as verifying
|
|
verified1 := []*fleet.HostMacOSProfile{
|
|
{Identifier: globalPfs[0].Identifier, DisplayName: globalPfs[0].Name, InstallDate: time.Now()},
|
|
{Identifier: globalPfs[1].Identifier, DisplayName: globalPfs[1].Name, InstallDate: time.Now()},
|
|
{Identifier: globalPfs[2].Identifier, DisplayName: globalPfs[2].Name, InstallDate: time.Now()},
|
|
}
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, host1, profilesByIdentifier(verified1)))
|
|
verified3 := []*fleet.HostMacOSProfile{
|
|
{Identifier: globalPfs[0].Identifier, DisplayName: globalPfs[0].Name, InstallDate: time.Now()},
|
|
{Identifier: globalPfs[1].Identifier, DisplayName: globalPfs[1].Name, InstallDate: time.Now()},
|
|
}
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, host3, profilesByIdentifier(verified3)))
|
|
|
|
// still no profiles to remove
|
|
toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, toRemove)
|
|
|
|
// add host1 to team
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID}))
|
|
require.NoError(t, err)
|
|
|
|
// simulate an update that bulk-sets the no team's profiles to NULL pending
|
|
_, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// profiles to be added are host1's team profiles and the user profile for host3
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
// profiles to be removed includes host1's old profiles
|
|
toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx)
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{
|
|
ProfileUUID: globalPfs[0].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[0].Identifier,
|
|
ProfileName: globalPfs[0].Name,
|
|
Status: &fleet.MDMDeliveryVerified,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
HostUUID: "test-uuid-1",
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[1].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[1].Identifier,
|
|
ProfileName: globalPfs[1].Name,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryVerified,
|
|
HostUUID: "test-uuid-1",
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[2].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[2].Identifier,
|
|
ProfileName: globalPfs[2].Name,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryVerified,
|
|
HostUUID: "test-uuid-1",
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
}, toRemove)
|
|
|
|
matchCombinedProfiles(profilesToInstall, toRemove)
|
|
|
|
// update host3's device enrollment so that it looks like it was enrolled a day ago
|
|
setNanoDeviceEnrolledAt(t, ds, host3, time.Now().Add(-24*time.Hour))
|
|
|
|
// user-scoped profile for host3 is still listed as to install
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
// delete global profile N3 (the user-scoped one)
|
|
err = ds.BatchSetMDMAppleProfiles(ctx, nil, globalProfiles[:2])
|
|
require.NoError(t, err)
|
|
|
|
profilesToInstall, err = ds.ListMDMAppleProfilesToInstall(ctx, "")
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin", Scope: fleet.PayloadScopeUser},
|
|
}, profilesToInstall)
|
|
|
|
// the to remove profiles are the same but N3 is now pending remove for host1
|
|
toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx)
|
|
require.NoError(t, err)
|
|
matchProfiles([]*fleet.MDMAppleProfilePayload{
|
|
{
|
|
ProfileUUID: globalPfs[0].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[0].Identifier,
|
|
ProfileName: globalPfs[0].Name,
|
|
Status: &fleet.MDMDeliveryVerified,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
HostUUID: "test-uuid-1",
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[1].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[1].Identifier,
|
|
ProfileName: globalPfs[1].Name,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryVerified,
|
|
HostUUID: "test-uuid-1",
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: globalPfs[2].ProfileUUID,
|
|
ProfileIdentifier: globalPfs[2].Identifier,
|
|
ProfileName: globalPfs[2].Name,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: nil,
|
|
HostUUID: "test-uuid-1",
|
|
CommandUUID: "command-uuid",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
}, toRemove)
|
|
|
|
matchCombinedProfiles(profilesToInstall, toRemove)
|
|
}
|
|
|
|
// checkMDMHostRelatedTables checks that rows are inserted for new MDM hosts in
|
|
// each of host_display_names, host_seen_times, and label_membership. Note that
|
|
// related tables records for pre-existing hosts are created outside of the MDM
|
|
// enrollment flows so they are not checked in some tests above (e.g.,
|
|
// testIngestMDMAppleHostAlreadyExistsInFleet)
|
|
func checkMDMHostRelatedTables(t *testing.T, ds *Datastore, hostID uint, expectedSerial string, expectedModel string) {
|
|
var displayName string
|
|
err := sqlx.GetContext(t.Context(), ds.reader(t.Context()), &displayName, `SELECT display_name FROM host_display_names WHERE host_id = ?`, hostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fmt.Sprintf("%s (%s)", expectedModel, expectedSerial), displayName)
|
|
|
|
var labelsOK []bool
|
|
err = sqlx.SelectContext(t.Context(), ds.reader(t.Context()), &labelsOK, `SELECT 1 FROM label_membership WHERE host_id = ?`, hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, labelsOK, 2)
|
|
require.True(t, labelsOK[0])
|
|
require.True(t, labelsOK[1])
|
|
|
|
appCfg, err := ds.AppConfig(t.Context())
|
|
require.NoError(t, err)
|
|
var hmdm fleet.HostMDM
|
|
err = sqlx.GetContext(t.Context(), ds.reader(t.Context()), &hmdm, `SELECT host_id, server_url, mdm_id FROM host_mdm WHERE host_id = ?`, hostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, hostID, hmdm.HostID)
|
|
serverURL, err := apple_mdm.ResolveAppleMDMURL(appCfg.ServerSettings.ServerURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, serverURL, hmdm.ServerURL)
|
|
require.NotEmpty(t, hmdm.MDMID)
|
|
|
|
var mdmSolution fleet.MDMSolution
|
|
err = sqlx.GetContext(t.Context(), ds.reader(t.Context()), &mdmSolution, `SELECT name, server_url FROM mobile_device_management_solutions WHERE id = ?`, hmdm.MDMID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, mdmSolution.Name)
|
|
require.Equal(t, serverURL, mdmSolution.ServerURL)
|
|
}
|
|
|
|
func testGetMDMAppleProfilesContents(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
profiles := []*fleet.MDMAppleConfigProfile{
|
|
configProfileForTest(t, "N1", "I1", "z"),
|
|
configProfileForTest(t, "N2", "I2", "b"),
|
|
configProfileForTest(t, "N3", "I3", "c"),
|
|
}
|
|
err := ds.BatchSetMDMAppleProfiles(ctx, nil, profiles)
|
|
require.NoError(t, err)
|
|
|
|
profiles, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0))
|
|
require.NoError(t, err)
|
|
|
|
cases := []struct {
|
|
uuids []string
|
|
want map[string]mobileconfig.Mobileconfig
|
|
}{
|
|
{[]string{}, nil},
|
|
{nil, nil},
|
|
{[]string{profiles[0].ProfileUUID}, map[string]mobileconfig.Mobileconfig{profiles[0].ProfileUUID: profiles[0].Mobileconfig}},
|
|
{
|
|
[]string{profiles[0].ProfileUUID, profiles[1].ProfileUUID, profiles[2].ProfileUUID},
|
|
map[string]mobileconfig.Mobileconfig{
|
|
profiles[0].ProfileUUID: profiles[0].Mobileconfig,
|
|
profiles[1].ProfileUUID: profiles[1].Mobileconfig,
|
|
profiles[2].ProfileUUID: profiles[2].Mobileconfig,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
out, err := ds.GetMDMAppleProfilesContents(ctx, c.uuids)
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.want, out)
|
|
}
|
|
}
|
|
|
|
// createBuiltinLabels creates entries for "All Hosts" and "macOS" labels, which are assumed to be
|
|
// extant for MDM flows
|
|
func createBuiltinLabels(t *testing.T, ds *Datastore) {
|
|
// Labels are deleted when truncating tables in between tests.
|
|
// We need to delete the iOS/iPadOS labels because these two are created on a table migration,
|
|
// and also we want to keep their indexes higher than "All Hosts" and "macOS" (to not break existing tests).
|
|
_, err := ds.writer(t.Context()).Exec(`
|
|
DELETE FROM labels WHERE name = 'iOS' OR name = 'iPadOS'`,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(t.Context()).Exec(`
|
|
INSERT INTO labels (
|
|
name,
|
|
description,
|
|
query,
|
|
platform,
|
|
label_type
|
|
) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`,
|
|
"All Hosts",
|
|
"",
|
|
"",
|
|
"",
|
|
fleet.LabelTypeBuiltIn,
|
|
"macOS",
|
|
"",
|
|
"",
|
|
"",
|
|
fleet.LabelTypeBuiltIn,
|
|
"iOS",
|
|
"",
|
|
"",
|
|
"",
|
|
fleet.LabelTypeBuiltIn,
|
|
"iPadOS",
|
|
"",
|
|
"",
|
|
"",
|
|
fleet.LabelTypeBuiltIn,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func nanoEnrollAndSetHostMDMData(t *testing.T, ds *Datastore, host *fleet.Host, withUser bool) {
|
|
ctx := t.Context()
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
expectedMDMServerURL, err := apple_mdm.ResolveAppleEnrollMDMURL(ac.ServerSettings.ServerURL)
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, host, withUser)
|
|
err = ds.SetOrUpdateMDMData(ctx, host.ID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func nanoEnrollUserDeviceAndSetHostMDMData(t *testing.T, ds *Datastore, host *fleet.Host) {
|
|
ctx := t.Context()
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
expectedMDMServerURL, err := apple_mdm.ResolveAppleEnrollMDMURL(ac.ServerSettings.ServerURL)
|
|
require.NoError(t, err)
|
|
nanoEnrollUserDevice(t, ds, host)
|
|
err = ds.SetOrUpdateMDMData(ctx, host.ID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Creates a "User Enrollment (Device)" enrollment for a given host simulating a BYOD account-driven
|
|
// user enrollment device.
|
|
func nanoEnrollUserDevice(t *testing.T, ds *Datastore, host *fleet.Host) {
|
|
_, err := ds.writer(t.Context()).Exec(`INSERT INTO nano_devices (id, serial_number, authenticate, platform, enroll_team_id) VALUES (?, NULLIF(?, ''), 'test', ?, ?)`, host.UUID, host.UUID, host.Platform, host.TeamID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(t.Context()).Exec(`
|
|
INSERT INTO nano_enrollments
|
|
(id, device_id, user_id, type, topic, push_magic, token_hex, token_update_tally, last_seen_at)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
host.UUID,
|
|
host.UUID,
|
|
nil,
|
|
"User Enrollment (Device)",
|
|
host.UUID+".topic",
|
|
host.UUID+".magic",
|
|
host.UUID,
|
|
1,
|
|
time.Now().Add(-2*time.Second).Truncate(time.Second),
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func nanoEnroll(t *testing.T, ds *Datastore, host *fleet.Host, withUser bool) {
|
|
_, err := ds.writer(t.Context()).Exec(`INSERT INTO nano_devices (id, serial_number, authenticate, platform, enroll_team_id) VALUES (?, NULLIF(?, ''), 'test', ?, ?)`, host.UUID, host.HardwareSerial, host.Platform, host.TeamID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(t.Context()).Exec(`
|
|
INSERT INTO nano_enrollments
|
|
(id, device_id, user_id, type, topic, push_magic, token_hex, token_update_tally, last_seen_at)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
host.UUID,
|
|
host.UUID,
|
|
nil,
|
|
"Device",
|
|
host.UUID+".topic",
|
|
host.UUID+".magic",
|
|
host.UUID,
|
|
1,
|
|
time.Now().Add(-2*time.Second).Truncate(time.Second),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
if withUser {
|
|
nanoEnrollUserOnly(t, ds, host)
|
|
}
|
|
}
|
|
|
|
const (
|
|
nanoenroll_username = "alice"
|
|
nanoenroll_useruuid_prefix = "useruuid-"
|
|
)
|
|
|
|
func nanoEnrollUserOnly(t *testing.T, ds *Datastore, host *fleet.Host) {
|
|
// Nano userIDs are a combination of the host UUID and a user UUID from the system. We will set
|
|
// it to something predictable here so tests can assert on the behavior
|
|
userID := host.UUID + ":" + nanoenroll_useruuid_prefix + host.UUID
|
|
|
|
_, err := ds.writer(t.Context()).Exec(`
|
|
INSERT INTO nano_users
|
|
(id, device_id, user_short_name, user_long_name)
|
|
VALUES
|
|
(?, ?, ?, ?)`,
|
|
userID,
|
|
host.UUID,
|
|
"alice",
|
|
"alice "+userID,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(t.Context()).Exec(`
|
|
INSERT INTO nano_enrollments
|
|
(id, device_id, user_id, type, topic, push_magic, token_hex, last_seen_at)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
userID,
|
|
host.UUID,
|
|
userID,
|
|
"User",
|
|
host.UUID+".topic",
|
|
host.UUID+".magic",
|
|
host.UUID,
|
|
time.Now().Add(-2*time.Second).Truncate(time.Second),
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func setNanoDeviceEnrolledAt(t *testing.T, ds *Datastore, host *fleet.Host, enrolledAt time.Time) {
|
|
_, err := ds.writer(t.Context()).Exec(`
|
|
UPDATE nano_devices
|
|
SET authenticate_at = ?
|
|
WHERE id = ?`, enrolledAt, host.UUID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func upsertHostCPs(
|
|
hosts []*fleet.Host,
|
|
profiles []*fleet.MDMAppleConfigProfile,
|
|
opType fleet.MDMOperationType,
|
|
status *fleet.MDMDeliveryStatus,
|
|
ctx context.Context,
|
|
ds *Datastore,
|
|
t *testing.T,
|
|
) {
|
|
upserts := []*fleet.MDMAppleBulkUpsertHostProfilePayload{}
|
|
for _, h := range hosts {
|
|
for _, cp := range profiles {
|
|
csum := []byte("csum")
|
|
if cp.Checksum != nil {
|
|
csum = cp.Checksum
|
|
}
|
|
payload := fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
ProfileUUID: cp.ProfileUUID,
|
|
ProfileIdentifier: cp.Identifier,
|
|
ProfileName: cp.Name,
|
|
HostUUID: h.UUID,
|
|
CommandUUID: "",
|
|
OperationType: opType,
|
|
Status: status,
|
|
Checksum: csum,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
}
|
|
upserts = append(upserts, &payload)
|
|
}
|
|
}
|
|
err := ds.BulkUpsertMDMAppleHostProfiles(ctx, upserts)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
checkListHosts := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
|
expectedIDs := []uint{}
|
|
for _, h := range expected {
|
|
expectedIDs = append(expectedIDs, h.ID)
|
|
}
|
|
|
|
gotHosts, err := ds.ListHosts(
|
|
ctx,
|
|
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}},
|
|
fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID},
|
|
)
|
|
gotIDs := []uint{}
|
|
for _, h := range gotHosts {
|
|
gotIDs = append(gotIDs, h.ID)
|
|
}
|
|
|
|
return assert.NoError(t, err) &&
|
|
assert.Len(t, gotHosts, len(expected)) &&
|
|
assert.ElementsMatch(t, expectedIDs, gotIDs)
|
|
}
|
|
|
|
var hosts []*fleet.Host
|
|
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)
|
|
nanoEnrollAndSetHostMDMData(t, ds, h, false)
|
|
}
|
|
|
|
// create somes config profiles for no team
|
|
var noTeamCPs []*fleet.MDMAppleConfigProfile
|
|
for i := 0; i < 10; i++ {
|
|
cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fmt.Sprintf("name%d", i), fmt.Sprintf("identifier%d", i), 0), nil)
|
|
require.NoError(t, err)
|
|
noTeamCPs = append(noTeamCPs, cp)
|
|
}
|
|
// add filevault profile for no team
|
|
fvNoTeam, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("filevault", "com.fleetdm.fleet.mdm.filevault", 0), nil)
|
|
require.NoError(t, err)
|
|
|
|
// upsert all host profiles with nil status, counts all as pending
|
|
upsertHostCPs(hosts, append(noTeamCPs, fvNoTeam), 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.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// upsert all but filevault to verifying
|
|
upsertHostCPs(hosts, noTeamCPs, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts), res.Pending) // still pending because filevault not installed
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// upsert all but filevault to verified
|
|
upsertHostCPs(hosts, noTeamCPs, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerified, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts), res.Pending) // still pending because filevault not installed
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// upsert filevault to pending
|
|
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{fvNoTeam}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryPending, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts), res.Pending) // still pending because filevault pending
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{fvNoTeam}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts), res.Pending) // still pending because no disk encryption key
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "foo", "", nil)
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
// hosts still pending because disk encryption key decryptable is not set
|
|
require.EqualValues(t, len(hosts)-1, res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
// one host is verifying because the disk is encrypted and we're verifying the key
|
|
require.Equal(t, uint(1), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[0].ID}, false, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts), res.Pending) // still pending because disk encryption key decryptable is false
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[0].ID}, true, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-1, res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(1), res.Verifying) // hosts[0] now has filevault fully enforced but not verified
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// upsert hosts[0] filevault to verified
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[0], profilesByIdentifier([]*fleet.HostMacOSProfile{{Identifier: fvNoTeam.Identifier, DisplayName: fvNoTeam.Name, InstallDate: time.Now()}})))
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-1, res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(1), res.Verified) // hosts[0] now has filevault fully enforced and verified
|
|
|
|
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1], "bar", "", nil)
|
|
require.NoError(t, err)
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, false, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-1, res.Pending) // hosts[1] still pending because disk encryption key decryptable is false
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(1), res.Verified)
|
|
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, true, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-2, res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(1), res.Verifying) // hosts[1] now has filevault fully enforced
|
|
require.Equal(t, uint(1), res.Verified)
|
|
|
|
// check that list hosts by status matches summary
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[1:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, hosts[0:1]))
|
|
|
|
// create a team
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
|
require.NoError(t, err)
|
|
|
|
// add hosts[9] to team
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{hosts[9].ID}))
|
|
require.NoError(t, err)
|
|
|
|
// remove profiles from hosts[9]
|
|
upsertHostCPs(hosts[9:10], append(noTeamCPs, fvNoTeam), fleet.MDMOperationTypeRemove, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &team.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying) // remove operations aren't currently subject to verification and only pending/failed removals are counted in summary
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// create somes config profiles for team
|
|
var teamCPs []*fleet.MDMAppleConfigProfile
|
|
for i := 0; i < 2; i++ {
|
|
cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fmt.Sprintf("name%d", i), fmt.Sprintf("identifier%d", i), team.ID), nil)
|
|
require.NoError(t, err)
|
|
teamCPs = append(teamCPs, cp)
|
|
}
|
|
// add filevault profile for team
|
|
fvTeam, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fleetmdm.FleetFileVaultProfileName, mobileconfig.FleetFileVaultPayloadIdentifier, team.ID), nil)
|
|
require.NoError(t, err)
|
|
|
|
upsertHostCPs(hosts[9:10], append(teamCPs, fvTeam), fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &team.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(1), res.Pending) // hosts[9] is pending because it has no disk encryption key
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9], "baz", "", nil)
|
|
require.NoError(t, err)
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &team.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(1), res.Verifying) // hosts[9] now has filevault fully enforced but still verifying
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// check that list hosts by status matches summary
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{}))
|
|
|
|
upsertHostCPs(hosts[9:10], append(teamCPs, fvTeam), fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerified, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &team.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(1), res.Verified) // hosts[9] now has filevault fully enforced and verified
|
|
|
|
// set decryptable to false for hosts[9]
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, false, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &team.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(1), res.Pending) // hosts[9] is pending because it has no disk encryption key even though it was previously verified
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
|
|
// check that list hosts by status matches summary
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{}))
|
|
|
|
// set decryptable back to true for hosts[9]
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour))
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &team.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(1), res.Verified) // hosts[9] goes back to verified
|
|
|
|
// check that list hosts by status matches summary
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, hosts[9:10]))
|
|
}
|
|
|
|
func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
checkFilterHostsByMacOSSettings := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
|
expectedIDs := []uint{}
|
|
for _, h := range expected {
|
|
expectedIDs = append(expectedIDs, h.ID)
|
|
}
|
|
|
|
// check that list hosts by macos settings status matches summary
|
|
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID})
|
|
gotIDs := []uint{}
|
|
for _, h := range gotHosts {
|
|
gotIDs = append(gotIDs, h.ID)
|
|
}
|
|
|
|
return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs)
|
|
}
|
|
|
|
// check that list hosts by os settings status matches summary
|
|
checkFilterHostsByOSSettings := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
|
expectedIDs := []uint{}
|
|
for _, h := range expected {
|
|
expectedIDs = append(expectedIDs, h.ID)
|
|
}
|
|
|
|
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{OSSettingsFilter: status, TeamFilter: teamID})
|
|
gotIDs := []uint{}
|
|
for _, h := range gotHosts {
|
|
gotIDs = append(gotIDs, h.ID)
|
|
}
|
|
|
|
return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs)
|
|
}
|
|
|
|
checkListHosts := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
|
return checkFilterHostsByMacOSSettings(status, teamID, expected) && checkFilterHostsByOSSettings(status, teamID, expected)
|
|
}
|
|
|
|
var hosts []*fleet.Host
|
|
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)
|
|
nanoEnrollAndSetHostMDMData(t, ds, h, false)
|
|
}
|
|
|
|
// create somes config profiles for no team
|
|
var noTeamCPs []*fleet.MDMAppleConfigProfile
|
|
for i := 0; i < 10; i++ {
|
|
cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fmt.Sprintf("name%d", i), fmt.Sprintf("identifier%d", i), 0), nil)
|
|
require.NoError(t, err)
|
|
noTeamCPs = append(noTeamCPs, cp)
|
|
}
|
|
|
|
// all hosts nil status (pending install) for all profiles
|
|
upsertHostCPs(hosts, noTeamCPs, 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) // each host only counts once
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// all hosts pending install of all profiles
|
|
upsertHostCPs(hosts, noTeamCPs, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryPending, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts), res.Pending) // each host only counts once
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// hosts[0] and hosts[1] failed one profile
|
|
upsertHostCPs(hosts[0:2], noTeamCPs[0:1], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryFailed, ctx, ds, t)
|
|
// hosts[0] and hosts[1] have one profile pending as nil
|
|
upsertHostCPs(hosts[0:2], noTeamCPs[3:4], fleet.MDMOperationTypeInstall, nil, ctx, ds, t)
|
|
// hosts[0] also failed another profile
|
|
upsertHostCPs(hosts[0:1], noTeamCPs[1:2], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryFailed, ctx, ds, t)
|
|
// hosts[4] has all profiles reported as nil (pending)
|
|
upsertHostCPs(hosts[4:5], noTeamCPs, fleet.MDMOperationTypeInstall, nil, ctx, ds, t)
|
|
// hosts[5] has one profile reported as nil (pending)
|
|
upsertHostCPs(hosts[5:6], noTeamCPs[0:1], fleet.MDMOperationTypeInstall, nil, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-2, res.Pending) // two hosts are failing at least one profile (hosts[0] and hosts[1])
|
|
require.Equal(t, uint(2), res.Failed) // only count one failure per host (hosts[0] failed two profiles but only counts once)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// hosts[0:3] installed a third profile
|
|
upsertHostCPs(hosts[0:3], noTeamCPs[2:3], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-2, res.Pending) // no change
|
|
require.Equal(t, uint(2), res.Failed) // no change
|
|
require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// hosts[6] deletes all its profiles
|
|
tx, err := ds.writer(ctx).BeginTxx(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.deleteMDMOSCustomSettingsForHost(ctx, tx, hosts[6].UUID, "darwin"))
|
|
require.NoError(t, tx.Commit())
|
|
pendingHosts := hosts[2:6:6]
|
|
pendingHosts = append(pendingHosts, hosts[7:]...)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-3, res.Pending) // hosts[6] not reported here anymore
|
|
require.Equal(t, uint(2), res.Failed) // no change
|
|
require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// hosts[9] installed all profiles but one is with status nil (pending)
|
|
upsertHostCPs(hosts[9:10], noTeamCPs[:9], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
upsertHostCPs(hosts[9:10], noTeamCPs[9:10], fleet.MDMOperationTypeInstall, nil, ctx, ds, t)
|
|
pendingHosts = hosts[2:6:6]
|
|
pendingHosts = append(pendingHosts, hosts[7:]...)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-3, res.Pending) // hosts[6] not reported here anymore, hosts[9] still pending
|
|
require.Equal(t, uint(2), res.Failed) // no change
|
|
require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// hosts[9] installed all profiles
|
|
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
pendingHosts = hosts[2:6:6]
|
|
pendingHosts = append(pendingHosts, hosts[7:9]...)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.EqualValues(t, len(hosts)-4, res.Pending) // subtract hosts[6 and 9] from pending
|
|
require.Equal(t, uint(2), res.Failed) // no change
|
|
require.Equal(t, uint(1), res.Verifying) // add one host that has installed all profiles
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
// create a team
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "rocket"})
|
|
require.NoError(t, err)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID) // get summary new team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending) // no profiles yet
|
|
require.Equal(t, uint(0), res.Failed) // no profiles yet
|
|
require.Equal(t, uint(0), res.Verifying) // no profiles yet
|
|
require.Equal(t, uint(0), res.Verified) // no profiles yet
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
|
|
|
// transfer hosts[9] to new team
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hosts[9].ID}))
|
|
require.NoError(t, err)
|
|
// remove all no team profiles from hosts[9]
|
|
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMOperationTypeRemove, &fleet.MDMDeliveryPending, ctx, ds, t)
|
|
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
pendingHosts = hosts[2:6:6]
|
|
pendingHosts = append(pendingHosts, hosts[7:9]...)
|
|
require.EqualValues(t, len(hosts)-4, res.Pending) // hosts[9] is still not pending, transferred to team
|
|
require.Equal(t, uint(2), res.Failed) // no change
|
|
require.Equal(t, uint(0), res.Verifying) // hosts[9] was transferred so this is now zero
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID) // get summary for new team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(1), res.Pending) // hosts[9] is pending removal of old profiles
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
|
|
|
// create somes config profiles for the new team
|
|
var teamCPs []*fleet.MDMAppleConfigProfile
|
|
for i := 0; i < 10; i++ {
|
|
cp, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fmt.Sprintf("name%d", i), fmt.Sprintf("identifier%d", i), tm.ID), nil)
|
|
require.NoError(t, err)
|
|
teamCPs = append(teamCPs, cp)
|
|
}
|
|
|
|
// install all team profiles on hosts[9]
|
|
upsertHostCPs(hosts[9:10], teamCPs, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(1), res.Pending) // hosts[9] is still pending removal of old profiles
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
|
|
|
// hosts[9] successfully removed old profiles
|
|
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMOperationTypeRemove, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(1), res.Verifying) // hosts[9] is verifying all new profiles
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
|
|
|
// verify one profile on hosts[9]
|
|
upsertHostCPs(hosts[9:10], teamCPs[0:1], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerified, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(1), res.Verifying) // hosts[9] is still verifying other profiles
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
|
|
|
// verify the other profiles on hosts[9]
|
|
upsertHostCPs(hosts[9:10], teamCPs[1:], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerified, ctx, ds, t)
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, uint(0), res.Pending)
|
|
require.Equal(t, uint(0), res.Failed)
|
|
require.Equal(t, uint(0), res.Verifying)
|
|
require.Equal(t, uint(1), res.Verified) // hosts[9] is all verified
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, hosts[9:10]))
|
|
|
|
// confirm no changes in summary for profiles with no team
|
|
res, err = ds.GetMDMAppleProfilesSummary(ctx, ptr.Uint(0)) // team id zero represents no team
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
pendingHosts = hosts[2:6:6]
|
|
pendingHosts = append(pendingHosts, hosts[7:9]...)
|
|
require.EqualValues(t, len(hosts)-4, res.Pending) // subtract two failed hosts, one without profiles and hosts[9] transferred
|
|
require.Equal(t, uint(2), res.Failed) // two failed hosts
|
|
require.Equal(t, uint(0), res.Verifying) // hosts[9] transferred to new team so is not counted under no team
|
|
require.Equal(t, uint(0), res.Verified)
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
|
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
|
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
|
}
|
|
|
|
func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
acc1 := &fleet.MDMIdPAccount{
|
|
Username: "email1@example.com",
|
|
Email: "email1@example.com",
|
|
Fullname: "John Doe",
|
|
}
|
|
|
|
acc2 := &fleet.MDMIdPAccount{
|
|
Username: "email2@example.com",
|
|
Email: "email2@example.com",
|
|
Fullname: "Jane Doe",
|
|
}
|
|
|
|
err := ds.InsertMDMIdPAccount(ctx, acc1)
|
|
require.NoError(t, err)
|
|
|
|
// try to instert the same account
|
|
err = ds.InsertMDMIdPAccount(ctx, acc1)
|
|
require.NoError(t, err)
|
|
|
|
out, err := ds.GetMDMIdPAccountByEmail(ctx, acc1.Email)
|
|
require.NoError(t, err)
|
|
// update the acc UUID
|
|
acc1.UUID = out.UUID
|
|
require.Equal(t, acc1, out)
|
|
|
|
err = ds.InsertMDMIdPAccount(ctx, acc2)
|
|
require.NoError(t, err)
|
|
|
|
out, err = ds.GetMDMIdPAccountByEmail(ctx, acc2.Email)
|
|
require.NoError(t, err)
|
|
// update the acc UUID
|
|
acc2.UUID = out.UUID
|
|
require.Equal(t, acc2, out)
|
|
|
|
var nfe fleet.NotFoundError
|
|
out, err = ds.GetMDMIdPAccountByEmail(ctx, "bad@email.com")
|
|
require.ErrorAs(t, err, &nfe)
|
|
require.Nil(t, out)
|
|
|
|
out, err = ds.GetMDMIdPAccountByUUID(ctx, acc1.UUID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, acc1, out)
|
|
|
|
out, err = ds.GetMDMIdPAccountByUUID(ctx, "BAD-TOKEN")
|
|
require.ErrorAs(t, err, &nfe)
|
|
require.Nil(t, out)
|
|
|
|
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-mdm-profiles",
|
|
Hostname: "hostname1",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
host2, err := ds.NewHost(ctx, &fleet.Host{
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now(),
|
|
OsqueryHostID: ptr.String("host2-osquery-id"),
|
|
NodeKey: ptr.String("host2-node-key"),
|
|
UUID: "host2-test-mdm-profiles",
|
|
Hostname: "hostname2",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
idpAccounts, err := ds.GetMDMIdPAccountsByHostUUIDs(ctx, []string{host1.UUID, host2.UUID})
|
|
require.NoError(t, err)
|
|
require.Len(t, idpAccounts, 0)
|
|
|
|
// Get account by UUID should also return nothing
|
|
idpAccount, err := ds.GetMDMIdPAccountByHostUUID(ctx, host1.UUID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, idpAccount)
|
|
|
|
err = ds.AssociateHostMDMIdPAccount(ctx, host1.UUID, acc1.UUID)
|
|
require.NoError(t, err)
|
|
|
|
err = ds.AssociateHostMDMIdPAccount(ctx, host2.UUID, acc2.UUID)
|
|
require.NoError(t, err)
|
|
|
|
idpAccounts, err = ds.GetMDMIdPAccountsByHostUUIDs(ctx, []string{host1.UUID, host2.UUID})
|
|
require.NoError(t, err)
|
|
require.Len(t, idpAccounts, 2)
|
|
require.Equal(t, *acc1, *idpAccounts[host1.UUID])
|
|
require.Equal(t, *acc2, *idpAccounts[host2.UUID])
|
|
|
|
idpAccount, err = ds.GetMDMIdPAccountByHostUUID(ctx, host1.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, idpAccount)
|
|
require.Equal(t, *acc1, *idpAccount)
|
|
}
|
|
|
|
func testDoNotIgnoreMDMClientError(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// create new record for remove pending
|
|
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
|
|
ProfileUUID: "a" + uuid.NewString(),
|
|
ProfileIdentifier: "p1",
|
|
ProfileName: "name1",
|
|
HostUUID: "h1",
|
|
CommandUUID: "c1",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Checksum: []byte("csum"),
|
|
Scope: fleet.PayloadScopeSystem,
|
|
}}))
|
|
cps, err := ds.GetHostMDMAppleProfiles(ctx, "h1")
|
|
require.NoError(t, err)
|
|
require.Len(t, cps, 1)
|
|
require.Equal(t, "name1", cps[0].Name)
|
|
require.Equal(t, fleet.MDMOperationTypeRemove, cps[0].OperationType)
|
|
require.NotNil(t, cps[0].Status)
|
|
require.Equal(t, fleet.MDMDeliveryPending, *cps[0].Status)
|
|
|
|
// simulate remove failed with client error message
|
|
require.NoError(t, ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
|
|
CommandUUID: "c1",
|
|
HostUUID: "h1",
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "MDMClientError (89): Profile with identifier 'p1' not found.",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
}))
|
|
cps, err = ds.GetHostMDMAppleProfiles(ctx, "h1")
|
|
require.NoError(t, err)
|
|
require.Len(t, cps, 1) // we no longer ignore error code 89
|
|
require.Equal(t, "name1", cps[0].Name)
|
|
require.Equal(t, fleet.MDMOperationTypeRemove, cps[0].OperationType)
|
|
require.NotNil(t, cps[0].Status)
|
|
require.Equal(t, fleet.MDMDeliveryFailed, *cps[0].Status)
|
|
require.Equal(t, "Failed to remove: MDMClientError (89): Profile with identifier 'p1' not found.", cps[0].Detail)
|
|
|
|
// create another new record
|
|
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
|
|
ProfileUUID: "a" + uuid.NewString(),
|
|
ProfileIdentifier: "p2",
|
|
ProfileName: "name2",
|
|
HostUUID: "h2",
|
|
CommandUUID: "c2",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Checksum: []byte("csum"),
|
|
Scope: fleet.PayloadScopeSystem,
|
|
}}))
|
|
cps, err = ds.GetHostMDMAppleProfiles(ctx, "h2")
|
|
require.NoError(t, err)
|
|
require.Len(t, cps, 1)
|
|
require.Equal(t, "name2", cps[0].Name)
|
|
require.Equal(t, fleet.MDMOperationTypeRemove, cps[0].OperationType)
|
|
require.NotNil(t, cps[0].Status)
|
|
require.Equal(t, fleet.MDMDeliveryPending, *cps[0].Status)
|
|
|
|
// simulate remove failed with another client error message that we don't want to ignore
|
|
require.NoError(t, ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
|
|
CommandUUID: "c2",
|
|
HostUUID: "h2",
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "MDMClientError (96): Cannot replace profile 'p2' because it was not installed by the MDM server.",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
}))
|
|
cps, err = ds.GetHostMDMAppleProfiles(ctx, "h2")
|
|
require.NoError(t, err)
|
|
require.Len(t, cps, 1)
|
|
require.Equal(t, "name2", cps[0].Name)
|
|
require.Equal(t, fleet.MDMOperationTypeRemove, cps[0].OperationType)
|
|
require.NotNil(t, cps[0].Status)
|
|
require.Equal(t, fleet.MDMDeliveryFailed, *cps[0].Status)
|
|
require.Equal(t, "Failed to remove: MDMClientError (96): Cannot replace profile 'p2' because it was not installed by the MDM server.", cps[0].Detail)
|
|
}
|
|
|
|
func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
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)
|
|
|
|
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
|
|
ProfileUUID: "a" + uuid.NewString(),
|
|
ProfileIdentifier: "p1",
|
|
ProfileName: "name1",
|
|
HostUUID: h.UUID,
|
|
CommandUUID: "c1",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Checksum: []byte("csum"),
|
|
Scope: fleet.PayloadScopeSystem,
|
|
}}))
|
|
|
|
gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, gotProfs, 1)
|
|
|
|
tx, err := ds.writer(ctx).BeginTxx(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.deleteMDMOSCustomSettingsForHost(ctx, tx, h.UUID, "darwin"))
|
|
require.NoError(t, tx.Commit())
|
|
require.NoError(t, err)
|
|
gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, gotProfs)
|
|
}
|
|
|
|
func createDiskEncryptionRecord(ctx context.Context, ds *Datastore, t *testing.T, host *fleet.Host, key string, decryptable bool,
|
|
threshold time.Time,
|
|
) {
|
|
_, err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, key, "", nil)
|
|
require.NoError(t, err)
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, decryptable, threshold)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestMDMAppleFileVaultSummary(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
ctx := t.Context()
|
|
|
|
// 10 new hosts
|
|
var hosts []*fleet.Host
|
|
for i := 0; i < 7; 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())
|
|
nanoEnrollUserDeviceAndSetHostMDMData(t, ds, h)
|
|
hosts = append(hosts, h)
|
|
}
|
|
|
|
hostCountEncryptionStatus := func(status fleet.DiskEncryptionStatus, teamID *uint) int {
|
|
gotHosts, err := ds.ListHosts(ctx,
|
|
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
|
|
fleet.HostListOptions{OSSettingsDiskEncryptionFilter: status, TeamFilter: teamID},
|
|
)
|
|
require.NoError(t, err)
|
|
return len(gotHosts)
|
|
}
|
|
|
|
// no teams tests =====
|
|
noTeamFVProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fleetmdm.FleetFileVaultProfileName, mobileconfig.FleetFileVaultPayloadIdentifier, 0), nil)
|
|
require.NoError(t, err)
|
|
|
|
// verifying status
|
|
verifyingHost := hosts[0]
|
|
upsertHostCPs(
|
|
[]*fleet.Host{verifyingHost},
|
|
[]*fleet.MDMAppleConfigProfile{noTeamFVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryVerifying,
|
|
ctx, ds, t,
|
|
)
|
|
oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute)
|
|
createDiskEncryptionRecord(ctx, ds, t, verifyingHost, "key-1", true, oneMinuteAfterThreshold)
|
|
|
|
fvProfileSummary, err := ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(0), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(0), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
|
|
allProfilesSummary, err := ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(0), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
|
|
// action required status
|
|
requiredActionHost := hosts[1]
|
|
upsertHostCPs(
|
|
[]*fleet.Host{requiredActionHost},
|
|
[]*fleet.MDMAppleConfigProfile{noTeamFVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryVerifying, ctx, ds, t,
|
|
)
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{requiredActionHost.ID}, false, oneMinuteAfterThreshold)
|
|
require.NoError(t, err)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(1), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(0), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionActionRequired, nil))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// enforcing status
|
|
enforcingHost := hosts[2]
|
|
|
|
// host profile status is `pending`
|
|
upsertHostCPs(
|
|
[]*fleet.Host{enforcingHost},
|
|
[]*fleet.MDMAppleConfigProfile{noTeamFVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryPending, ctx, ds, t,
|
|
)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(1), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(1), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionActionRequired, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionEnforcing, nil))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// host profile status does not exist
|
|
upsertHostCPs(
|
|
[]*fleet.Host{enforcingHost},
|
|
[]*fleet.MDMAppleConfigProfile{noTeamFVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
nil, ctx, ds, t,
|
|
)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(1), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(1), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionActionRequired, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionEnforcing, nil))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// host profile status is verifying but decryptable key field does not exist
|
|
upsertHostCPs(
|
|
[]*fleet.Host{enforcingHost},
|
|
[]*fleet.MDMAppleConfigProfile{noTeamFVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryPending, ctx, ds, t,
|
|
)
|
|
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{enforcingHost.ID}, false, oneMinuteAfterThreshold)
|
|
require.NoError(t, err)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(1), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(1), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionActionRequired, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionEnforcing, nil))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// failed status
|
|
failedHost := hosts[3]
|
|
upsertHostCPs([]*fleet.Host{failedHost}, []*fleet.MDMAppleConfigProfile{noTeamFVProfile}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryFailed, ctx, ds, t)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(1), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(1), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(1), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionActionRequired, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionEnforcing, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionFailed, nil))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(2), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(1), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// removing enforcement status
|
|
removingEnforcementHost := hosts[4]
|
|
upsertHostCPs([]*fleet.Host{removingEnforcementHost}, []*fleet.MDMAppleConfigProfile{noTeamFVProfile}, fleet.MDMOperationTypeRemove, &fleet.MDMDeliveryPending, ctx, ds, t)
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(1), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(1), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(1), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(1), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionActionRequired, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionEnforcing, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionFailed, nil))
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionRemovingEnforcement, nil))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(3), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(1), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// teams filter tests =====
|
|
verifyingTeam1Host := hosts[6]
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team-1"})
|
|
require.NoError(t, err)
|
|
team1FVProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fleetmdm.FleetFileVaultProfileName, mobileconfig.FleetFileVaultPayloadIdentifier, tm.ID), nil)
|
|
require.NoError(t, err)
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{verifyingTeam1Host.ID}))
|
|
require.NoError(t, err)
|
|
|
|
upsertHostCPs([]*fleet.Host{verifyingTeam1Host}, []*fleet.MDMAppleConfigProfile{team1FVProfile}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
createDiskEncryptionRecord(ctx, ds, t, verifyingTeam1Host, "key-2", true, oneMinuteAfterThreshold)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(0), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(0), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, &tm.ID))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(0), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verified)
|
|
|
|
// verified status
|
|
upsertHostCPs(
|
|
[]*fleet.Host{verifyingTeam1Host},
|
|
[]*fleet.MDMAppleConfigProfile{team1FVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryVerified,
|
|
ctx, ds, t,
|
|
)
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verifying)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verified)
|
|
require.Equal(t, uint(0), fvProfileSummary.ActionRequired)
|
|
require.Equal(t, uint(0), fvProfileSummary.Enforcing)
|
|
require.Equal(t, uint(0), fvProfileSummary.Failed)
|
|
require.Equal(t, uint(0), fvProfileSummary.RemovingEnforcement)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerified, &tm.ID))
|
|
|
|
allProfilesSummary, err = ds.GetMDMAppleProfilesSummary(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, allProfilesSummary)
|
|
require.Equal(t, uint(0), allProfilesSummary.Pending)
|
|
require.Equal(t, uint(0), allProfilesSummary.Failed)
|
|
require.Equal(t, uint(0), allProfilesSummary.Verifying)
|
|
require.Equal(t, uint(1), allProfilesSummary.Verified)
|
|
}
|
|
|
|
func testGetMDMAppleCommandResults(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// no enrolled host, unknown command
|
|
res, err := ds.GetMDMAppleCommandResults(ctx, uuid.New().String(), "")
|
|
require.NoError(t, err)
|
|
require.Empty(t, res)
|
|
|
|
p, err := ds.GetMDMCommandPlatform(ctx, uuid.New().String())
|
|
require.True(t, fleet.IsNotFound(err))
|
|
require.Empty(t, p)
|
|
|
|
// create some hosts, all enrolled
|
|
enrolledHosts := make([]*fleet.Host, 3)
|
|
for i := 0; i < 3; 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)
|
|
}
|
|
|
|
// create a non-enrolled host
|
|
i := 3
|
|
unenrolledHost, 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)
|
|
|
|
commander, storage := createMDMAppleCommanderAndStorage(t, ds)
|
|
|
|
// enqueue a command for an unenrolled host fails with a foreign key error (no enrollment)
|
|
uuid1 := uuid.New().String()
|
|
err = commander.EnqueueCommand(ctx, []string{unenrolledHost.UUID}, createRawAppleCmd("ProfileList", uuid1))
|
|
require.Error(t, err)
|
|
var mysqlErr *mysql.MySQLError
|
|
require.ErrorAs(t, err, &mysqlErr)
|
|
require.Equal(t, uint16(mysqlerr.ER_NO_REFERENCED_ROW_2), mysqlErr.Number)
|
|
|
|
// command has no results
|
|
res, err = ds.GetMDMAppleCommandResults(ctx, uuid1, "")
|
|
require.NoError(t, err)
|
|
require.Empty(t, res)
|
|
|
|
p, err = ds.GetMDMCommandPlatform(ctx, uuid1)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
require.Empty(t, p)
|
|
|
|
// enqueue a command for a couple of enrolled hosts
|
|
uuid2 := uuid.New().String()
|
|
rawCmd2 := createRawAppleCmd("ProfileList", uuid2)
|
|
err = commander.EnqueueCommand(ctx, []string{enrolledHosts[0].UUID, enrolledHosts[1].UUID}, rawCmd2)
|
|
require.NoError(t, err)
|
|
|
|
// command has no results yet
|
|
res, err = ds.GetMDMAppleCommandResults(ctx, uuid2, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 2)
|
|
for _, r := range res {
|
|
require.Equal(t, r.CommandUUID, uuid2)
|
|
require.Equal(t, r.Status, "Pending")
|
|
require.Equal(t, r.RequestType, "ProfileList")
|
|
require.Empty(t, r.Result)
|
|
require.Equal(t, []byte(rawCmd2), r.Payload)
|
|
}
|
|
p, err = ds.GetMDMCommandPlatform(ctx, uuid2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "darwin", p)
|
|
|
|
// simulate a result for enrolledHosts[0]
|
|
err = storage.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: enrolledHosts[0].UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: uuid2,
|
|
Status: "Acknowledged",
|
|
Raw: []byte(rawCmd2),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// command has a result for [0]
|
|
res, err = ds.GetMDMAppleCommandResults(ctx, uuid2, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 2)
|
|
for _, r := range res {
|
|
require.Equal(t, r.CommandUUID, uuid2)
|
|
require.Equal(t, r.RequestType, "ProfileList")
|
|
require.Equal(t, []byte(rawCmd2), r.Payload)
|
|
if r.HostUUID == enrolledHosts[0].UUID {
|
|
require.Equal(t, r.Status, "Acknowledged")
|
|
require.Equal(t, r.Result, []byte(rawCmd2))
|
|
} else {
|
|
require.Equal(t, r.HostUUID, enrolledHosts[1].UUID)
|
|
require.Equal(t, r.Status, "Pending")
|
|
require.Empty(t, r.Result)
|
|
}
|
|
}
|
|
p, err = ds.GetMDMCommandPlatform(ctx, uuid2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "darwin", p)
|
|
|
|
// simulate a result for enrolledHosts[1]
|
|
err = storage.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: enrolledHosts[1].UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: uuid2,
|
|
Status: "Error",
|
|
Raw: []byte(rawCmd2),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// command has both results
|
|
res, err = ds.GetMDMAppleCommandResults(ctx, uuid2, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 2)
|
|
|
|
require.NotZero(t, res[0].UpdatedAt)
|
|
res[0].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[1].UpdatedAt)
|
|
res[1].UpdatedAt = time.Time{}
|
|
|
|
require.ElementsMatch(t, res, []*fleet.MDMCommandResult{
|
|
{
|
|
HostUUID: enrolledHosts[0].UUID,
|
|
CommandUUID: uuid2,
|
|
Status: "Acknowledged",
|
|
RequestType: "ProfileList",
|
|
Result: []byte(rawCmd2),
|
|
Payload: []byte(rawCmd2),
|
|
},
|
|
{
|
|
HostUUID: enrolledHosts[1].UUID,
|
|
CommandUUID: uuid2,
|
|
Status: "Error",
|
|
RequestType: "ProfileList",
|
|
Result: []byte(rawCmd2),
|
|
Payload: []byte(rawCmd2),
|
|
},
|
|
})
|
|
|
|
p, err = ds.GetMDMCommandPlatform(ctx, uuid2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "darwin", p)
|
|
|
|
// delete host [0] and verify that it didn't delete its command results
|
|
err = ds.DeleteHost(ctx, enrolledHosts[0].ID)
|
|
require.NoError(t, err)
|
|
|
|
res, err = ds.GetMDMAppleCommandResults(ctx, uuid2, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 2)
|
|
|
|
require.NotZero(t, res[0].UpdatedAt)
|
|
res[0].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[1].UpdatedAt)
|
|
res[1].UpdatedAt = time.Time{}
|
|
require.ElementsMatch(t, res, []*fleet.MDMCommandResult{
|
|
{
|
|
HostUUID: enrolledHosts[0].UUID,
|
|
CommandUUID: uuid2,
|
|
Status: "Acknowledged",
|
|
RequestType: "ProfileList",
|
|
Result: []byte(rawCmd2),
|
|
Payload: []byte(rawCmd2),
|
|
},
|
|
{
|
|
HostUUID: enrolledHosts[1].UUID,
|
|
CommandUUID: uuid2,
|
|
Status: "Error",
|
|
RequestType: "ProfileList",
|
|
Result: []byte(rawCmd2),
|
|
Payload: []byte(rawCmd2),
|
|
},
|
|
})
|
|
|
|
p, err = ds.GetMDMCommandPlatform(ctx, uuid2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "darwin", p)
|
|
}
|
|
|
|
func createMDMAppleCommanderAndStorage(t *testing.T, ds *Datastore) (*apple_mdm.MDMAppleCommander, *NanoMDMStorage) {
|
|
mdmStorage, err := ds.NewMDMAppleMDMStorage()
|
|
require.NoError(t, err)
|
|
|
|
return apple_mdm.NewMDMAppleCommander(mdmStorage, pusherFunc(okPusherFunc)), mdmStorage
|
|
}
|
|
|
|
func okPusherFunc(ctx context.Context, ids []string) (map[string]*push.Response, error) {
|
|
m := make(map[string]*push.Response, len(ids))
|
|
for _, id := range ids {
|
|
m[id] = &push.Response{Id: id}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
type pusherFunc func(context.Context, []string) (map[string]*push.Response, error)
|
|
|
|
func (f pusherFunc) Push(ctx context.Context, ids []string) (map[string]*push.Response, error) {
|
|
return f(ctx, ids)
|
|
}
|
|
|
|
func testBulkUpsertMDMAppleConfigProfile(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
mc := mobileconfig.Mobileconfig([]byte("TestConfigProfile"))
|
|
globalCP := &fleet.MDMAppleConfigProfile{
|
|
Name: "DummyTestName",
|
|
Identifier: "DummyTestIdentifier",
|
|
Mobileconfig: mc,
|
|
TeamID: nil,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
}
|
|
teamCP := &fleet.MDMAppleConfigProfile{
|
|
Name: "DummyTestName",
|
|
Identifier: "DummyTestIdentifier",
|
|
Mobileconfig: mc,
|
|
TeamID: ptr.Uint(1),
|
|
Scope: fleet.PayloadScopeSystem,
|
|
}
|
|
allProfiles := []*fleet.MDMAppleConfigProfile{globalCP, teamCP}
|
|
|
|
checkProfiles := func(uploadedAtMatch bool) {
|
|
for _, p := range allProfiles {
|
|
profiles, err := ds.ListMDMAppleConfigProfiles(ctx, p.TeamID)
|
|
require.NoError(t, err)
|
|
require.Len(t, profiles, 1)
|
|
|
|
wantProf := *p
|
|
if !uploadedAtMatch {
|
|
require.True(t, profiles[0].UploadedAt.After(wantProf.UploadedAt))
|
|
wantProf.UploadedAt = time.Time{}
|
|
}
|
|
checkConfigProfile(t, wantProf, *profiles[0])
|
|
}
|
|
}
|
|
|
|
err := ds.BulkUpsertMDMAppleConfigProfiles(ctx, allProfiles)
|
|
require.NoError(t, err)
|
|
|
|
reloadUploadedAt := func() {
|
|
// reload to get the uploaded_at timestamps
|
|
profiles, err := ds.ListMDMAppleConfigProfiles(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, profiles, 1)
|
|
globalCP.UploadedAt = profiles[0].UploadedAt
|
|
profiles, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(1))
|
|
require.NoError(t, err)
|
|
require.Len(t, profiles, 1)
|
|
teamCP.UploadedAt = profiles[0].UploadedAt
|
|
}
|
|
reloadUploadedAt()
|
|
|
|
checkProfiles(true)
|
|
|
|
time.Sleep(time.Second) // ensure DB timestamps change
|
|
|
|
newMc := mobileconfig.Mobileconfig([]byte("TestUpdatedConfigProfile"))
|
|
globalCP.Mobileconfig = newMc
|
|
teamCP.Mobileconfig = newMc
|
|
err = ds.BulkUpsertMDMAppleConfigProfiles(ctx, allProfiles)
|
|
require.NoError(t, err)
|
|
|
|
// uploaded_at should be after the previously loaded timestamps
|
|
checkProfiles(false)
|
|
|
|
time.Sleep(time.Second) // ensure DB timestamps change
|
|
|
|
// call it again with no changes, should not update timestamps
|
|
reloadUploadedAt()
|
|
err = ds.BulkUpsertMDMAppleConfigProfiles(ctx, allProfiles)
|
|
require.NoError(t, err)
|
|
checkProfiles(true)
|
|
}
|
|
|
|
func testMDMAppleBootstrapPackageCRUD(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
var nfe fleet.NotFoundError
|
|
var aerr fleet.AlreadyExistsError
|
|
|
|
err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{}, nil)
|
|
require.Error(t, err)
|
|
|
|
content1 := []byte("content")
|
|
hasher1 := sha256.New()
|
|
hasher1.Write(content1)
|
|
bp1 := &fleet.MDMAppleBootstrapPackage{
|
|
TeamID: uint(0),
|
|
Name: t.Name(),
|
|
Sha256: hasher1.Sum(nil),
|
|
Bytes: content1,
|
|
Token: uuid.New().String(),
|
|
}
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, bp1, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, bp1, nil)
|
|
require.ErrorAs(t, err, &aerr)
|
|
|
|
content2 := []byte("content")
|
|
hasher2 := sha256.New()
|
|
hasher2.Write(content2)
|
|
bp2 := &fleet.MDMAppleBootstrapPackage{
|
|
TeamID: uint(2),
|
|
Name: t.Name(),
|
|
Sha256: hasher2.Sum(nil),
|
|
Bytes: content2,
|
|
Token: uuid.New().String(),
|
|
}
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, bp2, nil)
|
|
require.NoError(t, err)
|
|
|
|
meta, err := ds.GetMDMAppleBootstrapPackageMeta(ctx, 0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, bp1.TeamID, meta.TeamID)
|
|
require.Equal(t, bp1.Name, meta.Name)
|
|
require.Equal(t, bp1.Sha256, meta.Sha256)
|
|
require.Equal(t, bp1.Token, meta.Token)
|
|
|
|
meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 3)
|
|
require.ErrorAs(t, err, &nfe)
|
|
require.Nil(t, meta)
|
|
|
|
bytes, err := ds.GetMDMAppleBootstrapPackageBytes(ctx, bp1.Token, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, bp1.Bytes, bytes.Bytes)
|
|
|
|
bytes, err = ds.GetMDMAppleBootstrapPackageBytes(ctx, "fake", nil)
|
|
require.ErrorAs(t, err, &nfe)
|
|
require.Nil(t, bytes)
|
|
|
|
err = ds.DeleteMDMAppleBootstrapPackage(ctx, 0)
|
|
require.NoError(t, err)
|
|
|
|
meta, err = ds.GetMDMAppleBootstrapPackageMeta(ctx, 0)
|
|
require.ErrorAs(t, err, &nfe)
|
|
require.Nil(t, meta)
|
|
|
|
err = ds.DeleteMDMAppleBootstrapPackage(ctx, 0)
|
|
require.ErrorAs(t, err, &nfe)
|
|
require.Nil(t, meta)
|
|
}
|
|
|
|
func testListMDMAppleCommands(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// create some enrolled hosts
|
|
enrolledHosts := make([]*fleet.Host, 4)
|
|
for i := 0; i < 3; 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)
|
|
}
|
|
|
|
h, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "test-host3-byod-name",
|
|
OsqueryHostID: ptr.String("osquery-3"),
|
|
NodeKey: ptr.String("nodekey-3"),
|
|
UUID: "test-uuid-byod-3",
|
|
Platform: "ios",
|
|
})
|
|
require.NoError(t, err)
|
|
nanoEnrollUserDevice(t, ds, h)
|
|
enrolledHosts[3] = h
|
|
t.Logf("enrolled BYOD host [%d]: %s", 3, h.UUID)
|
|
|
|
// create a team
|
|
tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
|
require.NoError(t, err)
|
|
// assign enrolledHosts[2] to tm1
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm1.ID, []uint{enrolledHosts[2].ID}))
|
|
require.NoError(t, err)
|
|
|
|
commander, storage := createMDMAppleCommanderAndStorage(t, ds)
|
|
|
|
// no commands yet
|
|
res, err := ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
|
|
require.NoError(t, err)
|
|
require.Empty(t, res)
|
|
|
|
// enqueue a command for enrolled hosts [0], [1] and [3]
|
|
uuid1 := uuid.New().String()
|
|
rawCmd1 := createRawAppleCmd("ListApps", uuid1)
|
|
err = commander.EnqueueCommand(ctx, []string{enrolledHosts[0].UUID, enrolledHosts[1].UUID, enrolledHosts[3].UUID}, rawCmd1)
|
|
require.NoError(t, err)
|
|
|
|
// command has no results yet, so the status is empty
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 3)
|
|
|
|
require.NotZero(t, res[0].UpdatedAt)
|
|
res[0].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[1].UpdatedAt)
|
|
res[1].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[2].UpdatedAt)
|
|
res[2].UpdatedAt = time.Time{}
|
|
|
|
require.ElementsMatch(t, res, []*fleet.MDMAppleCommand{
|
|
{
|
|
DeviceID: enrolledHosts[0].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Pending",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[0].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
DeviceID: enrolledHosts[1].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Pending",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[1].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
DeviceID: enrolledHosts[3].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Pending",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[3].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
})
|
|
|
|
// 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)
|
|
|
|
// command is now listed with a status for this result
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 3)
|
|
|
|
require.NotZero(t, res[0].UpdatedAt)
|
|
res[0].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[1].UpdatedAt)
|
|
res[1].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[2].UpdatedAt)
|
|
res[2].UpdatedAt = time.Time{}
|
|
|
|
require.ElementsMatch(t, res, []*fleet.MDMAppleCommand{
|
|
{
|
|
DeviceID: enrolledHosts[0].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Acknowledged",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[0].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
DeviceID: enrolledHosts[1].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Pending",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[1].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
DeviceID: enrolledHosts[3].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Pending",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[3].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
})
|
|
|
|
// simulate a result for enrolledHosts[1] and enrolledHosts[3]
|
|
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)
|
|
|
|
err = storage.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: enrolledHosts[3].UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: uuid1,
|
|
Status: "Acknowledged",
|
|
Raw: []byte(rawCmd1),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// both results are now listed
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 3)
|
|
|
|
require.NotZero(t, res[0].UpdatedAt)
|
|
res[0].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[1].UpdatedAt)
|
|
res[1].UpdatedAt = time.Time{}
|
|
require.NotZero(t, res[2].UpdatedAt)
|
|
res[2].UpdatedAt = time.Time{}
|
|
|
|
require.ElementsMatch(t, res, []*fleet.MDMAppleCommand{
|
|
{
|
|
DeviceID: enrolledHosts[0].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Acknowledged",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[0].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
DeviceID: enrolledHosts[1].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Error",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[1].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
DeviceID: enrolledHosts[3].UUID,
|
|
CommandUUID: uuid1,
|
|
Status: "Acknowledged",
|
|
RequestType: "ListApps",
|
|
Hostname: enrolledHosts[3].Hostname,
|
|
TeamID: nil,
|
|
},
|
|
})
|
|
|
|
// enqueue another command for enrolled hosts [1] and [2]
|
|
uuid2 := uuid.New().String()
|
|
rawCmd2 := createRawAppleCmd("InstallApp", uuid2)
|
|
err = commander.EnqueueCommand(ctx, []string{enrolledHosts[1].UUID, enrolledHosts[2].UUID}, rawCmd2)
|
|
require.NoError(t, err)
|
|
|
|
// simulate a result for enrolledHosts[1] and [2]
|
|
err = storage.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: enrolledHosts[1].UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: uuid2,
|
|
Status: "Acknowledged",
|
|
Raw: []byte(rawCmd2),
|
|
})
|
|
require.NoError(t, err)
|
|
err = storage.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: enrolledHosts[2].UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: uuid2,
|
|
Status: "Acknowledged",
|
|
Raw: []byte(rawCmd2),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// results are listed
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 5)
|
|
|
|
// page-by-page: first page
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{
|
|
ListOptions: fleet.ListOptions{Page: 0, PerPage: 3, OrderKey: "device_id", OrderDirection: fleet.OrderDescending},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 3)
|
|
|
|
// page-by-page: second page
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{
|
|
ListOptions: fleet.ListOptions{Page: 1, PerPage: 3, OrderKey: "device_id", OrderDirection: fleet.OrderDescending},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 2)
|
|
|
|
// filter by a user from team tm1, can only see that team's host
|
|
u1, err := ds.NewUser(ctx, &fleet.User{
|
|
Password: []byte("garbage"),
|
|
Salt: "garbage",
|
|
Name: "user1",
|
|
Email: "user1@example.com",
|
|
GlobalRole: nil,
|
|
Teams: []fleet.UserTeam{
|
|
{Team: *tm1, Role: fleet.RoleObserver},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
u1, err = ds.UserByID(ctx, u1.ID)
|
|
require.NoError(t, err)
|
|
|
|
// u1 is an observer, so if IncludeObserver is not set, returns nothing
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: u1}, &fleet.MDMCommandListOptions{
|
|
ListOptions: fleet.ListOptions{PerPage: 3},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 0)
|
|
|
|
// now with IncludeObserver set to true
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: u1, IncludeObserver: true}, &fleet.MDMCommandListOptions{
|
|
ListOptions: fleet.ListOptions{PerPage: 3, OrderKey: "updated_at", OrderDirection: fleet.OrderDescending},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 1)
|
|
require.NotZero(t, res[0].UpdatedAt)
|
|
res[0].UpdatedAt = time.Time{}
|
|
require.ElementsMatch(t, res, []*fleet.MDMAppleCommand{
|
|
{
|
|
DeviceID: enrolledHosts[2].UUID,
|
|
CommandUUID: uuid2,
|
|
Status: "Acknowledged",
|
|
RequestType: "InstallApp",
|
|
Hostname: enrolledHosts[2].Hostname,
|
|
TeamID: &tm1.ID,
|
|
},
|
|
})
|
|
|
|
// randomly set two commadns as inactive
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx, `UPDATE nano_enrollment_queue SET active = 0 LIMIT 2`)
|
|
return err
|
|
})
|
|
// only three results are listed
|
|
res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res, 3)
|
|
}
|
|
|
|
func testMDMAppleSetupAssistant(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// get non-existing
|
|
_, err := ds.GetMDMAppleSetupAssistant(ctx, nil)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, nil, "no-such-token")
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
|
|
// create for no team
|
|
noTeamAsst, err := ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{Name: "test", Profile: json.RawMessage("{}")})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, noTeamAsst.ID)
|
|
require.NotZero(t, noTeamAsst.UploadedAt)
|
|
require.Nil(t, noTeamAsst.TeamID)
|
|
require.Equal(t, "test", noTeamAsst.Name)
|
|
require.Equal(t, "{}", string(noTeamAsst.Profile))
|
|
|
|
// get for no team returns the same data
|
|
getAsst, err := ds.GetMDMAppleSetupAssistant(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, noTeamAsst, getAsst)
|
|
|
|
// create for non-existing team fails
|
|
_, err = ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{TeamID: ptr.Uint(123), Name: "test", Profile: json.RawMessage("{}")})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "foreign key constraint fails")
|
|
|
|
// create a team
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "tm"})
|
|
require.NoError(t, err)
|
|
|
|
// create for existing team
|
|
tmAsst, err := ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{TeamID: &tm.ID, Name: "test", Profile: json.RawMessage("{}")})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, tmAsst.ID)
|
|
require.NotZero(t, tmAsst.UploadedAt)
|
|
require.NotNil(t, tmAsst.TeamID)
|
|
require.Equal(t, tm.ID, *tmAsst.TeamID)
|
|
require.Equal(t, "test", tmAsst.Name)
|
|
require.Equal(t, "{}", string(tmAsst.Profile))
|
|
|
|
// get for team returns the same data
|
|
getAsst, err = ds.GetMDMAppleSetupAssistant(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tmAsst, getAsst)
|
|
|
|
// create an ABM token and set a profile uuid for no team
|
|
tok1, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "o1", EncryptedToken: []byte(uuid.NewString()), RenewAt: time.Now().Add(365 * 24 * time.Hour)})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, tok1.ID)
|
|
profUUID1 := uuid.NewString()
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, nil, profUUID1, "o1")
|
|
require.NoError(t, err)
|
|
gotProf, gotTs, err := ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, nil, "o1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, profUUID1, gotProf)
|
|
require.NotZero(t, gotTs)
|
|
|
|
// set a profile uuid for an unknown token, no error but nothing inserted
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, nil, profUUID1, "no-such-token")
|
|
require.NoError(t, err)
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, nil, "no-such-token")
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
|
|
// create another ABM token and set a profile uuid for the team assistant
|
|
// with both tokens
|
|
tok2, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "o2", EncryptedToken: []byte(uuid.NewString()), RenewAt: time.Now().Add(365 * 24 * time.Hour)})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, tok2.ID)
|
|
profUUID2 := uuid.NewString()
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, &tm.ID, profUUID1, "o1")
|
|
require.NoError(t, err)
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, &tm.ID, profUUID2, "o2")
|
|
require.NoError(t, err)
|
|
gotProf, gotTs, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, profUUID1, gotProf)
|
|
require.NotZero(t, gotTs)
|
|
gotProf, gotTs, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o2")
|
|
require.NoError(t, err)
|
|
require.Equal(t, profUUID2, gotProf)
|
|
require.NotZero(t, gotTs)
|
|
|
|
// update the profile uuid for o2 only
|
|
profUUID3 := uuid.NewString()
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, &tm.ID, profUUID3, "o2")
|
|
require.NoError(t, err)
|
|
gotProf, gotTs, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, profUUID1, gotProf)
|
|
require.NotZero(t, gotTs)
|
|
gotProf, gotTs, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o2")
|
|
require.NoError(t, err)
|
|
require.Equal(t, profUUID3, gotProf)
|
|
require.NotZero(t, gotTs)
|
|
|
|
// upsert team assistant
|
|
tmAsst2, err := ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{TeamID: &tm.ID, Name: "test2", Profile: json.RawMessage(`{"x":2}`)})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tmAsst2.ID, tmAsst.ID)
|
|
require.False(t, tmAsst2.UploadedAt.Before(tmAsst.UploadedAt)) // after or equal
|
|
require.Equal(t, tmAsst.TeamID, tmAsst2.TeamID)
|
|
require.Equal(t, "test2", tmAsst2.Name)
|
|
require.JSONEq(t, `{"x": 2}`, string(tmAsst2.Profile))
|
|
|
|
// profile uuids have been cleared
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o1")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o2")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
// upsert no team assistant
|
|
noTeamAsst2, err := ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{Name: "test3", Profile: json.RawMessage(`{"x": 3}`)})
|
|
require.NoError(t, err)
|
|
require.Equal(t, noTeamAsst2.ID, noTeamAsst.ID)
|
|
require.False(t, noTeamAsst2.UploadedAt.Before(noTeamAsst.UploadedAt)) // after or equal
|
|
require.Nil(t, noTeamAsst2.TeamID)
|
|
require.Equal(t, "test3", noTeamAsst2.Name)
|
|
require.JSONEq(t, `{"x": 3}`, string(noTeamAsst2.Profile))
|
|
|
|
// profile uuid has been cleared
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, nil, "o1")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
time.Sleep(time.Second) // ensures the timestamp checks are not by chance
|
|
|
|
// set profile uuids for team and no team (one each)
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, nil, profUUID1, "o1")
|
|
require.NoError(t, err)
|
|
err = ds.SetMDMAppleSetupAssistantProfileUUID(ctx, &tm.ID, profUUID2, "o2")
|
|
require.NoError(t, err)
|
|
|
|
// upsert team no change, uploaded at timestamp does not change
|
|
tmAsst3, err := ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{TeamID: &tm.ID, Name: "test2", Profile: json.RawMessage(`{"x":2}`)})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tmAsst2, tmAsst3)
|
|
|
|
// TODO(mna): ideally the profiles would not be cleared when the profile
|
|
// stayed the same, but does not work at the moment and we're pressed by
|
|
// time.
|
|
// gotProf, gotTs, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o2")
|
|
// require.NoError(t, err)
|
|
// require.Equal(t, profUUID2, gotProf)
|
|
// require.Equal(t, tmAsst3.UploadedAt, gotTs)
|
|
|
|
time.Sleep(time.Second) // ensures the timestamp checks are not by chance
|
|
|
|
// upsert team with a change, clears the profile uuid and updates the uploaded at timestamp
|
|
tmAsst4, err := ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{TeamID: &tm.ID, Name: "test2", Profile: json.RawMessage(`{"x":3}`)})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tmAsst3.ID, tmAsst4.ID)
|
|
require.True(t, tmAsst4.UploadedAt.After(tmAsst3.UploadedAt))
|
|
require.Equal(t, tmAsst3.TeamID, tmAsst4.TeamID)
|
|
require.Equal(t, "test2", tmAsst4.Name)
|
|
require.JSONEq(t, `{"x": 3}`, string(tmAsst4.Profile))
|
|
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, &tm.ID, "o2")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
// delete no team
|
|
err = ds.DeleteMDMAppleSetupAssistant(ctx, nil)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = ds.GetMDMAppleSetupAssistantProfileForABMToken(ctx, nil, "o1")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
// delete the team, which will cascade delete the setup assistant
|
|
err = ds.DeleteTeam(ctx, tm.ID)
|
|
require.NoError(t, err)
|
|
|
|
// get the team assistant
|
|
_, err = ds.GetMDMAppleSetupAssistant(ctx, &tm.ID)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
// delete the team assistant, no error if it doesn't exist
|
|
err = ds.DeleteMDMAppleSetupAssistant(ctx, &tm.ID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testMDMAppleEnrollmentProfile(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
_, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
_, err = ds.GetMDMAppleEnrollmentProfileByToken(ctx, "abcd")
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
// add a new automatic enrollment profile
|
|
rawMsg := json.RawMessage(`{"allow_pairing": true}`)
|
|
profAuto, err := ds.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{
|
|
Type: "automatic",
|
|
DEPProfile: &rawMsg,
|
|
Token: "abcd",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, profAuto.ID)
|
|
|
|
// add a new manual enrollment profile
|
|
profMan, err := ds.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{
|
|
Type: "manual",
|
|
DEPProfile: &rawMsg,
|
|
Token: "efgh",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, profMan.ID)
|
|
|
|
profs, err := ds.ListMDMAppleEnrollmentProfiles(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, profs, 2)
|
|
|
|
tokens := make([]string, 2)
|
|
for i, p := range profs {
|
|
tokens[i] = p.Token
|
|
}
|
|
require.ElementsMatch(t, []string{"abcd", "efgh"}, tokens)
|
|
|
|
// get the automatic profile by type
|
|
getProf, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
|
require.NoError(t, err)
|
|
getProf.UpdateCreateTimestamps = fleet.UpdateCreateTimestamps{}
|
|
require.Equal(t, profAuto, getProf)
|
|
|
|
// get the manual profile by token
|
|
getProf, err = ds.GetMDMAppleEnrollmentProfileByToken(ctx, "efgh")
|
|
require.NoError(t, err)
|
|
getProf.UpdateCreateTimestamps = fleet.UpdateCreateTimestamps{}
|
|
require.Equal(t, profMan, getProf)
|
|
}
|
|
|
|
func testListMDMAppleSerials(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
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)
|
|
|
|
// create a mix of DEP-enrolled hosts, non-Fleet-MDM, pending DEP-enrollment
|
|
hosts := make([]*fleet.Host, 7)
|
|
for i := 0; i < len(hosts); i++ {
|
|
serial := fmt.Sprintf("serial-%d", i)
|
|
if i == 6 {
|
|
serial = ""
|
|
}
|
|
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",
|
|
HardwareSerial: serial,
|
|
})
|
|
require.NoError(t, err)
|
|
switch {
|
|
case i <= 3:
|
|
// assigned in ABM to Fleet
|
|
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmToken.ID, make(map[uint]time.Time))
|
|
require.NoError(t, err)
|
|
case i == 4:
|
|
// not ABM assigned
|
|
case i == 5:
|
|
// ABM assignment was deleted
|
|
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmToken.ID, make(map[uint]time.Time))
|
|
require.NoError(t, err)
|
|
err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{h.HardwareSerial})
|
|
require.NoError(t, err)
|
|
case i == 6:
|
|
// assigned in ABM, but we don't have a serial
|
|
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmToken.ID, make(map[uint]time.Time))
|
|
require.NoError(t, err)
|
|
}
|
|
hosts[i] = h
|
|
t.Logf("host [%d]: %s - %s", i, h.UUID, h.HardwareSerial)
|
|
}
|
|
|
|
// create 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)
|
|
|
|
// assign hosts[2,4,5] to tm1
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm1.ID, []uint{hosts[2].ID, hosts[4].ID, hosts[5].ID}))
|
|
require.NoError(t, err)
|
|
|
|
// list serials in team 2, has none
|
|
serials, err := ds.ListMDMAppleDEPSerialsInTeam(ctx, &tm2.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, serials)
|
|
|
|
// list serials in team 1, has one (hosts[2])
|
|
serials, err = ds.ListMDMAppleDEPSerialsInTeam(ctx, &tm1.ID)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"serial-2"}, serials)
|
|
|
|
// list serials in no-team, has 3 (hosts[0,1,3]), hosts[6] doesn't have a serial number
|
|
serials, err = ds.ListMDMAppleDEPSerialsInTeam(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"serial-0", "serial-1", "serial-3"}, serials)
|
|
|
|
// list serials with no host IDs returns empty
|
|
serials, err = ds.ListMDMAppleDEPSerialsInHostIDs(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, serials)
|
|
|
|
// list serials in hosts[0,1,2,3] returns all of them
|
|
serials, err = ds.ListMDMAppleDEPSerialsInHostIDs(ctx, []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID})
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"serial-0", "serial-1", "serial-2", "serial-3"}, serials)
|
|
|
|
// list serials in hosts[4,5,6] returns none
|
|
serials, err = ds.ListMDMAppleDEPSerialsInHostIDs(ctx, []uint{hosts[4].ID, hosts[5].ID, hosts[6].ID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, serials)
|
|
|
|
// list serials in all hosts returns [0-3]
|
|
serials, err = ds.ListMDMAppleDEPSerialsInHostIDs(ctx, []uint{
|
|
hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID,
|
|
hosts[5].ID, hosts[6].ID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"serial-0", "serial-1", "serial-2", "serial-3"}, serials)
|
|
}
|
|
|
|
func testMDMAppleDefaultSetupAssistant(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// create a couple ABM tokens
|
|
tok1, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: "o1", EncryptedToken: []byte(uuid.NewString()), 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: "o2", EncryptedToken: []byte(uuid.NewString()), RenewAt: time.Now().Add(365 * 24 * time.Hour)})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, tok2.ID)
|
|
|
|
// get non-existing
|
|
_, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, nil, "no-such-token")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
|
|
// set for no team
|
|
err = ds.SetMDMAppleDefaultSetupAssistantProfileUUID(ctx, nil, "no-team", "o1")
|
|
require.NoError(t, err)
|
|
|
|
// get for no team returns the same data
|
|
uuid, ts, err := ds.GetMDMAppleDefaultSetupAssistant(ctx, nil, "o1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "no-team", uuid)
|
|
require.NotZero(t, ts)
|
|
|
|
// set for non-existing team fails
|
|
err = ds.SetMDMAppleDefaultSetupAssistantProfileUUID(ctx, ptr.Uint(123), "xyz", "o2")
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "foreign key constraint fails")
|
|
|
|
// get for non-existing team fails
|
|
_, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, ptr.Uint(123), "o2")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
|
|
// create a team
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "tm"})
|
|
require.NoError(t, err)
|
|
|
|
// set a couple profiles for existing team
|
|
err = ds.SetMDMAppleDefaultSetupAssistantProfileUUID(ctx, &tm.ID, "tm1", "o1")
|
|
require.NoError(t, err)
|
|
err = ds.SetMDMAppleDefaultSetupAssistantProfileUUID(ctx, &tm.ID, "tm2", "o2")
|
|
require.NoError(t, err)
|
|
|
|
// get for existing team
|
|
uuid, ts, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, &tm.ID, "o1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "tm1", uuid)
|
|
require.NotZero(t, ts)
|
|
uuid, ts, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, &tm.ID, "o2")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "tm2", uuid)
|
|
require.NotZero(t, ts)
|
|
// get for unknown abm token
|
|
_, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, &tm.ID, "no-such-token")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
|
|
// clear all profiles for team
|
|
err = ds.SetMDMAppleDefaultSetupAssistantProfileUUID(ctx, &tm.ID, "", "")
|
|
require.NoError(t, err)
|
|
_, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, &tm.ID, "o1")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
_, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, &tm.ID, "o2")
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// map of host IDs to map of profile identifiers to delivery status
|
|
expectedHostMDMStatus := make(map[uint]map[string]fleet.MDMDeliveryStatus)
|
|
|
|
// create some config profiles for no team
|
|
cp1, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "name1", "cp1", "uuid1"), nil)
|
|
require.NoError(t, err)
|
|
cp2, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "name2", "cp2", "uuid2"), nil)
|
|
require.NoError(t, err)
|
|
cp3, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "name3", "cp3", "uuid3"), nil)
|
|
require.NoError(t, err)
|
|
cp4, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t, "name4", "cp4", "uuid4"), nil)
|
|
require.NoError(t, err)
|
|
|
|
// list config profiles for no team
|
|
cps, err := ds.ListMDMAppleConfigProfiles(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, cps, 4)
|
|
storedByIdentifier := make(map[string]*fleet.MDMAppleConfigProfile)
|
|
for _, cp := range cps {
|
|
storedByIdentifier[cp.Identifier] = cp
|
|
}
|
|
|
|
// create test hosts
|
|
var hosts []*fleet.Host
|
|
for i := 0; i < 3; 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().Add(-1*time.Hour))
|
|
hosts = append(hosts, h)
|
|
expectedHostMDMStatus[h.ID] = map[string]fleet.MDMDeliveryStatus{
|
|
cp1.Identifier: fleet.MDMDeliveryPending,
|
|
cp2.Identifier: fleet.MDMDeliveryVerifying,
|
|
cp3.Identifier: fleet.MDMDeliveryVerified,
|
|
cp4.Identifier: fleet.MDMDeliveryPending,
|
|
}
|
|
}
|
|
|
|
// add a team config profile with the same name and identifer as one of the no-team profiles
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "tm"})
|
|
require.NoError(t, err)
|
|
_, err = ds.NewMDMAppleConfigProfile(ctx, *teamConfigProfileForTest(t, cp2.Name, cp2.Identifier, "uuid2", tm.ID), nil)
|
|
require.NoError(t, err)
|
|
|
|
checkHostMDMProfileStatuses := func() {
|
|
for _, h := range hosts {
|
|
gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, gotProfs, 4)
|
|
for _, p := range gotProfs {
|
|
s, ok := expectedHostMDMStatus[h.ID][p.Identifier]
|
|
require.True(t, ok)
|
|
require.NotNil(t, p.Status)
|
|
require.Equalf(t, s, *p.Status, "profile identifier %s", p.Identifier)
|
|
}
|
|
}
|
|
}
|
|
|
|
adHocSetVerifying := func(hostUUID, profileIndentifier string) {
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx,
|
|
`UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_identifier = ?`,
|
|
fleet.MDMDeliveryVerifying, hostUUID, profileIndentifier)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// initialize the host MDM profile statuses
|
|
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{storedByIdentifier[cp1.Identifier]}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryPending, ctx, ds, t)
|
|
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{storedByIdentifier[cp2.Identifier]}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{storedByIdentifier[cp3.Identifier]}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerified, ctx, ds, t)
|
|
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{storedByIdentifier[cp4.Identifier]}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryPending, ctx, ds, t)
|
|
checkHostMDMProfileStatuses()
|
|
|
|
// statuses don't change during the grace period if profiles are missing (i.e. not installed)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[0], map[string]*fleet.HostMacOSProfile{}))
|
|
checkHostMDMProfileStatuses()
|
|
|
|
// if install date is before the updated at timestamp of the profile, statuses don't change
|
|
// during the grace period
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[1], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp1.Identifier,
|
|
DisplayName: cp1.Name,
|
|
InstallDate: storedByIdentifier[cp1.Identifier].UploadedAt.Add(-1 * time.Hour),
|
|
},
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: storedByIdentifier[cp2.Identifier].UploadedAt.Add(-1 * time.Hour),
|
|
},
|
|
{
|
|
Identifier: cp3.Identifier,
|
|
DisplayName: cp3.Name,
|
|
InstallDate: storedByIdentifier[cp3.Identifier].UploadedAt.Add(-1 * time.Hour),
|
|
},
|
|
{
|
|
Identifier: cp4.Identifier,
|
|
DisplayName: cp4.Name,
|
|
InstallDate: storedByIdentifier[cp4.Identifier].UploadedAt.Add(-1 * time.Hour),
|
|
},
|
|
})))
|
|
checkHostMDMProfileStatuses()
|
|
|
|
// if install date is on or after the updated at timestamp of the profile, "verifying" or "pending" status
|
|
// changes to "verified". Any "pending" profiles not reported are not changed
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[2], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: storedByIdentifier[cp2.Identifier].UploadedAt,
|
|
},
|
|
{
|
|
Identifier: cp3.Identifier,
|
|
DisplayName: cp3.Name,
|
|
InstallDate: storedByIdentifier[cp3.Identifier].UploadedAt,
|
|
},
|
|
{
|
|
Identifier: cp4.Identifier,
|
|
DisplayName: cp4.Name,
|
|
InstallDate: storedByIdentifier[cp4.Identifier].UploadedAt,
|
|
},
|
|
})))
|
|
expectedHostMDMStatus[hosts[2].ID][cp2.Identifier] = fleet.MDMDeliveryVerified
|
|
expectedHostMDMStatus[hosts[2].ID][cp4.Identifier] = fleet.MDMDeliveryVerified
|
|
checkHostMDMProfileStatuses()
|
|
|
|
// repeated call doesn't change statuses
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[2], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: storedByIdentifier[cp2.Identifier].UploadedAt,
|
|
},
|
|
{
|
|
Identifier: cp3.Identifier,
|
|
DisplayName: cp3.Name,
|
|
InstallDate: storedByIdentifier[cp3.Identifier].UploadedAt,
|
|
},
|
|
{
|
|
Identifier: cp4.Identifier,
|
|
DisplayName: cp4.Name,
|
|
InstallDate: storedByIdentifier[cp4.Identifier].UploadedAt,
|
|
},
|
|
})))
|
|
checkHostMDMProfileStatuses()
|
|
|
|
// simulate expired grace period by setting uploaded_at timestamp of profiles back by 24 hours
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx,
|
|
`UPDATE mdm_apple_configuration_profiles SET uploaded_at = ? WHERE profile_uuid IN(?, ?, ?, ?)`,
|
|
time.Now().Add(-24*time.Hour),
|
|
cp1.ProfileUUID, cp2.ProfileUUID, cp3.ProfileUUID, cp4.ProfileUUID,
|
|
)
|
|
return err
|
|
})
|
|
|
|
// after the grace period and max retry attempts, status changes to "failed" if a profile is missing (i.e. not installed)
|
|
for missingRetry := range fleetmdm.MaxAppleProfileRetries {
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[2], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp1.Identifier,
|
|
DisplayName: cp1.Name,
|
|
InstallDate: time.Now(),
|
|
},
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: time.Now(),
|
|
},
|
|
})))
|
|
if missingRetry == 0 {
|
|
expectedHostMDMStatus[hosts[2].ID][cp1.Identifier] = fleet.MDMDeliveryVerified // cp1 can go from pending to verified
|
|
}
|
|
expectedHostMDMStatus[hosts[2].ID][cp3.Identifier] = fleet.MDMDeliveryPending // retry for cp3
|
|
expectedHostMDMStatus[hosts[2].ID][cp4.Identifier] = fleet.MDMDeliveryPending // retry for cp4
|
|
checkHostMDMProfileStatuses()
|
|
// simulate retry command acknowledged by setting status to "verifying"
|
|
adHocSetVerifying(hosts[2].UUID, cp3.Identifier)
|
|
adHocSetVerifying(hosts[2].UUID, cp4.Identifier)
|
|
}
|
|
// report osquery results again with cp3 and cp4 still missing after max retries
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[2], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp1.Identifier,
|
|
DisplayName: cp1.Name,
|
|
InstallDate: time.Now(),
|
|
},
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: time.Now(),
|
|
},
|
|
})))
|
|
expectedHostMDMStatus[hosts[2].ID][cp3.Identifier] = fleet.MDMDeliveryFailed // still missing after max retries so expect cp3 to fail
|
|
expectedHostMDMStatus[hosts[2].ID][cp4.Identifier] = fleet.MDMDeliveryFailed // still missing after max retries so expect cp4 to fail
|
|
checkHostMDMProfileStatuses()
|
|
|
|
// after the grace period and max retry attempts, status changes to "failed" if a profile is outdated (i.e. installed
|
|
// before the updated at timestamp of the profile)
|
|
for range fleetmdm.MaxAppleProfileRetries {
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[2], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp1.Identifier,
|
|
DisplayName: cp1.Name,
|
|
InstallDate: time.Now(),
|
|
},
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: time.Now().Add(-48 * time.Hour),
|
|
},
|
|
})))
|
|
expectedHostMDMStatus[hosts[2].ID][cp2.Identifier] = fleet.MDMDeliveryPending // retry for cp2
|
|
checkHostMDMProfileStatuses()
|
|
// simulate retry command acknowledged by setting status to "verifying"
|
|
adHocSetVerifying(hosts[2].UUID, cp2.Identifier)
|
|
}
|
|
// report osquery results again with cp2 still outdated after max retries
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, hosts[2], profilesByIdentifier([]*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: cp1.Identifier,
|
|
DisplayName: cp1.Name,
|
|
InstallDate: time.Now(),
|
|
},
|
|
{
|
|
Identifier: cp2.Identifier,
|
|
DisplayName: cp2.Name,
|
|
InstallDate: time.Now().Add(-48 * time.Hour),
|
|
},
|
|
})))
|
|
expectedHostMDMStatus[hosts[2].ID][cp2.Identifier] = fleet.MDMDeliveryFailed // still outdated after max retries so expect cp2 to fail
|
|
checkHostMDMProfileStatuses()
|
|
}
|
|
|
|
func TestCopyDefaultMDMAppleBootstrapPackage(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
defer ds.Close()
|
|
|
|
ctx := t.Context()
|
|
|
|
checkStoredBP := func(teamID uint, wantErr error, wantNewToken bool, wantBP *fleet.MDMAppleBootstrapPackage) {
|
|
var gotBP fleet.MDMAppleBootstrapPackage
|
|
err := sqlx.GetContext(ctx, ds.primary, &gotBP, "SELECT * FROM mdm_apple_bootstrap_packages WHERE team_id = ?", teamID)
|
|
if wantErr != nil {
|
|
require.EqualError(t, err, wantErr.Error())
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
if wantNewToken {
|
|
require.NotEqual(t, wantBP.Token, gotBP.Token)
|
|
} else {
|
|
require.Equal(t, wantBP.Token, gotBP.Token)
|
|
}
|
|
require.Equal(t, wantBP.Name, gotBP.Name)
|
|
require.Equal(t, wantBP.Sha256[:32], gotBP.Sha256)
|
|
require.Equal(t, wantBP.Bytes, gotBP.Bytes)
|
|
}
|
|
|
|
checkAppConfig := func(wantURL string) {
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, wantURL, ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
}
|
|
|
|
checkTeamConfig := func(teamID uint, wantURL string) {
|
|
tm, err := ds.TeamLite(ctx, teamID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, wantURL, tm.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
}
|
|
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
|
require.NoError(t, err)
|
|
teamID := tm.ID
|
|
noTeamID := uint(0)
|
|
|
|
// confirm bootstrap package url is empty by default
|
|
checkAppConfig("")
|
|
checkTeamConfig(teamID, "")
|
|
|
|
// create a default bootstrap package
|
|
content := []byte("content")
|
|
hasher := sha256.New()
|
|
hasher.Write(content)
|
|
defaultBP := &fleet.MDMAppleBootstrapPackage{
|
|
TeamID: noTeamID,
|
|
Name: "name",
|
|
Sha256: hasher.Sum(nil),
|
|
Bytes: content,
|
|
Token: uuid.New().String(),
|
|
}
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, defaultBP, nil)
|
|
require.NoError(t, err)
|
|
checkStoredBP(noTeamID, nil, false, defaultBP) // default bootstrap package is stored
|
|
checkStoredBP(teamID, sql.ErrNoRows, false, nil) // no bootstrap package yet for team
|
|
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
err = ds.CopyDefaultMDMAppleBootstrapPackage(ctx, ac, teamID)
|
|
require.NoError(t, err)
|
|
|
|
checkAppConfig("") // no bootstrap package url set in app config
|
|
checkTeamConfig(teamID, "") // no bootstrap package url set in team config
|
|
checkStoredBP(noTeamID, nil, false, defaultBP) // no change to default bootstrap package
|
|
checkStoredBP(teamID, nil, true, defaultBP) // copied default bootstrap package
|
|
|
|
// delete and update the default bootstrap package
|
|
err = ds.DeleteMDMAppleBootstrapPackage(ctx, noTeamID)
|
|
require.NoError(t, err)
|
|
checkStoredBP(noTeamID, sql.ErrNoRows, false, nil) // deleted
|
|
checkStoredBP(teamID, nil, true, defaultBP) // still exists
|
|
|
|
// update the default bootstrap package
|
|
newContent := []byte("new content")
|
|
newHasher := sha256.New()
|
|
newHasher.Write(newContent)
|
|
defaultBP2 := &fleet.MDMAppleBootstrapPackage{
|
|
TeamID: noTeamID,
|
|
Name: "new name",
|
|
Sha256: newHasher.Sum(nil),
|
|
Bytes: newContent,
|
|
Token: uuid.New().String(),
|
|
}
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, defaultBP2, nil)
|
|
require.NoError(t, err)
|
|
checkStoredBP(noTeamID, nil, false, defaultBP2)
|
|
// set bootstrap package url in app config
|
|
ac.MDM.MacOSSetup.BootstrapPackage = optjson.SetString("https://example.com/bootstrap.pkg")
|
|
err = ds.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
checkAppConfig("https://example.com/bootstrap.pkg")
|
|
|
|
// copy default bootstrap package fails when there is already a team bootstrap package
|
|
var wantErr error = &existsError{ResourceType: "BootstrapPackage", TeamID: &teamID}
|
|
err = ds.CopyDefaultMDMAppleBootstrapPackage(ctx, ac, teamID)
|
|
require.ErrorContains(t, err, wantErr.Error())
|
|
// confirm team bootstrap package is unchanged
|
|
checkStoredBP(teamID, nil, true, defaultBP)
|
|
checkTeamConfig(teamID, "")
|
|
|
|
// delete the team bootstrap package
|
|
err = ds.DeleteMDMAppleBootstrapPackage(ctx, teamID)
|
|
require.NoError(t, err)
|
|
checkStoredBP(teamID, sql.ErrNoRows, false, nil)
|
|
checkTeamConfig(teamID, "")
|
|
|
|
// confirm no change to default bootstrap package
|
|
checkStoredBP(noTeamID, nil, false, defaultBP2)
|
|
checkAppConfig("https://example.com/bootstrap.pkg")
|
|
|
|
// copy default bootstrap package succeeds when there is no team bootstrap package
|
|
err = ds.CopyDefaultMDMAppleBootstrapPackage(ctx, ac, teamID)
|
|
require.NoError(t, err)
|
|
// confirm team bootstrap package gets new token and otherwise matches default bootstrap package
|
|
checkStoredBP(teamID, nil, true, defaultBP2)
|
|
// confirm bootstrap package url was set in team config to match app config
|
|
checkTeamConfig(teamID, "https://example.com/bootstrap.pkg")
|
|
|
|
// test some edge cases
|
|
|
|
// delete the team bootstrap package doesn't affect the team config
|
|
err = ds.DeleteMDMAppleBootstrapPackage(ctx, teamID)
|
|
require.NoError(t, err)
|
|
checkStoredBP(teamID, sql.ErrNoRows, false, nil)
|
|
checkTeamConfig(teamID, "https://example.com/bootstrap.pkg")
|
|
|
|
// set other team config values so we can confirm they are not affected by bootstrap package changes
|
|
tc, err := ds.TeamWithExtras(ctx, teamID)
|
|
require.NoError(t, err)
|
|
tc.Config.MDM.MacOSSetup.MacOSSetupAssistant = optjson.SetString("/path/to/setupassistant")
|
|
tc.Config.MDM.MacOSUpdates.Deadline = optjson.SetString("2024-01-01")
|
|
tc.Config.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("10.15.4")
|
|
tc.Config.WebhookSettings.FailingPoliciesWebhook = fleet.FailingPoliciesWebhookSettings{
|
|
Enable: true,
|
|
DestinationURL: "https://example.com/webhook",
|
|
}
|
|
tc.Config.Features.EnableHostUsers = true
|
|
savedTeam, err := ds.SaveTeam(ctx, tc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.Config, savedTeam.Config)
|
|
|
|
// change the default bootstrap package url
|
|
ac.MDM.MacOSSetup.BootstrapPackage = optjson.SetString("https://example.com/bs.pkg")
|
|
err = ds.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
checkAppConfig("https://example.com/bs.pkg")
|
|
checkTeamConfig(teamID, "https://example.com/bootstrap.pkg") // team config is unchanged
|
|
|
|
// copy default bootstrap package succeeds when there is no team bootstrap package
|
|
err = ds.CopyDefaultMDMAppleBootstrapPackage(ctx, ac, teamID)
|
|
require.NoError(t, err)
|
|
// confirm team bootstrap package gets new token and otherwise matches default bootstrap package
|
|
checkStoredBP(teamID, nil, true, defaultBP2)
|
|
// confirm bootstrap package url was set in team config to match app config
|
|
checkTeamConfig(teamID, "https://example.com/bs.pkg")
|
|
|
|
// confirm other team config values are unchanged
|
|
teamFromDb, err := ds.TeamWithExtras(ctx, teamID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, teamFromDb.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value, "/path/to/setupassistant")
|
|
require.Equal(t, teamFromDb.Config.MDM.MacOSUpdates.Deadline.Value, "2024-01-01")
|
|
require.Equal(t, teamFromDb.Config.MDM.MacOSUpdates.MinimumVersion.Value, "10.15.4")
|
|
require.Equal(t, teamFromDb.Config.WebhookSettings.FailingPoliciesWebhook.DestinationURL, "https://example.com/webhook")
|
|
require.Equal(t, teamFromDb.Config.WebhookSettings.FailingPoliciesWebhook.Enable, true)
|
|
require.Equal(t, teamFromDb.Config.Features.EnableHostUsers, true)
|
|
}
|
|
|
|
func TestHostDEPAssignments(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
defer ds.Close()
|
|
|
|
ctx := t.Context()
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
expectedMDMServerURL, err := apple_mdm.ResolveAppleEnrollMDMURL(ac.ServerSettings.ServerURL)
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
t.Run("DEP enrollment", func(t *testing.T) {
|
|
depSerial := "dep-serial"
|
|
depUUID := "dep-uuid"
|
|
depOrbitNodeKey := "dep-orbit-node-key"
|
|
depDeviceTok := "dep-device-token"
|
|
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{{SerialNumber: depSerial}}, abmToken.ID, nil, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), n)
|
|
|
|
var depHostID uint
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &depHostID, "SELECT id FROM hosts WHERE hardware_serial = ?", depSerial)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is created when DEP device is ingested
|
|
getHostResp, err := ds.Host(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, depHostID, getHostResp.ID)
|
|
require.Equal(t, "Pending", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment is created when DEP device is ingested
|
|
depAssignment, err := ds.GetHostDEPAssignment(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, depHostID, depAssignment.HostID)
|
|
require.Nil(t, depAssignment.DeletedAt)
|
|
require.WithinDuration(t, time.Now(), depAssignment.AddedAt, 5*time.Second)
|
|
require.NotNil(t, depAssignment.ABMTokenID)
|
|
require.Equal(t, *depAssignment.ABMTokenID, abmToken.ID)
|
|
|
|
// simulate initial osquery enrollment via Orbit
|
|
testHost, err := ds.EnrollOrbit(ctx,
|
|
fleet.WithEnrollOrbitMDMEnabled(true),
|
|
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{HardwareSerial: depSerial, Platform: "darwin", HardwareUUID: depUUID, Hostname: "dep-host"}),
|
|
fleet.WithEnrollOrbitNodeKey(depOrbitNodeKey),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, testHost)
|
|
|
|
// create device auth token for host
|
|
err = ds.SetOrUpdateDeviceAuthToken(ctx, depHostID, depDeviceTok)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM doesn't change upon Orbit enrollment
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.Equal(t, "Pending", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment is reported for load host by Orbit node key and by device token
|
|
h, err := ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// simulate osquery report of MDM detail query
|
|
err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
// enrollment status changes to "On (automatic)"
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.Equal(t, "On (automatic)", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// Host should not be marked as migrating
|
|
checkinInfo, err := ds.GetHostMDMCheckinInfo(ctx, depUUID)
|
|
require.NoError(t, err)
|
|
// Migration is complete
|
|
require.False(t, checkinInfo.MigrationInProgress)
|
|
|
|
// simulate MDM unenroll
|
|
_, _, err = ds.MDMTurnOff(ctx, depUUID)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is set to defaults on unenrollment
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.NotNil(t, getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, "Off", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Empty(t, getHostResp.MDM.ServerURL)
|
|
require.Empty(t, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// simulate osquery report of MDM detail query reflecting re-enrollment to MDM
|
|
err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is re-created when osquery reports MDM detail query
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.Equal(t, "On (automatic)", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// simulate osquery report of MDM detail query with empty server URL (signals unenrollment
|
|
// from MDM)
|
|
err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, false, "", false, "", "", false)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is reset to defaults when osquery reports MDM detail query with empty server URL
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.NotNil(t, getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, "Off", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Empty(t, getHostResp.MDM.ServerURL)
|
|
require.Empty(t, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
hdepa, err := ds.GetHostDEPAssignment(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, depHostID, hdepa.HostID)
|
|
require.Nil(t, hdepa.DeletedAt)
|
|
require.Equal(t, depAssignment.AddedAt, hdepa.AddedAt)
|
|
})
|
|
|
|
t.Run("DEP enrollment with migration", func(t *testing.T) {
|
|
depSerial := "dep-migration-serial"
|
|
depUUID := "dep-migration-uuid"
|
|
depOrbitNodeKey := "dep-migration-orbit-node-key"
|
|
depDeviceTok := "dep-migration-device-token"
|
|
|
|
migrationDeadline := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Millisecond)
|
|
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{{SerialNumber: depSerial, MDMMigrationDeadline: &migrationDeadline}}, abmToken.ID, nil, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), n)
|
|
|
|
var depHostID uint
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &depHostID, "SELECT id FROM hosts WHERE hardware_serial = ?", depSerial)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is created when DEP device is ingested
|
|
getHostResp, err := ds.Host(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, depHostID, getHostResp.ID)
|
|
require.Equal(t, "Pending", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment is created when DEP device is ingested
|
|
depAssignment, err := ds.GetHostDEPAssignment(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, depHostID, depAssignment.HostID)
|
|
require.Nil(t, depAssignment.DeletedAt)
|
|
require.WithinDuration(t, time.Now(), depAssignment.AddedAt, 5*time.Second)
|
|
require.NotNil(t, depAssignment.ABMTokenID)
|
|
require.Equal(t, *depAssignment.ABMTokenID, abmToken.ID)
|
|
|
|
// simulate initial osquery enrollment via Orbit
|
|
testHost, err := ds.EnrollOrbit(ctx,
|
|
fleet.WithEnrollOrbitMDMEnabled(true),
|
|
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{HardwareSerial: depSerial, Platform: "darwin", HardwareUUID: depUUID, Hostname: "dep-host"}),
|
|
fleet.WithEnrollOrbitNodeKey(depOrbitNodeKey),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, testHost)
|
|
|
|
// create device auth token for host
|
|
err = ds.SetOrUpdateDeviceAuthToken(ctx, depHostID, depDeviceTok)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM doesn't change upon Orbit enrollment
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.Equal(t, "Pending", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment is reported for load host by Orbit node key and by device token
|
|
h, err := ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// simulate osquery report of MDM detail query
|
|
err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
// enrollment status changes to "On (automatic)"
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.Equal(t, "On (automatic)", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
checkinInfo, err := ds.GetHostMDMCheckinInfo(ctx, depUUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, checkinInfo)
|
|
require.Equal(t, testHost.ID, checkinInfo.HostID)
|
|
require.True(t, checkinInfo.DEPAssignedToFleet)
|
|
require.True(t, checkinInfo.MigrationInProgress)
|
|
|
|
err = ds.SetHostMDMMigrationCompleted(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
|
|
checkinInfo, err = ds.GetHostMDMCheckinInfo(ctx, depUUID)
|
|
require.NoError(t, err)
|
|
// Migration is complete
|
|
require.False(t, checkinInfo.MigrationInProgress)
|
|
|
|
// simulate MDM unenroll
|
|
_, _, err = ds.MDMTurnOff(ctx, depUUID)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is set to defaults on unenrollment
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.NotNil(t, getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, "Off", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Empty(t, getHostResp.MDM.ServerURL)
|
|
require.Empty(t, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// host DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// simulate osquery report of MDM detail query reflecting re-enrollment to MDM
|
|
err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is re-created when osquery reports MDM detail query
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.Equal(t, "On (automatic)", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
// simulate osquery report of MDM detail query with empty server URL (signals unenrollment
|
|
// from MDM)
|
|
err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, false, "", false, "", "", false)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is reset to defaults when osquery reports MDM detail query with empty server URL
|
|
getHostResp, err = ds.Host(ctx, testHost.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, testHost.ID, getHostResp.ID)
|
|
require.NotNil(t, getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, "Off", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Empty(t, getHostResp.MDM.ServerURL)
|
|
require.Empty(t, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// DEP assignment doesn't change
|
|
h, err = ds.LoadHostByOrbitNodeKey(ctx, depOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, depDeviceTok, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.True(t, *h.DEPAssignedToFleet)
|
|
|
|
hdepa, err := ds.GetHostDEPAssignment(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, depHostID, hdepa.HostID)
|
|
require.Nil(t, hdepa.DeletedAt)
|
|
require.Equal(t, depAssignment.AddedAt, hdepa.AddedAt)
|
|
})
|
|
|
|
t.Run("manual enrollment", func(t *testing.T) {
|
|
// create a non-DEP host
|
|
manualSerial := "manual-serial"
|
|
manualUUID := "manual-uuid"
|
|
manualOrbitNodeKey := "manual-orbit-node-key"
|
|
manualDeviceToken := "manual-device-token"
|
|
|
|
err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{HardwareSerial: manualSerial, UUID: manualUUID}, false)
|
|
require.NoError(t, err)
|
|
|
|
var manualHostID uint
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &manualHostID, "SELECT id FROM hosts WHERE hardware_serial = ?", manualSerial)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM is "On (manual)"
|
|
getHostResp, err := ds.Host(ctx, manualHostID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, manualHostID, getHostResp.ID)
|
|
require.Equal(t, "On (manual)", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
// check host DEP assignment not created for non-DEP host
|
|
hdepa, err := ds.GetHostDEPAssignment(ctx, manualHostID)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
require.Nil(t, hdepa)
|
|
|
|
// simulate initial osquery enrollment via Orbit
|
|
manualHost, err := ds.EnrollOrbit(ctx,
|
|
fleet.WithEnrollOrbitMDMEnabled(true),
|
|
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{HardwareSerial: manualSerial, Platform: "darwin", HardwareUUID: manualUUID, Hostname: "maunual-host"}),
|
|
fleet.WithEnrollOrbitNodeKey(manualOrbitNodeKey),
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, manualHostID, manualHost.ID)
|
|
|
|
// create device auth token for host
|
|
err = ds.SetOrUpdateDeviceAuthToken(ctx, manualHostID, manualDeviceToken)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM doesn't change upon Orbit enrollment
|
|
getHostResp, err = ds.Host(ctx, manualHostID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getHostResp)
|
|
require.Equal(t, manualHostID, getHostResp.ID)
|
|
require.Equal(t, "On (manual)", *getHostResp.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, getHostResp.MDM.Name)
|
|
require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host
|
|
|
|
h, err := ds.LoadHostByOrbitNodeKey(ctx, manualOrbitNodeKey)
|
|
require.NoError(t, err)
|
|
require.False(t, *h.DEPAssignedToFleet)
|
|
h, err = ds.LoadHostByDeviceAuthToken(ctx, manualDeviceToken, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
require.False(t, *h.DEPAssignedToFleet)
|
|
})
|
|
}
|
|
|
|
func testMDMAppleConfigProfileHash(t *testing.T, ds *Datastore) {
|
|
// test that the mysql md5 hash exactly matches the hash produced by Go in
|
|
// the preassign profiles logic (no corner cases with extra whitespace, etc.)
|
|
ctx := t.Context()
|
|
|
|
// sprintf placeholders for prefix, content and suffix
|
|
const base = `%s<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple Inc//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
%s
|
|
</plist>%s`
|
|
|
|
cases := []struct {
|
|
prefix, content, suffix string
|
|
}{
|
|
{"", "", ""},
|
|
{" ", "", ""},
|
|
{"", "", " "},
|
|
{"\t\n ", "", "\t\n "},
|
|
{"", `<dict>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>PayloadUUID</key>
|
|
<string>Ignored</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>Ignored</string>
|
|
</dict>`, ""},
|
|
{" ", `<dict>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>PayloadUUID</key>
|
|
<string>Ignored</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>Ignored</string>
|
|
</dict>`, "\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', '<?xml')
|
|
`)
|
|
require.NoError(t, err)
|
|
_, err = ds.writer(ctx).Exec(`
|
|
INSERT INTO nano_command_results (id, command_uuid, status, result)
|
|
VALUES (?, 'command-uuid', 'Acknowledged', '<?xml')
|
|
`, host.UUID)
|
|
require.NoError(t, err)
|
|
packageContent := []byte("content")
|
|
packageHasher := sha256.New()
|
|
packageHasher.Write(packageContent)
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
TeamID: uint(0),
|
|
Name: t.Name(),
|
|
Sha256: packageHasher.Sum(nil),
|
|
Bytes: packageContent,
|
|
Token: uuid.New().String(),
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// host has no boostrap package command yet
|
|
_, err = ds.GetHostBootstrapPackageCommand(ctx, host.UUID)
|
|
require.Error(t, err)
|
|
nfe := &common_mysql.NotFoundError{}
|
|
require.ErrorAs(t, err, &nfe)
|
|
|
|
err = ds.RecordHostBootstrapPackage(ctx, "command-uuid", host.UUID)
|
|
require.NoError(t, err)
|
|
// add a record of the host DEP assignment
|
|
_, err = ds.writer(ctx).Exec(`
|
|
INSERT INTO host_dep_assignments (host_id)
|
|
VALUES (?)
|
|
ON DUPLICATE KEY UPDATE added_at = CURRENT_TIMESTAMP, deleted_at = NULL
|
|
`, host.ID)
|
|
require.NoError(t, err)
|
|
cmd, err := ds.GetHostBootstrapPackageCommand(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "command-uuid", cmd)
|
|
err = ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "foo.mdm.example.com", true, "", "", false)
|
|
require.NoError(t, err)
|
|
|
|
sum, err := ds.GetMDMAppleBootstrapPackageSummary(ctx, uint(0))
|
|
require.NoError(t, err)
|
|
require.Zero(t, sum.Failed)
|
|
require.Zero(t, sum.Pending)
|
|
require.EqualValues(t, 1, sum.Installed)
|
|
|
|
// reset the enrollment
|
|
err = ds.MDMResetEnrollment(ctx, host.UUID, false)
|
|
require.NoError(t, err)
|
|
|
|
gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, gotProfs)
|
|
|
|
sum, err = ds.GetMDMAppleBootstrapPackageSummary(ctx, uint(0))
|
|
require.NoError(t, err)
|
|
require.Zero(t, sum.Failed)
|
|
require.Zero(t, sum.Installed)
|
|
require.EqualValues(t, 1, sum.Pending)
|
|
|
|
// Mark the host as if it skipped the bootstrap package installation
|
|
err = ds.RecordSkippedHostBootstrapPackage(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
sum, err = ds.GetMDMAppleBootstrapPackageSummary(ctx, uint(0))
|
|
require.NoError(t, err)
|
|
require.Zero(t, sum.Failed)
|
|
require.Zero(t, sum.Installed)
|
|
require.Zero(t, sum.Pending)
|
|
}
|
|
|
|
func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
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)
|
|
|
|
cases := []struct {
|
|
name string
|
|
in []string
|
|
want []string
|
|
err string
|
|
}{
|
|
{"no serials provided", []string{}, []string{"foo", "bar", "baz"}, ""},
|
|
{"no matching serials", []string{"oof", "rab"}, []string{"foo", "bar", "baz"}, ""},
|
|
{"partial matches", []string{"foo", "rab"}, []string{"bar", "baz"}, ""},
|
|
{"all matching", []string{"foo", "bar", "baz"}, []string{}, ""},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
devices := []godep.Device{
|
|
{SerialNumber: "foo"},
|
|
{SerialNumber: "bar"},
|
|
{SerialNumber: "baz"},
|
|
}
|
|
_, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, devices, abmToken.ID, nil, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, tt.in)
|
|
if tt.err == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, tt.err)
|
|
}
|
|
var got []string
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.SelectContext(
|
|
ctx, q, &got,
|
|
`SELECT hardware_serial FROM hosts h
|
|
JOIN host_dep_assignments hda ON hda.host_id = h.id
|
|
WHERE hda.deleted_at IS NULL`,
|
|
)
|
|
})
|
|
require.ElementsMatch(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testLockUnlockWipeMacOS(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)
|
|
nanoEnroll(t, ds, host, false)
|
|
|
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
|
|
// default state
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
|
|
appleStore, err := ds.NewMDMAppleMDMStorage()
|
|
require.NoError(t, err)
|
|
|
|
// record a request to lock the host
|
|
cmd := &mdm.Command{
|
|
CommandUUID: "command-uuid",
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "DeviceLock"
|
|
err = appleStore.EnqueueDeviceLockCommand(ctx, host, cmd, "123456")
|
|
require.NoError(t, err)
|
|
|
|
// it is now pending lock
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, true, false)
|
|
|
|
// record a command result to simulate locked state
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Acknowledged",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, "DeviceLock", true)
|
|
require.NoError(t, err)
|
|
|
|
// it is now locked
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, false, true, false, false, false, false)
|
|
|
|
// request an unlock. This is a NOOP for Apple MDM.
|
|
err = ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC())
|
|
require.NoError(t, err)
|
|
|
|
// it is still locked
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, false, true, false, false, false, false)
|
|
|
|
// execute CleanAppleMDMLock to simulate successful unlock
|
|
err = ds.CleanAppleMDMLock(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// it is back to unlocked state
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
require.Empty(t, status.UnlockPIN)
|
|
|
|
// record a request to wipe the host
|
|
cmd = &mdm.Command{
|
|
CommandUUID: uuid.NewString(),
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "EraseDevice"
|
|
err = appleStore.EnqueueDeviceWipeCommand(ctx, host, cmd)
|
|
require.NoError(t, err)
|
|
|
|
// it is now pending wipe
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, true)
|
|
|
|
// record a command result failure to simulate failed wipe (back to unlocked)
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Error",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, cmd.Command.RequestType, false)
|
|
require.NoError(t, err)
|
|
|
|
// it is back to unlocked
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
|
|
// record a new request to wipe the host
|
|
cmd = &mdm.Command{
|
|
CommandUUID: uuid.NewString(),
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "EraseDevice"
|
|
err = appleStore.EnqueueDeviceWipeCommand(ctx, host, cmd)
|
|
require.NoError(t, err)
|
|
|
|
// it is back to pending wipe
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, true)
|
|
|
|
// record a command result success to simulate wipe
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Acknowledged",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, cmd.Command.RequestType, true)
|
|
require.NoError(t, err)
|
|
|
|
// it is wiped
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, false, false, true, false, false, false)
|
|
}
|
|
|
|
func testLockUnlockWipeIphone(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: "ios",
|
|
})
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, host, false)
|
|
|
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
|
|
// default state
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
|
|
appleStore, err := ds.NewMDMAppleMDMStorage()
|
|
require.NoError(t, err)
|
|
|
|
// record a request to enable lost mode on the host
|
|
cmd := &mdm.Command{
|
|
CommandUUID: uuid.NewString(),
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "EnableLostMode"
|
|
err = appleStore.EnqueueDeviceLockCommand(ctx, host, cmd, "")
|
|
require.NoError(t, err)
|
|
|
|
// it is now pending lock
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, true, false)
|
|
|
|
// record a command result to simulate locked state
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Acknowledged",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, "EnableLostMode", true)
|
|
require.NoError(t, err)
|
|
|
|
// Record a location from the DeviceLocation command
|
|
err = ds.InsertHostLocationData(ctx, fleet.HostLocationData{HostID: host.ID, Latitude: 42.42, Longitude: -42.42})
|
|
require.NoError(t, err)
|
|
|
|
// it is now locked
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, false, true, false, false, false, false)
|
|
|
|
// record a request to disable lost mode on the host
|
|
cmd = &mdm.Command{
|
|
CommandUUID: uuid.NewString(),
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "DisableLostMode"
|
|
err = appleStore.EnqueueDeviceUnlockCommand(ctx, host, cmd)
|
|
require.NoError(t, err)
|
|
|
|
// it is now pending unlock
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, false, true, false, true, false, false)
|
|
|
|
// record a command result to simulate unlocked state
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Acknowledged",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, "DisableLostMode", true)
|
|
require.NoError(t, err)
|
|
|
|
// it is now unlocked
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
|
|
// record a request to wipe the host
|
|
cmd = &mdm.Command{
|
|
CommandUUID: uuid.NewString(),
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "EraseDevice"
|
|
err = appleStore.EnqueueDeviceWipeCommand(ctx, host, cmd)
|
|
require.NoError(t, err)
|
|
|
|
// it is now pending wipe
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, true)
|
|
|
|
// record a command result failure to simulate failed wipe (back to unlocked)
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Error",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, cmd.Command.RequestType, false)
|
|
require.NoError(t, err)
|
|
|
|
// it is back to unlocked
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
|
|
// record a new request to wipe the host
|
|
cmd = &mdm.Command{
|
|
CommandUUID: uuid.NewString(),
|
|
Raw: []byte("<?xml"),
|
|
}
|
|
cmd.Command.RequestType = "EraseDevice"
|
|
err = appleStore.EnqueueDeviceWipeCommand(ctx, host, cmd)
|
|
require.NoError(t, err)
|
|
|
|
// it is back to pending wipe
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, true)
|
|
|
|
// record a command result success to simulate wipe
|
|
err = appleStore.StoreCommandReport(&mdm.Request{
|
|
EnrollID: &mdm.EnrollID{ID: host.UUID},
|
|
Context: ctx,
|
|
}, &mdm.CommandResults{
|
|
CommandUUID: cmd.CommandUUID,
|
|
Status: "Acknowledged",
|
|
Raw: cmd.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.UpdateHostLockWipeStatusFromAppleMDMResult(ctx, host.UUID, cmd.CommandUUID, cmd.Command.RequestType, true)
|
|
require.NoError(t, err)
|
|
|
|
// it is wiped
|
|
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, false, false, true, false, false, false)
|
|
}
|
|
|
|
func testOrphanMDMCommandRef(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
const orphanUUID = "orphan-command-uuid"
|
|
|
|
oldLogger := ds.logger
|
|
t.Cleanup(func() { ds.logger = oldLogger })
|
|
buf := &bytes.Buffer{}
|
|
ds.logger = logging.NewSlogLogger(logging.Options{Output: buf, Debug: true})
|
|
|
|
t.Run("darwin orphan lock_ref", func(t *testing.T) {
|
|
host, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "orphan-lock-darwin",
|
|
OsqueryHostID: ptr.String("orphan-lock-darwin"),
|
|
NodeKey: ptr.String("orphan-lock-darwin"),
|
|
UUID: "orphan-lock-darwin-uuid",
|
|
Platform: "darwin",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// insert a lock_ref pointing to a non-existent MDM command (orphan reference)
|
|
_, err = ds.writer(ctx).ExecContext(ctx,
|
|
`INSERT INTO host_mdm_actions (host_id, lock_ref, fleet_platform) VALUES (?, ?, ?)`,
|
|
host.ID, orphanUUID, "darwin")
|
|
require.NoError(t, err)
|
|
|
|
buf.Reset()
|
|
// should return no error and appear unlocked (not pending lock, not locked)
|
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
require.Nil(t, status.LockMDMCommand)
|
|
require.Contains(t, buf.String(), "orphan lock MDM command reference")
|
|
})
|
|
|
|
t.Run("darwin orphan wipe_ref", func(t *testing.T) {
|
|
host, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "orphan-wipe-darwin",
|
|
OsqueryHostID: ptr.String("orphan-wipe-darwin"),
|
|
NodeKey: ptr.String("orphan-wipe-darwin"),
|
|
UUID: "orphan-wipe-darwin-uuid",
|
|
Platform: "darwin",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx,
|
|
`INSERT INTO host_mdm_actions (host_id, wipe_ref, fleet_platform) VALUES (?, ?, ?)`,
|
|
host.ID, orphanUUID, "darwin")
|
|
require.NoError(t, err)
|
|
|
|
buf.Reset()
|
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
require.Nil(t, status.WipeMDMCommand)
|
|
require.Contains(t, buf.String(), "orphan wipe MDM command reference")
|
|
})
|
|
|
|
t.Run("ios orphan lock_ref", func(t *testing.T) {
|
|
host, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "orphan-lock-ios",
|
|
OsqueryHostID: ptr.String("orphan-lock-ios"),
|
|
NodeKey: ptr.String("orphan-lock-ios"),
|
|
UUID: "orphan-lock-ios-uuid",
|
|
Platform: "ios",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx,
|
|
`INSERT INTO host_mdm_actions (host_id, lock_ref, fleet_platform) VALUES (?, ?, ?)`,
|
|
host.ID, orphanUUID, "ios")
|
|
require.NoError(t, err)
|
|
|
|
buf.Reset()
|
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
require.Nil(t, status.LockMDMCommand)
|
|
require.Contains(t, buf.String(), "orphan lock MDM command reference")
|
|
})
|
|
|
|
t.Run("ios orphan unlock_ref", func(t *testing.T) {
|
|
host, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "orphan-unlock-ios",
|
|
OsqueryHostID: ptr.String("orphan-unlock-ios"),
|
|
NodeKey: ptr.String("orphan-unlock-ios"),
|
|
UUID: "orphan-unlock-ios-uuid",
|
|
Platform: "ios",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx,
|
|
`INSERT INTO host_mdm_actions (host_id, unlock_ref, fleet_platform) VALUES (?, ?, ?)`,
|
|
host.ID, orphanUUID, "ios")
|
|
require.NoError(t, err)
|
|
|
|
buf.Reset()
|
|
status, err := ds.GetHostLockWipeStatus(ctx, host)
|
|
require.NoError(t, err)
|
|
checkLockWipeState(t, status, true, false, false, false, false, false)
|
|
require.Nil(t, status.UnlockMDMCommand)
|
|
require.Contains(t, buf.String(), "orphan unlock MDM command reference")
|
|
})
|
|
}
|
|
|
|
func testScreenDEPAssignProfileSerialsForCooldown(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
skip, assign, err := ds.ScreenDEPAssignProfileSerialsForCooldown(ctx, []string{})
|
|
require.NoError(t, err)
|
|
require.Empty(t, skip)
|
|
require.Empty(t, assign)
|
|
}
|
|
|
|
func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
toks, err := ds.MDMAppleDDMDeclarationsToken(ctx, "not-exists")
|
|
require.NoError(t, err)
|
|
require.Empty(t, toks.DeclarationsToken)
|
|
|
|
decl, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "decl-1",
|
|
Name: "decl-1",
|
|
RawJSON: json.RawMessage(`{"Identifier": "decl-1"}`),
|
|
})
|
|
require.NoError(t, err)
|
|
updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
|
require.NoError(t, err)
|
|
assert.False(t, updates.AppleConfigProfile)
|
|
assert.False(t, updates.AppleDeclaration)
|
|
assert.False(t, updates.WindowsConfigProfile)
|
|
|
|
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, "not-exists")
|
|
require.NoError(t, err)
|
|
require.Empty(t, toks.DeclarationsToken)
|
|
require.NotZero(t, toks.Timestamp)
|
|
|
|
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",
|
|
})
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, host1, true)
|
|
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
|
require.NoError(t, err)
|
|
assert.False(t, updates.AppleConfigProfile)
|
|
assert.True(t, updates.AppleDeclaration)
|
|
assert.False(t, updates.WindowsConfigProfile)
|
|
|
|
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, toks.DeclarationsToken)
|
|
require.NotZero(t, toks.Timestamp)
|
|
oldTok := toks.DeclarationsToken
|
|
|
|
decl2, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "decl-2",
|
|
Name: "decl-2",
|
|
RawJSON: json.RawMessage(`{"Identifier": "decl-2"}`),
|
|
})
|
|
require.NoError(t, err)
|
|
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
|
require.NoError(t, err)
|
|
assert.False(t, updates.AppleConfigProfile)
|
|
assert.True(t, updates.AppleDeclaration)
|
|
assert.False(t, updates.WindowsConfigProfile)
|
|
|
|
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, toks.DeclarationsToken)
|
|
require.NotZero(t, toks.Timestamp)
|
|
require.NotEqual(t, oldTok, toks.DeclarationsToken)
|
|
oldTok = toks.DeclarationsToken
|
|
|
|
err = ds.DeleteMDMAppleDeclaration(ctx, decl.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
|
require.NoError(t, err)
|
|
assert.False(t, updates.AppleConfigProfile)
|
|
assert.False(t, updates.AppleDeclaration) // This is false because we delete references in `host_mdm_apple_declarations` for declarations that aren't sent to the host
|
|
assert.False(t, updates.WindowsConfigProfile)
|
|
|
|
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, toks.DeclarationsToken)
|
|
require.NotZero(t, toks.Timestamp)
|
|
require.NotEqual(t, oldTok, toks.DeclarationsToken)
|
|
}
|
|
|
|
func testMDMAppleSetPendingDeclarationsAs(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
_, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: fmt.Sprintf("decl-%d", i),
|
|
Name: fmt.Sprintf("decl-%d", i),
|
|
RawJSON: json.RawMessage(fmt.Sprintf(`{"Identifier": "decl-%d"}`, i)),
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
checkStatus := func(declarations []fleet.HostMDMAppleProfile, wantStatus fleet.MDMDeliveryStatus, wantDetail string) {
|
|
for _, d := range declarations {
|
|
require.Equal(t, &wantStatus, d.Status)
|
|
require.Equal(t, wantDetail, d.Detail)
|
|
}
|
|
}
|
|
|
|
h, 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)
|
|
nanoEnroll(t, ds, h, true)
|
|
|
|
uuids, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, h.UUID, uuids[0])
|
|
|
|
profs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, profs, 10)
|
|
checkStatus(profs, fleet.MDMDeliveryPending, "")
|
|
|
|
err = ds.MDMAppleSetPendingDeclarationsAs(ctx, h.UUID, &fleet.MDMDeliveryFailed, "mock error")
|
|
require.NoError(t, err)
|
|
profs, err = ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
|
require.NoError(t, err)
|
|
checkStatus(profs, fleet.MDMDeliveryFailed, "mock error")
|
|
}
|
|
|
|
func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
l1, err := ds.NewLabel(ctx, &fleet.Label{Name: "l1", Query: "select 1"})
|
|
require.NoError(t, err)
|
|
l2, err := ds.NewLabel(ctx, &fleet.Label{Name: "l2", Query: "select 2"})
|
|
require.NoError(t, err)
|
|
tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: "tm1"})
|
|
require.NoError(t, err)
|
|
|
|
d1, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1",
|
|
Name: "d1",
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1"}`),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// try to create same name, different identifier fails
|
|
_, err = ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1b",
|
|
Name: "d1",
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
|
})
|
|
require.Error(t, err)
|
|
var existsErr *existsError
|
|
require.ErrorAs(t, err, &existsErr)
|
|
|
|
// try to create different name, same identifier fails
|
|
_, err = ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1",
|
|
Name: "d1b",
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1"}`),
|
|
})
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, &existsErr)
|
|
|
|
// create same declaration for a different team works
|
|
d1tm1, err := ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1",
|
|
Name: "d1",
|
|
TeamID: &tm1.ID,
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1"}`),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, d1.DeclarationUUID, d1tm1.DeclarationUUID)
|
|
|
|
d1Ori, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, d1Ori.LabelsIncludeAll)
|
|
|
|
// update d1 with different identifier and labels
|
|
d1, err = ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1b",
|
|
Name: "d1",
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
|
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID)
|
|
require.NotEqual(t, d1.DeclarationUUID, d1tm1.DeclarationUUID)
|
|
|
|
d1B, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, d1B.LabelsIncludeAll, 1)
|
|
require.Equal(t, l1.ID, d1B.LabelsIncludeAll[0].LabelID)
|
|
|
|
// update d1 with different label
|
|
d1, err = ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1b",
|
|
Name: "d1",
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
|
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l2.Name, LabelID: l2.ID}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID)
|
|
|
|
d1C, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, d1C.LabelsIncludeAll, 1)
|
|
require.Equal(t, l2.ID, d1C.LabelsIncludeAll[0].LabelID)
|
|
|
|
// update d1tm1 with different identifier and label
|
|
d1tm1B, err := ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "i1b",
|
|
Name: "d1",
|
|
TeamID: &tm1.ID,
|
|
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
|
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, d1tm1B.DeclarationUUID, d1tm1.DeclarationUUID)
|
|
|
|
d1tm1B, err = ds.GetMDMAppleDeclaration(ctx, d1tm1B.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, d1tm1B.LabelsIncludeAll, 1)
|
|
require.Equal(t, l1.ID, d1tm1B.LabelsIncludeAll[0].LabelID)
|
|
|
|
// delete no-team d1
|
|
err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, "d1")
|
|
require.NoError(t, err)
|
|
|
|
// it does not exist anymore, but the tm1 one still does
|
|
_, err = ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID)
|
|
require.Error(t, err)
|
|
|
|
d1tm1B, err = ds.GetMDMAppleDeclaration(ctx, d1tm1B.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, d1tm1B.DeclarationUUID, d1tm1.DeclarationUUID)
|
|
}
|
|
|
|
func testDeleteMDMAppleDeclarationWithPendingInstalls(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
decl, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
|
Identifier: "decl-1",
|
|
Name: "decl-1",
|
|
RawJSON: json.RawMessage(`{"Identifier": "decl-1"}`),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
nanoEnroll(t, ds, host, true)
|
|
|
|
_, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// verify the correct state of the declaration for the host
|
|
profs, err := ds.GetHostMDMAppleProfiles(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, profs, 1)
|
|
require.Equal(t, decl.DeclarationUUID, profs[0].ProfileUUID)
|
|
require.Equal(t, fleet.MDMDeliveryPending, *profs[0].Status)
|
|
require.Equal(t, fleet.MDMOperationTypeInstall, profs[0].OperationType)
|
|
|
|
err = ds.DeleteMDMAppleDeclaration(ctx, decl.DeclarationUUID)
|
|
require.NoError(t, err)
|
|
|
|
profs, err = ds.GetHostMDMAppleProfiles(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.Len(t, profs, 0)
|
|
}
|
|
|
|
func TestMDMAppleProfileVerification(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
ctx := t.Context()
|
|
|
|
now := time.Now()
|
|
twoMinutesAgo := now.Add(-2 * time.Minute)
|
|
twoHoursAgo := now.Add(-2 * time.Hour)
|
|
twoDaysAgo := now.Add(-2 * 24 * time.Hour)
|
|
|
|
type testCase struct {
|
|
name string
|
|
initialStatus fleet.MDMDeliveryStatus
|
|
expectedStatus fleet.MDMDeliveryStatus
|
|
expectedDetail string
|
|
}
|
|
|
|
setupTestProfile := func(t *testing.T, suffix string) *fleet.MDMAppleConfigProfile {
|
|
cp, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t,
|
|
fmt.Sprintf("name-test-profile-%s", suffix),
|
|
fmt.Sprintf("identifier-test-profile-%s", suffix),
|
|
fmt.Sprintf("uuid-test-profile-%s", suffix)), nil)
|
|
require.NoError(t, err)
|
|
return cp
|
|
}
|
|
|
|
setProfileUploadedAt := func(t *testing.T, cp *fleet.MDMAppleConfigProfile, ua time.Time) {
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx, `UPDATE mdm_apple_configuration_profiles SET uploaded_at = ? WHERE profile_uuid = ?`, ua, cp.ProfileUUID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
setRetries := func(t *testing.T, hostUUID string, retries uint) {
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET retries = ? WHERE host_uuid = ?`, retries, hostUUID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
checkHostStatus := func(t *testing.T, h *fleet.Host, expectedStatus fleet.MDMDeliveryStatus, expectedDetail string) error {
|
|
gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(gotProfs) != 1 {
|
|
return errors.New("expected exactly one profile")
|
|
}
|
|
if gotProfs[0].Status == nil {
|
|
return errors.New("expected status to be non-nil")
|
|
}
|
|
if *gotProfs[0].Status != expectedStatus {
|
|
return fmt.Errorf("expected status %s, got %s", expectedStatus, *gotProfs[0].Status)
|
|
}
|
|
if gotProfs[0].Detail != expectedDetail {
|
|
return fmt.Errorf("expected detail %s, got %s", expectedDetail, gotProfs[0].Detail)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
initializeProfile := func(t *testing.T, h *fleet.Host, cp *fleet.MDMAppleConfigProfile, status fleet.MDMDeliveryStatus, prevRetries uint) {
|
|
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMOperationTypeInstall, &status, ctx, ds, t)
|
|
require.NoError(t, checkHostStatus(t, h, status, ""))
|
|
setRetries(t, h.UUID, prevRetries)
|
|
}
|
|
|
|
cleanupProfiles := func(t *testing.T) {
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx, `DELETE FROM mdm_apple_configuration_profiles; DELETE FROM host_mdm_apple_profiles`)
|
|
return err
|
|
})
|
|
}
|
|
|
|
t.Run("MissingProfileWithRetry", func(t *testing.T) {
|
|
defer cleanupProfiles(t)
|
|
// missing profile, verifying and verified statuses should change to failed after the grace
|
|
// period and max retries
|
|
cases := []testCase{
|
|
{
|
|
name: "PendingThenMissing",
|
|
initialStatus: fleet.MDMDeliveryPending,
|
|
expectedStatus: fleet.MDMDeliveryPending, // no change
|
|
},
|
|
{
|
|
name: "VerifyingThenMissing",
|
|
initialStatus: fleet.MDMDeliveryVerifying,
|
|
expectedStatus: fleet.MDMDeliveryFailed, // change to failed
|
|
},
|
|
{
|
|
name: "VerifiedThenMissing",
|
|
initialStatus: fleet.MDMDeliveryVerified,
|
|
expectedStatus: fleet.MDMDeliveryFailed, // change to failed
|
|
},
|
|
{
|
|
name: "FailedThenMissing",
|
|
initialStatus: fleet.MDMDeliveryFailed,
|
|
expectedStatus: fleet.MDMDeliveryFailed, // no change
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
// setup
|
|
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
|
|
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
|
|
var reportedProfiles []*fleet.HostMacOSProfile // no profiles reported for this test
|
|
|
|
// initialize
|
|
initializeProfile(t, h, cp, tc.initialStatus, 0)
|
|
|
|
// within grace period
|
|
setProfileUploadedAt(t, cp, twoMinutesAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, "")) // if missing within grace period, no change
|
|
|
|
// reinitialize
|
|
initializeProfile(t, h, cp, tc.initialStatus, 0)
|
|
|
|
// outside grace period
|
|
setProfileUploadedAt(t, cp, twoHoursAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
if tc.expectedStatus == fleet.MDMDeliveryFailed {
|
|
// grace period expired, first failure gets retried so status should be pending and empty detail
|
|
require.NoError(t, checkHostStatus(t, h, fleet.MDMDeliveryPending, ""), tc.name)
|
|
}
|
|
|
|
if tc.initialStatus != fleet.MDMDeliveryPending {
|
|
// simulate retry cycles up to max retries
|
|
for retry := uint(1); retry < fleetmdm.MaxAppleProfileRetries; retry++ {
|
|
// after retry, assume successful install profile command so status should be verifying
|
|
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
// report osquery results with profile still missing
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
// still retrying so status should be pending
|
|
require.NoError(t, checkHostStatus(t, h, fleet.MDMDeliveryPending, ""), tc.name)
|
|
}
|
|
// final retry: after max retries, expect failure
|
|
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, string(fleet.HostMDMProfileDetailFailedWasVerifying)), tc.name) // grace period expired, max retries so check expected status
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("OutdatedProfile", func(t *testing.T) {
|
|
// found profile with the expected identifier, but it's outdated (i.e. the install date is
|
|
// before the last update date) so treat it as missing the expected profile verifying and
|
|
// verified statuses should change to failed after the grace period)
|
|
cases := []testCase{
|
|
{
|
|
name: "PendingThenFoundOutdated",
|
|
initialStatus: fleet.MDMDeliveryPending,
|
|
expectedStatus: fleet.MDMDeliveryPending, // no change
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "VerifyingThenFoundOutdated",
|
|
initialStatus: fleet.MDMDeliveryVerifying,
|
|
expectedStatus: fleet.MDMDeliveryFailed, // change to failed
|
|
expectedDetail: string(fleet.HostMDMProfileDetailFailedWasVerifying),
|
|
},
|
|
{
|
|
name: "VerifiedThenFoundOutdated",
|
|
initialStatus: fleet.MDMDeliveryVerified,
|
|
expectedStatus: fleet.MDMDeliveryFailed, // change to failed
|
|
expectedDetail: string(fleet.HostMDMProfileDetailFailedWasVerified),
|
|
},
|
|
{
|
|
name: "FailedThenFoundOutdated",
|
|
initialStatus: fleet.MDMDeliveryFailed,
|
|
expectedStatus: fleet.MDMDeliveryFailed, // no change
|
|
expectedDetail: "",
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer cleanupProfiles(t)
|
|
|
|
// setup
|
|
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
|
|
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
|
|
reportedProfiles := []*fleet.HostMacOSProfile{
|
|
{
|
|
DisplayName: cp.Name,
|
|
Identifier: cp.Identifier,
|
|
InstallDate: twoDaysAgo,
|
|
},
|
|
}
|
|
|
|
// initialize with no remaining retries
|
|
initializeProfile(t, h, cp, tc.initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// within grace period
|
|
setProfileUploadedAt(t, cp, twoMinutesAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, "")) // outdated profiles are treated similar to missing profiles so status doesn't change if within grace period
|
|
|
|
// reinitalize with no remaining retries
|
|
initializeProfile(t, h, cp, tc.initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// outside grace period
|
|
setProfileUploadedAt(t, cp, twoHoursAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("ExpectedProfile", func(t *testing.T) {
|
|
// happy path, expected profile found so verifying should change to verified
|
|
cases := []testCase{
|
|
{
|
|
name: "PendingThenFoundExpected",
|
|
initialStatus: fleet.MDMDeliveryPending,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // pending can go to verified if found
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "VerifyingThenFoundExpected",
|
|
initialStatus: fleet.MDMDeliveryVerifying,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // change to verified
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "VerifiedThenFoundExpected",
|
|
initialStatus: fleet.MDMDeliveryVerified,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // no change
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "FailedThenFoundExpected",
|
|
initialStatus: fleet.MDMDeliveryFailed,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // failed can become verified if found later
|
|
expectedDetail: "",
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer cleanupProfiles(t)
|
|
|
|
// setup
|
|
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
|
|
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
|
|
reportedProfiles := []*fleet.HostMacOSProfile{
|
|
{
|
|
DisplayName: cp.Name,
|
|
Identifier: cp.Identifier,
|
|
InstallDate: now,
|
|
},
|
|
}
|
|
|
|
// initialize with no remaining retries
|
|
initializeProfile(t, h, cp, tc.initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// within grace period
|
|
setProfileUploadedAt(t, cp, twoMinutesAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // if found within grace period, verifying status can become verified so check expected status
|
|
|
|
// reinitializewith no remaining retries
|
|
initializeProfile(t, h, cp, tc.initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// outside grace period
|
|
setProfileUploadedAt(t, cp, twoHoursAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("UnexpectedProfile", func(t *testing.T) {
|
|
// unexpected profile is ignored and doesn't change status of existing profile
|
|
cases := []testCase{
|
|
{
|
|
name: "PendingThenFoundExpectedAndUnexpected",
|
|
initialStatus: fleet.MDMDeliveryPending,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // profile can go from pending to verified
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "VerifyingThenFoundExpectedAndUnexpected",
|
|
initialStatus: fleet.MDMDeliveryVerifying,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // no change
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "VerifiedThenFounExpectedAnddUnexpected",
|
|
initialStatus: fleet.MDMDeliveryVerified,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // no change
|
|
expectedDetail: "",
|
|
},
|
|
{
|
|
name: "FailedThenFoundExpectedAndUnexpected",
|
|
initialStatus: fleet.MDMDeliveryFailed,
|
|
expectedStatus: fleet.MDMDeliveryVerified, // failed can become verified if found later
|
|
expectedDetail: "",
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer cleanupProfiles(t)
|
|
|
|
// setup
|
|
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
|
|
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
|
|
reportedProfiles := []*fleet.HostMacOSProfile{
|
|
{
|
|
DisplayName: "unexpected-name",
|
|
Identifier: "unexpected-identifier",
|
|
InstallDate: now,
|
|
},
|
|
{
|
|
DisplayName: cp.Name,
|
|
Identifier: cp.Identifier,
|
|
InstallDate: now,
|
|
},
|
|
}
|
|
|
|
// initialize with no remaining retries
|
|
initializeProfile(t, h, cp, tc.initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// within grace period
|
|
setProfileUploadedAt(t, cp, twoMinutesAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // if found within grace period, verifying status can become verified so check expected status
|
|
|
|
// reinitialize with no remaining retries
|
|
initializeProfile(t, h, cp, tc.initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// outside grace period
|
|
setProfileUploadedAt(t, cp, twoHoursAgo)
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("EarliestInstallDate", func(t *testing.T) {
|
|
defer cleanupProfiles(t)
|
|
|
|
hostString := "host-earliest-install-date"
|
|
h := test.NewHost(t, ds, hostString, hostString, hostString, hostString, twoMinutesAgo)
|
|
|
|
cp := configProfileForTest(t,
|
|
fmt.Sprintf("name-test-profile-%s", hostString),
|
|
fmt.Sprintf("identifier-test-profile-%s", hostString),
|
|
fmt.Sprintf("uuid-test-profile-%s", hostString))
|
|
|
|
// save the config profile to no team
|
|
stored0, err := ds.NewMDMAppleConfigProfile(ctx, *cp, nil)
|
|
require.NoError(t, err)
|
|
|
|
reportedProfiles := []*fleet.HostMacOSProfile{
|
|
{
|
|
DisplayName: cp.Name,
|
|
Identifier: cp.Identifier,
|
|
InstallDate: twoDaysAgo,
|
|
},
|
|
}
|
|
initialStatus := fleet.MDMDeliveryVerifying
|
|
|
|
// initialize with no remaining retries
|
|
initializeProfile(t, h, stored0, initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// within grace period
|
|
setProfileUploadedAt(t, stored0, twoMinutesAgo) // host is out of date but still within grace period
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, fleet.MDMDeliveryVerifying, "")) // no change
|
|
|
|
// reinitialize with no remaining retries
|
|
initializeProfile(t, h, stored0, initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// outside grace period
|
|
setProfileUploadedAt(t, stored0, twoHoursAgo) // host is out of date and grace period has passed
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, fleet.MDMDeliveryFailed, string(fleet.HostMDMProfileDetailFailedWasVerifying))) // set to failed
|
|
|
|
// reinitialize with no remaining retries
|
|
initializeProfile(t, h, stored0, initialStatus, fleetmdm.MaxAppleProfileRetries)
|
|
|
|
// save a copy of the config profile to team 1
|
|
cp.TeamID = ptr.Uint(1)
|
|
stored1, err := ds.NewMDMAppleConfigProfile(ctx, *cp, nil)
|
|
require.NoError(t, err)
|
|
|
|
setProfileUploadedAt(t, stored0, twoHoursAgo) // host would be out of date based on this copy of the profile record
|
|
setProfileUploadedAt(t, stored1, twoDaysAgo.Add(-1*time.Hour)) // BUT this record now establishes the earliest install date
|
|
|
|
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, ds, h, profilesByIdentifier(reportedProfiles)))
|
|
require.NoError(t, checkHostStatus(t, h, fleet.MDMDeliveryVerified, "")) // set to verified based on earliest install date
|
|
})
|
|
}
|
|
|
|
func profilesByIdentifier(profiles []*fleet.HostMacOSProfile) map[string]*fleet.HostMacOSProfile {
|
|
byIdentifier := map[string]*fleet.HostMacOSProfile{}
|
|
for _, p := range profiles {
|
|
byIdentifier[p.Identifier] = p
|
|
}
|
|
return byIdentifier
|
|
}
|
|
|
|
func TestRestorePendingDEPHost(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
defer ds.Close()
|
|
|
|
ctx := t.Context()
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
expectedMDMServerURL, err := apple_mdm.ResolveAppleEnrollMDMURL(ac.ServerSettings.ServerURL)
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
t.Run("DEP enrollment", func(t *testing.T) {
|
|
checkHostExistsInTable := func(t *testing.T, tableName string, hostID uint, expected bool, where ...string) {
|
|
stmt := "SELECT 1 FROM " + tableName + " WHERE host_id = ?"
|
|
if len(where) != 0 {
|
|
stmt += " AND " + strings.Join(where, " AND ")
|
|
}
|
|
var exists bool
|
|
err := sqlx.GetContext(ctx, ds.primary, &exists, stmt, hostID)
|
|
if expected {
|
|
require.NoError(t, err, tableName)
|
|
require.True(t, exists, tableName)
|
|
} else {
|
|
require.ErrorIs(t, err, sql.ErrNoRows, tableName)
|
|
require.False(t, exists, tableName)
|
|
}
|
|
}
|
|
|
|
checkStoredHost := func(t *testing.T, hostID uint, expectedHost *fleet.Host) {
|
|
h, err := ds.Host(ctx, hostID)
|
|
if expectedHost != nil {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, h)
|
|
require.Equal(t, expectedHost.ID, h.ID)
|
|
require.Equal(t, expectedHost.OrbitNodeKey, h.OrbitNodeKey)
|
|
require.Equal(t, expectedHost.HardwareModel, h.HardwareModel)
|
|
require.Equal(t, expectedHost.HardwareSerial, h.HardwareSerial)
|
|
require.Equal(t, expectedHost.UUID, h.UUID)
|
|
require.Equal(t, expectedHost.Platform, h.Platform)
|
|
require.Equal(t, expectedHost.TeamID, h.TeamID)
|
|
} else {
|
|
nfe := &common_mysql.NotFoundError{}
|
|
require.ErrorAs(t, err, &nfe)
|
|
}
|
|
|
|
for _, table := range []string{
|
|
"host_mdm",
|
|
"host_display_names",
|
|
// "label_membership", // TODO: uncomment this if/when we add the builtin labels to the mysql test setup
|
|
} {
|
|
checkHostExistsInTable(t, table, hostID, expectedHost != nil)
|
|
}
|
|
|
|
// host DEP assignment row is NEVER deleted
|
|
checkHostExistsInTable(t, "host_dep_assignments", hostID, true, "deleted_at IS NULL")
|
|
}
|
|
|
|
setupTestHost := func(t *testing.T) (pendingHost, mdmEnrolledHost *fleet.Host) {
|
|
depSerial := "dep-serial"
|
|
depUUID := "dep-uuid"
|
|
depOrbitNodeKey := "dep-orbit-node-key"
|
|
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{{SerialNumber: depSerial}}, abmToken.ID, nil, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), n)
|
|
|
|
var depHostID uint
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &depHostID, "SELECT id FROM hosts WHERE hardware_serial = ?", depSerial)
|
|
require.NoError(t, err)
|
|
|
|
// host MDM row is created when DEP device is ingested
|
|
pendingHost, err = ds.Host(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, pendingHost)
|
|
require.Equal(t, depHostID, pendingHost.ID)
|
|
require.Equal(t, "Pending", *pendingHost.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, pendingHost.MDM.Name)
|
|
require.Nil(t, pendingHost.OsqueryHostID)
|
|
|
|
// host DEP assignment is created when DEP device is ingested
|
|
depAssignment, err := ds.GetHostDEPAssignment(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, depHostID, depAssignment.HostID)
|
|
require.Nil(t, depAssignment.DeletedAt)
|
|
require.WithinDuration(t, time.Now(), depAssignment.AddedAt, 5*time.Second)
|
|
|
|
// simulate initial osquery enrollment via Orbit
|
|
h, err := ds.EnrollOrbit(ctx,
|
|
fleet.WithEnrollOrbitMDMEnabled(true),
|
|
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{
|
|
HardwareSerial: depSerial,
|
|
Platform: "darwin",
|
|
HardwareUUID: depUUID,
|
|
Hostname: "dep-host",
|
|
}),
|
|
fleet.WithEnrollOrbitNodeKey(depOrbitNodeKey),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, h)
|
|
require.Equal(t, depHostID, h.ID)
|
|
|
|
// simulate osquery report of MDM detail query
|
|
err = ds.SetOrUpdateMDMData(ctx, depHostID, false, true, expectedMDMServerURL, true, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
// enrollment status changes to "On (automatic)"
|
|
mdmEnrolledHost, err = ds.Host(ctx, depHostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "On (automatic)", *mdmEnrolledHost.MDM.EnrollmentStatus)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, mdmEnrolledHost.MDM.Name)
|
|
require.Equal(t, depUUID, *mdmEnrolledHost.OsqueryHostID)
|
|
|
|
return pendingHost, mdmEnrolledHost
|
|
}
|
|
|
|
pendingHost, mdmEnrolledHost := setupTestHost(t)
|
|
require.Equal(t, pendingHost.ID, mdmEnrolledHost.ID)
|
|
checkStoredHost(t, mdmEnrolledHost.ID, mdmEnrolledHost)
|
|
|
|
// delete the host from Fleet
|
|
err = ds.DeleteHost(ctx, mdmEnrolledHost.ID)
|
|
require.NoError(t, err)
|
|
checkStoredHost(t, mdmEnrolledHost.ID, nil)
|
|
|
|
// host is restored
|
|
err = ds.RestoreMDMApplePendingDEPHost(ctx, mdmEnrolledHost)
|
|
require.NoError(t, err)
|
|
expectedHost := *pendingHost
|
|
// host uuid is preserved for restored hosts. It isn't available via DEP so the original
|
|
// pending host record did not include it so we add it to our expected host here.
|
|
expectedHost.UUID = mdmEnrolledHost.UUID
|
|
checkStoredHost(t, mdmEnrolledHost.ID, &expectedHost)
|
|
})
|
|
}
|
|
|
|
func testMDMAppleDEPAssignmentUpdates(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
n := t.Name()
|
|
h, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: fmt.Sprintf("test-host%s-name", n),
|
|
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%s", n)),
|
|
NodeKey: ptr.String(fmt.Sprintf("nodekey-%s", n)),
|
|
UUID: fmt.Sprintf("test-uuid-%s", n),
|
|
Platform: "darwin",
|
|
HardwareSerial: n,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
_, err = ds.GetHostDEPAssignment(ctx, h.ID)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmToken.ID, make(map[uint]time.Time))
|
|
require.NoError(t, err)
|
|
|
|
assignment, err := ds.GetHostDEPAssignment(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, h.ID, assignment.HostID)
|
|
require.Nil(t, assignment.DeletedAt)
|
|
// profile fields are nil before any assignment response has been recorded
|
|
require.Nil(t, assignment.ProfileUUID)
|
|
require.Nil(t, assignment.AssignProfileResponse)
|
|
require.Nil(t, assignment.ResponseUpdatedAt)
|
|
|
|
// simulate a successful profile assignment response from Apple
|
|
profileUUID := uuid.NewString()
|
|
err = ds.UpdateHostDEPAssignProfileResponses(ctx, &godep.ProfileResponse{
|
|
ProfileUUID: profileUUID,
|
|
Devices: map[string]string{h.HardwareSerial: string(fleet.DEPAssignProfileResponseSuccess)},
|
|
}, abmToken.ID)
|
|
require.NoError(t, err)
|
|
|
|
beforeDelete, err := ds.GetHostDEPAssignment(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, h.ID, beforeDelete.HostID)
|
|
require.Nil(t, beforeDelete.DeletedAt)
|
|
// profile fields are now populated
|
|
require.NotNil(t, beforeDelete.ProfileUUID)
|
|
require.Equal(t, profileUUID, *beforeDelete.ProfileUUID)
|
|
require.NotNil(t, beforeDelete.AssignProfileResponse)
|
|
require.Equal(t, fleet.DEPAssignProfileResponseSuccess, *beforeDelete.AssignProfileResponse)
|
|
require.NotNil(t, beforeDelete.ResponseUpdatedAt)
|
|
require.WithinDuration(t, time.Now(), *beforeDelete.ResponseUpdatedAt, 5*time.Second)
|
|
|
|
// simulate a failed profile assignment response — fields should be updated
|
|
profileUUID2 := uuid.NewString()
|
|
err = ds.UpdateHostDEPAssignProfileResponses(ctx, &godep.ProfileResponse{
|
|
ProfileUUID: profileUUID2,
|
|
Devices: map[string]string{h.HardwareSerial: string(fleet.DEPAssignProfileResponseFailed)},
|
|
}, abmToken.ID)
|
|
require.NoError(t, err)
|
|
|
|
afterFail, err := ds.GetHostDEPAssignment(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, afterFail.ProfileUUID)
|
|
require.Equal(t, profileUUID2, *afterFail.ProfileUUID)
|
|
require.NotNil(t, afterFail.AssignProfileResponse)
|
|
require.Equal(t, fleet.DEPAssignProfileResponseFailed, *afterFail.AssignProfileResponse)
|
|
require.NotNil(t, afterFail.ResponseUpdatedAt)
|
|
|
|
err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{h.HardwareSerial})
|
|
require.NoError(t, err)
|
|
|
|
assignment, err = ds.GetHostDEPAssignment(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, h.ID, assignment.HostID)
|
|
require.NotNil(t, assignment.DeletedAt)
|
|
|
|
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmToken.ID, make(map[uint]time.Time))
|
|
require.NoError(t, err)
|
|
assignment, err = ds.GetHostDEPAssignment(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, h.ID, assignment.HostID)
|
|
require.Nil(t, assignment.DeletedAt)
|
|
// profile fields survive an upsert (the upsert only resets added_at/deleted_at)
|
|
require.NotNil(t, assignment.ProfileUUID)
|
|
require.Equal(t, profileUUID2, *assignment.ProfileUUID)
|
|
require.NotNil(t, assignment.AssignProfileResponse)
|
|
require.Equal(t, fleet.DEPAssignProfileResponseFailed, *assignment.AssignProfileResponse)
|
|
}
|
|
|
|
func createRawAppleCmd(reqType, cmdUUID string) string {
|
|
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Command</key>
|
|
<dict>
|
|
<key>ManagedOnly</key>
|
|
<false/>
|
|
<key>RequestType</key>
|
|
<string>%s</string>
|
|
</dict>
|
|
<key>CommandUUID</key>
|
|
<string>%s</string>
|
|
</dict>
|
|
</plist>`, 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"},
|
|
{SerialNumber: "iPod_SERIAL", DeviceFamily: "iPod", OpType: "added"},
|
|
}
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices, abmToken.ID, nil, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(3), 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)
|
|
|
|
err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
|
|
UUID: "iPod_UUID",
|
|
HardwareSerial: "iPod_SERIAL",
|
|
HardwareModel: "iPod 7",
|
|
Platform: "ios",
|
|
OsqueryHostID: ptr.String("iPod_OSQUERY_HOST_ID"),
|
|
}, false)
|
|
require.NoError(t, err)
|
|
iPod, err := ds.HostByIdentifier(ctx, "iPod_SERIAL")
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, iPod, 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, 3)
|
|
uuids := []string{devices[0].UUID, devices[1].UUID, devices[2].UUID}
|
|
sort.Slice(uuids, func(i, j int) bool {
|
|
return uuids[i] < uuids[j]
|
|
})
|
|
assert.Equal(t, uuids, []string{"iOS0_UUID", "iPadOS0_UUID", "iPod_UUID"})
|
|
assert.Empty(t, devices[0].CommandsAlreadySent)
|
|
assert.Empty(t, devices[1].CommandsAlreadySent)
|
|
assert.Empty(t, devices[2].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, 2)
|
|
require.Equal(t, devices[0].UUID, "iPadOS0_UUID")
|
|
require.Equal(t, devices[1].UUID, "iPod_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
|
|
})
|
|
|
|
// two devices are up to date now, only iPod should be returned
|
|
devices, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
|
|
require.NoError(t, err)
|
|
require.Len(t, devices, 1)
|
|
require.Equal(t, devices[0].UUID, "iPod_UUID")
|
|
|
|
// set iPod device 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 = ?`, iPod.ID)
|
|
return err
|
|
})
|
|
|
|
// all devices are up to date now, 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"},
|
|
{SerialNumber: "iPod_SERIAL", DeviceFamily: "iPod", 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(3), 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, 3)
|
|
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)
|
|
require.Equal(t, "ios", hosts[2].Platform)
|
|
require.Equal(t, false, hosts[2].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)
|
|
err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
|
|
UUID: "iPod_UUID",
|
|
HardwareSerial: "iPod_SERIAL",
|
|
HardwareModel: "iPod 7",
|
|
Platform: "ios",
|
|
OsqueryHostID: ptr.String("iPod_OSQUERY_HOST_ID"),
|
|
}, false)
|
|
require.NoError(t, err)
|
|
iPod, err := ds.HostByIdentifier(ctx, "iPod_UUID")
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, iPod, 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)
|
|
profiles, err = ds.GetHostMDMAppleProfiles(ctx, "iPod_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)
|
|
|
|
firstIds, 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]}, firstIds)
|
|
|
|
// Get a list of pending ID's 3 times and match against the first list, to avoid test flakiness.
|
|
// In a real world scenario it's okay for it to be the same as we will fetch it again later.
|
|
allAttemptsWereEqual := true
|
|
for range 3 {
|
|
secondIds, err := ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx)
|
|
require.NoError(t, err)
|
|
isIdsEqual := assert.ObjectsAreEqualValues(firstIds, secondIds)
|
|
if !isIdsEqual {
|
|
allAttemptsWereEqual = false
|
|
break
|
|
}
|
|
}
|
|
require.False(t, allAttemptsWereEqual, "GetEnrollmentIDsWithPendingMDMAppleCommands returned the same result 3 times in a row")
|
|
|
|
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 (?, ?, '<?xml', 'None')`, commandUUID, requestType)
|
|
require.NoError(t, err)
|
|
|
|
// Insert into nano_enrollment_queue
|
|
_, err = q.ExecContext(ctx, `INSERT INTO nano_enrollment_queue (id, command_uuid, active, priority) VALUES (?, ?, 1, 0)`, hostUUID, commandUUID)
|
|
require.NoError(t, err)
|
|
|
|
// Insert into nano_command_results
|
|
_, err = q.ExecContext(ctx, `INSERT INTO nano_command_results (id, command_uuid, status, result, not_now_tally) VALUES (?, ?, 'Acknowledged', '<?xml', 0)`, hostUUID, commandUUID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func testSetLockCommandForLostModeCheckin(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
hostID := uint(1)
|
|
commandUUID := uuid.NewString()
|
|
|
|
// Insert successfully
|
|
err := ds.SetLockCommandForLostModeCheckin(ctx, hostID, commandUUID)
|
|
require.NoError(t, err)
|
|
|
|
// Fails if trying to insert on existing row
|
|
err = ds.SetLockCommandForLostModeCheckin(ctx, hostID, commandUUID)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func testDeviceLocation(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
iOSHost := newTestHostWithPlatform(t, ds, "iphone_"+t.Name(), string(fleet.IOSPlatform), nil)
|
|
|
|
expected := fleet.HostLocationData{HostID: iOSHost.ID, Latitude: 42.42, Longitude: -42.42}
|
|
err := ds.InsertHostLocationData(ctx, expected)
|
|
require.NoError(t, err)
|
|
|
|
locData, err := ds.GetHostLocationData(ctx, iOSHost.ID)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, expected, *locData)
|
|
|
|
// Update data
|
|
expected = fleet.HostLocationData{HostID: iOSHost.ID, Latitude: 20.25, Longitude: 20.25}
|
|
err = ds.InsertHostLocationData(ctx, expected)
|
|
require.NoError(t, err)
|
|
|
|
locData, err = ds.GetHostLocationData(ctx, iOSHost.ID)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, expected, *locData)
|
|
|
|
err = ds.DeleteHostLocationData(ctx, iOSHost.ID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.GetHostLocationData(ctx, iOSHost.ID)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testGetDEPAssignProfileExpiredCooldowns(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
host := newTestHostWithPlatform(t, ds, "macos", "macos", nil)
|
|
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO host_dep_assignments (host_id, profile_uuid) VALUES (?, ?)`, host.ID, uuid.NewString())
|
|
return err
|
|
})
|
|
|
|
cooldowns, err := ds.GetDEPAssignProfileExpiredCooldowns(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cooldowns, 0, "no failed assign response")
|
|
|
|
// Set failed and assigned response within the last minute
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE host_dep_assignments SET assign_profile_response = ?, retry_job_id = 0, response_updated_at = NOW() WHERE host_id = ?`, fleet.DEPAssignProfileResponseFailed, host.ID)
|
|
return err
|
|
})
|
|
cooldowns, err = ds.GetDEPAssignProfileExpiredCooldowns(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cooldowns, 0, "failed but still in cooldown")
|
|
|
|
// Set response_updated_at to be dep cooldown + 10 seconds to avoid timing issues
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE host_dep_assignments SET response_updated_at = DATE_SUB(NOW(), INTERVAL ? SECOND) WHERE host_id = ?`, depFailedCooldownPeriod.Seconds()+10, host.ID)
|
|
return err
|
|
})
|
|
cooldowns, err = ds.GetDEPAssignProfileExpiredCooldowns(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cooldowns, 1, "failed and cooldown expired")
|
|
|
|
// Generate 200 entries to test limit and order by
|
|
for i := range 200 {
|
|
h := newTestHostWithPlatform(t, ds, fmt.Sprintf("host-%d", i), "macos", nil)
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO host_dep_assignments (host_id, profile_uuid, assign_profile_response, response_updated_at, retry_job_id) VALUES (?, ?, ?, DATE_SUB(NOW(), INTERVAL ? SECOND), 0)`, h.ID, uuid.NewString(), fleet.DEPAssignProfileResponseFailed, depFailedCooldownPeriod.Seconds()+10)
|
|
return err
|
|
})
|
|
}
|
|
|
|
cooldowns, err = ds.GetDEPAssignProfileExpiredCooldowns(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cooldowns, 1, "only expect no team ID")
|
|
allSerials := []string{}
|
|
for _, cd := range cooldowns {
|
|
allSerials = append(allSerials, cd...)
|
|
}
|
|
require.Len(t, allSerials, apple_mdm.DEPSyncLimit, "limit process cooldowns to sync limit")
|
|
require.LessOrEqual(t, len(allSerials), 1000, "never go above 1000 devices as per Apple's recommendations")
|
|
}
|
|
|
|
func testMDMAppleHostsDiskEncryption(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
|
|
require.NoError(t, err)
|
|
|
|
hostCountEncryptionStatus := func(status fleet.DiskEncryptionStatus, teamID *uint) int {
|
|
gotHosts, err := ds.ListHosts(ctx,
|
|
fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}},
|
|
fleet.HostListOptions{OSSettingsDiskEncryptionFilter: status, TeamFilter: teamID},
|
|
)
|
|
require.NoError(t, err)
|
|
return len(gotHosts)
|
|
}
|
|
|
|
hostUnenrolled := test.NewHost(t, ds, "host-unenrolled", "", "key-1", "uuid-1", time.Now())
|
|
hostEnrolled := test.NewHost(t, ds, "host-enrolled", "", "key-2", "uuid-2", time.Now())
|
|
nanoEnrollUserDeviceAndSetHostMDMData(t, ds, hostEnrolled)
|
|
|
|
FVProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP(fleetmdm.FleetFileVaultProfileName, mobileconfig.FleetFileVaultPayloadIdentifier, team.ID), nil)
|
|
require.NoError(t, err)
|
|
|
|
upsertHostCPs(
|
|
[]*fleet.Host{hostUnenrolled},
|
|
[]*fleet.MDMAppleConfigProfile{FVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryVerifying,
|
|
ctx, ds, t,
|
|
)
|
|
oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute)
|
|
createDiskEncryptionRecord(ctx, ds, t, hostUnenrolled, "key-1", true, oneMinuteAfterThreshold)
|
|
|
|
fvProfileSummary, err := ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(0), fvProfileSummary.Verifying)
|
|
require.Equal(t, 0, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
|
|
upsertHostCPs(
|
|
[]*fleet.Host{hostEnrolled},
|
|
[]*fleet.MDMAppleConfigProfile{FVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryVerifying,
|
|
ctx, ds, t,
|
|
)
|
|
oneMinuteAfterThreshold = time.Now().Add(+1 * time.Minute)
|
|
createDiskEncryptionRecord(ctx, ds, t, hostEnrolled, "key-2", true, oneMinuteAfterThreshold)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Verifying)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionVerifying, nil))
|
|
|
|
upsertHostCPs(
|
|
[]*fleet.Host{hostEnrolled},
|
|
[]*fleet.MDMAppleConfigProfile{FVProfile},
|
|
fleet.MDMOperationTypeInstall,
|
|
&fleet.MDMDeliveryPending,
|
|
ctx, ds, t,
|
|
)
|
|
|
|
fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fvProfileSummary)
|
|
require.Equal(t, uint(1), fvProfileSummary.Enforcing)
|
|
require.Equal(t, 1, hostCountEncryptionStatus(fleet.DiskEncryptionEnforcing, nil))
|
|
}
|
|
|
|
func testDeleteMDMAppleDeclarationByNameCancelsInstalls(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
SetTestABMAssets(t, ds, "fleet")
|
|
|
|
runTest := func(t *testing.T, teamID *uint) {
|
|
// Create two declarations.
|
|
appleDecls := []*fleet.MDMAppleDeclaration{
|
|
declForTest("D1", "D1", "{}"),
|
|
declForTest("D2", "D2", "{}"),
|
|
}
|
|
_, err := ds.BatchSetMDMProfiles(ctx, teamID, nil, nil, appleDecls, nil, nil)
|
|
require.NoError(t, err)
|
|
declNameToDeclarations := make(map[string]*fleet.MDMConfigProfilePayload)
|
|
profs, _, err := ds.ListMDMConfigProfiles(ctx, teamID, fleet.ListOptions{})
|
|
require.NoError(t, err)
|
|
for _, prof := range profs {
|
|
declNameToDeclarations[prof.Name] = prof
|
|
}
|
|
|
|
// Delete declaration D1.
|
|
err = ds.DeleteMDMAppleDeclarationByName(ctx, teamID, "D1")
|
|
require.NoError(t, err)
|
|
|
|
// Create two hosts.
|
|
var opts []test.NewHostOption
|
|
if teamID != nil {
|
|
opts = append(opts, test.WithTeamID(*teamID))
|
|
}
|
|
host1 := test.NewHost(t, ds, "host1", "1"+t.Name(), "h1key"+t.Name(), "host1uuid"+t.Name(), time.Now(), opts...)
|
|
host2 := test.NewHost(t, ds, "host2", "2"+t.Name(), "h2key"+t.Name(), "host2uuid"+t.Name(), time.Now(), opts...)
|
|
nanoEnroll(t, ds, host1, false)
|
|
nanoEnroll(t, ds, host2, false)
|
|
|
|
for _, h := range []*fleet.Host{host1, host2} {
|
|
err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://fleetdm.com", false, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Set the D2 declaration as pending install on host1, installed on host2
|
|
forceSetAppleHostDeclarationStatus(t, ds, host1.UUID, test.ToMDMAppleDecl(declNameToDeclarations["D2"]), fleet.MDMOperationTypeInstall, "")
|
|
forceSetAppleHostDeclarationStatus(t, ds, host2.UUID, test.ToMDMAppleDecl(declNameToDeclarations["D2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
|
|
assertHostProfileOpStatus(t, ds, host1.UUID,
|
|
hostProfileOpStatus{declNameToDeclarations["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall})
|
|
assertHostProfileOpStatus(t, ds, host2.UUID,
|
|
hostProfileOpStatus{declNameToDeclarations["D2"].ProfileUUID, fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall})
|
|
|
|
// Delete declaration D2.
|
|
err = ds.DeleteMDMAppleDeclarationByName(ctx, teamID, "D2")
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileOpStatus(t, ds, host1.UUID)
|
|
assertHostProfileOpStatus(t, ds, host2.UUID,
|
|
hostProfileOpStatus{declNameToDeclarations["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
|
|
|
|
// Deleting unexisting declaration should not fail.
|
|
err = ds.DeleteMDMAppleDeclarationByName(ctx, teamID, "D3")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("No team", func(t *testing.T) {
|
|
runTest(t, nil)
|
|
})
|
|
|
|
t.Run("Team", func(t *testing.T) {
|
|
team, err := ds.NewTeam(t.Context(), &fleet.Team{
|
|
Name: t.Name(),
|
|
})
|
|
require.NoError(t, err)
|
|
runTest(t, &team.ID)
|
|
})
|
|
}
|
|
|
|
func testRecoveryLockPasswordSetAndGet(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
host := test.NewHost(t, ds, "test-host-1", "1.2.3.4", "h1key", "h1uuid", time.Now())
|
|
|
|
// Generate and set password
|
|
password := apple_mdm.GenerateRecoveryLockPassword()
|
|
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: password}})
|
|
require.NoError(t, err)
|
|
|
|
// Get password and verify it matches
|
|
result, err := ds.GetHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, password, result.Password)
|
|
assert.False(t, result.UpdatedAt.IsZero())
|
|
}
|
|
|
|
func testRecoveryLockPasswordBulkSet(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Create multiple hosts
|
|
host1 := test.NewHost(t, ds, "bulk-host-1", "1.2.3.10", "bulk1key", "bulk1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "bulk-host-2", "1.2.3.11", "bulk2key", "bulk2uuid", time.Now())
|
|
host3 := test.NewHost(t, ds, "bulk-host-3", "1.2.3.12", "bulk3key", "bulk3uuid", time.Now())
|
|
|
|
// Generate passwords for all hosts
|
|
pw1 := apple_mdm.GenerateRecoveryLockPassword()
|
|
pw2 := apple_mdm.GenerateRecoveryLockPassword()
|
|
pw3 := apple_mdm.GenerateRecoveryLockPassword()
|
|
|
|
// Bulk set passwords
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{
|
|
{HostUUID: host1.UUID, Password: pw1},
|
|
{HostUUID: host2.UUID, Password: pw2},
|
|
{HostUUID: host3.UUID, Password: pw3},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify all passwords are stored correctly
|
|
result1, err := ds.GetHostRecoveryLockPassword(ctx, host1.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, pw1, result1.Password)
|
|
|
|
result2, err := ds.GetHostRecoveryLockPassword(ctx, host2.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, pw2, result2.Password)
|
|
|
|
result3, err := ds.GetHostRecoveryLockPassword(ctx, host3.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, pw3, result3.Password)
|
|
|
|
// Verify all passwords are different
|
|
assert.NotEqual(t, pw1, pw2)
|
|
assert.NotEqual(t, pw2, pw3)
|
|
assert.NotEqual(t, pw1, pw3)
|
|
}
|
|
|
|
func testRecoveryLockPasswordGetNotFound(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Try to get password for non-existent host
|
|
_, err := ds.GetHostRecoveryLockPassword(ctx, "non-existent-uuid")
|
|
require.Error(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testRecoveryLockPasswordSetOverwrite(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
host := test.NewHost(t, ds, "test-host-2", "1.2.3.5", "h2key", "h2uuid", time.Now())
|
|
|
|
// Set password first time
|
|
password1 := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: password1}})
|
|
require.NoError(t, err)
|
|
|
|
// Set password second time (should overwrite)
|
|
password2 := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: password2}})
|
|
require.NoError(t, err)
|
|
|
|
// Passwords should be different (randomly generated)
|
|
assert.NotEqual(t, password1, password2)
|
|
|
|
// Verify only the new password is stored
|
|
result, err := ds.GetHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, password2, result.Password)
|
|
}
|
|
|
|
func testRecoveryLockPasswordUpdatedAtChanges(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
host := test.NewHost(t, ds, "test-host-3", "1.2.3.6", "h3key", "h3uuid", time.Now())
|
|
|
|
// Set password first time
|
|
password1 := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: password1}})
|
|
require.NoError(t, err)
|
|
|
|
result1, err := ds.GetHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Wait a bit to ensure timestamp changes
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Set password second time
|
|
password2 := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: password2}})
|
|
require.NoError(t, err)
|
|
|
|
result2, err := ds.GetHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// updated_at should have changed
|
|
assert.True(t, result2.UpdatedAt.After(result1.UpdatedAt), "updated_at should increase after overwrite")
|
|
}
|
|
|
|
func testRecoveryLockStatusMethods(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Helper to create a host with a recovery lock password (status is set to 'pending' atomically)
|
|
setupHost := func(t *testing.T, name, ip, key, uuid string) *fleet.Host {
|
|
t.Helper()
|
|
host := test.NewHost(t, ds, name, ip, key, uuid, time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
return host
|
|
}
|
|
|
|
t.Run("SetHostsRecoveryLockPasswords sets pending status atomically", func(t *testing.T) {
|
|
host := setupHost(t, "atomic-pending-host", "1.2.3.6", "atomickey", "atomicuuid")
|
|
|
|
// Verify status is pending immediately after storing password
|
|
var status string
|
|
err := ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryPending), status)
|
|
})
|
|
|
|
t.Run("SetRecoveryLockVerified", func(t *testing.T) {
|
|
host := setupHost(t, "verified-host", "1.2.3.9", "verifiedkey", "verifieduuid")
|
|
|
|
// Set verified status
|
|
err := ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify status
|
|
var status string
|
|
err = ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), status)
|
|
})
|
|
|
|
t.Run("SetRecoveryLockFailed", func(t *testing.T) {
|
|
host := setupHost(t, "failed-host", "1.2.3.10", "failedkey", "faileduuid")
|
|
|
|
// Set failed status
|
|
err := ds.SetRecoveryLockFailed(ctx, host.UUID, "test error message")
|
|
require.NoError(t, err)
|
|
|
|
// Verify status and error message
|
|
var status, errorMsg string
|
|
err = ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryFailed), status)
|
|
|
|
err = ds.writer(ctx).GetContext(ctx, &errorMsg, "SELECT error_message FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test error message", errorMsg)
|
|
})
|
|
|
|
t.Run("ClearRecoveryLockPendingStatus", func(t *testing.T) {
|
|
host := setupHost(t, "clear-pending-host", "1.2.3.11", "clearkey", "clearuuid")
|
|
|
|
// Verify status is pending
|
|
var status sql.NullString
|
|
err := ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryPending), status.String)
|
|
|
|
// Clear pending status
|
|
err = ds.ClearRecoveryLockPendingStatus(ctx, []string{host.UUID})
|
|
require.NoError(t, err)
|
|
|
|
// Verify status is now NULL
|
|
err = ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.False(t, status.Valid, "status should be NULL after clearing")
|
|
})
|
|
|
|
t.Run("ClearRecoveryLockPendingStatus only clears pending", func(t *testing.T) {
|
|
host := setupHost(t, "no-clear-verified-host", "1.2.3.12", "ncvkey", "ncvuuid")
|
|
|
|
// Set to verified
|
|
err := ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Try to clear - should not affect verified status
|
|
err = ds.ClearRecoveryLockPendingStatus(ctx, []string{host.UUID})
|
|
require.NoError(t, err)
|
|
|
|
// Verify status is still verified
|
|
var status string
|
|
err = ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), status)
|
|
})
|
|
|
|
t.Run("ResetRecoveryLockForRetry", func(t *testing.T) {
|
|
host := setupHost(t, "reset-retry-host", "1.2.3.13", "rrkey", "rruuid")
|
|
|
|
// First set to remove/pending (simulating a clear in progress)
|
|
_, err := ds.writer(ctx).ExecContext(ctx, `
|
|
UPDATE host_recovery_key_passwords
|
|
SET operation_type = ?, status = ?, error_message = ?
|
|
WHERE host_uuid = ?
|
|
`, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryPending, "test error", host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Reset for retry
|
|
err = ds.ResetRecoveryLockForRetry(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify it was reset to install/verified with no error message
|
|
var result struct {
|
|
OperationType string `db:"operation_type"`
|
|
Status string `db:"status"`
|
|
ErrorMessage sql.NullString `db:"error_message"`
|
|
}
|
|
err = ds.writer(ctx).GetContext(ctx, &result, `
|
|
SELECT operation_type, status, error_message
|
|
FROM host_recovery_key_passwords
|
|
WHERE host_uuid = ?
|
|
`, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMOperationTypeInstall), result.OperationType)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), result.Status)
|
|
assert.False(t, result.ErrorMessage.Valid, "error_message should be NULL after reset")
|
|
})
|
|
|
|
t.Run("ResetRecoveryLockForRetry from failed state", func(t *testing.T) {
|
|
host := setupHost(t, "reset-failed-host", "1.2.3.14", "rfkey", "rfuuid")
|
|
|
|
// Set to remove/failed (simulating a failed clear)
|
|
_, err := ds.writer(ctx).ExecContext(ctx, `
|
|
UPDATE host_recovery_key_passwords
|
|
SET operation_type = ?, status = ?, error_message = ?
|
|
WHERE host_uuid = ?
|
|
`, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryFailed, "previous error", host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Reset for retry
|
|
err = ds.ResetRecoveryLockForRetry(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify it was reset to install/verified
|
|
var result struct {
|
|
OperationType string `db:"operation_type"`
|
|
Status string `db:"status"`
|
|
ErrorMessage sql.NullString `db:"error_message"`
|
|
}
|
|
err = ds.writer(ctx).GetContext(ctx, &result, `
|
|
SELECT operation_type, status, error_message
|
|
FROM host_recovery_key_passwords
|
|
WHERE host_uuid = ?
|
|
`, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMOperationTypeInstall), result.OperationType)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), result.Status)
|
|
assert.False(t, result.ErrorMessage.Valid, "error_message should be NULL after reset")
|
|
})
|
|
}
|
|
|
|
func testGetHostsForRecoveryLockAction(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Helper to create a team with recovery lock setting
|
|
createTeamWithRecoveryLock := func(name string, enabled bool) *fleet.Team {
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: name})
|
|
require.NoError(t, err)
|
|
|
|
team.Config.MDM.EnableRecoveryLockPassword = enabled
|
|
team, err = ds.SaveTeam(ctx, team)
|
|
require.NoError(t, err)
|
|
return team
|
|
}
|
|
|
|
// Helper to set app config recovery lock setting
|
|
setAppConfigRecoveryLock := func(enabled bool) {
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
ac.MDM.EnableRecoveryLockPassword = optjson.SetBool(enabled)
|
|
err = ds.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Helper to set host CPU type
|
|
setHostCPUType := func(hostID uint, cpuType string) {
|
|
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE hosts SET cpu_type = ? WHERE id = ?`, cpuType, hostID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Initially no eligible hosts
|
|
hosts, err := ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, hosts)
|
|
|
|
// Create eligible Apple Silicon host in team with recovery lock enabled
|
|
teamARM := createTeamWithRecoveryLock("team-arm", true)
|
|
hostARM := test.NewHost(t, ds, "arm-host", "1.2.5.1", "armkey", "armuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamARM.ID))
|
|
setHostCPUType(hostARM.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostARM, false)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.True(t, slices.Contains(hosts, hostARM.UUID), "Apple Silicon (ARM) host should be eligible")
|
|
|
|
// Create ineligible Intel host
|
|
teamIntel := createTeamWithRecoveryLock("team-intel", true)
|
|
hostIntel := test.NewHost(t, ds, "intel-host", "1.2.5.2", "intelkey", "inteluuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamIntel.ID))
|
|
setHostCPUType(hostIntel.ID, "x86_64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostIntel, false)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostIntel.UUID), "Intel host should NOT be eligible")
|
|
|
|
// Create host in team with recovery lock DISABLED
|
|
teamDisabled := createTeamWithRecoveryLock("team-disabled", false)
|
|
hostDisabled := test.NewHost(t, ds, "disabled-team-host", "1.2.5.4", "dtkey", "dtuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamDisabled.ID))
|
|
setHostCPUType(hostDisabled.ID, "arm64e")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostDisabled, false)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostDisabled.UUID), "host in disabled team should NOT be eligible")
|
|
|
|
// Create host without MDM enrollment
|
|
teamNotEnrolled := createTeamWithRecoveryLock("team-not-enrolled", true)
|
|
hostNotEnrolled := test.NewHost(t, ds, "not-enrolled-host", "1.2.5.5", "nekey", "neuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamNotEnrolled.ID))
|
|
setHostCPUType(hostNotEnrolled.ID, "arm64e")
|
|
// No nano enrollment
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostNotEnrolled.UUID), "non-enrolled host should NOT be eligible")
|
|
|
|
// Create Windows host (not darwin)
|
|
teamNotDarwin := createTeamWithRecoveryLock("team-not-darwin", true)
|
|
hostWindows := test.NewHost(t, ds, "windows-host", "1.2.5.6", "wkey", "wuuid", time.Now(),
|
|
test.WithPlatform("windows"), test.WithTeamID(teamNotDarwin.ID))
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostWindows, false)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostWindows.UUID), "Windows host should NOT be eligible")
|
|
|
|
// Create host with pending status (already has SetRecoveryLock in progress)
|
|
// Note: SetHostsRecoveryLockPasswords now sets status to 'pending' atomically
|
|
teamPending := createTeamWithRecoveryLock("team-pending", true)
|
|
hostPending := test.NewHost(t, ds, "pending-host2", "1.2.5.7", "pkey2", "puuid2", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamPending.ID))
|
|
setHostCPUType(hostPending.ID, "arm64e")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostPending, false)
|
|
pendingPW := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: hostPending.UUID, Password: pendingPW}})
|
|
require.NoError(t, err)
|
|
// Status is already 'pending' from SetHostsRecoveryLockPasswords - no need to call SetRecoveryLockPending
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostPending.UUID), "pending host should NOT be eligible")
|
|
|
|
// Create host with verified status (already has recovery lock set)
|
|
teamVerified := createTeamWithRecoveryLock("team-verified", true)
|
|
hostVerified := test.NewHost(t, ds, "verified-host2", "1.2.5.8", "vkey2", "vuuid2", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamVerified.ID))
|
|
setHostCPUType(hostVerified.ID, "arm64e")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostVerified, false)
|
|
verifiedPW := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: hostVerified.UUID, Password: verifiedPW}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, hostVerified.UUID)
|
|
require.NoError(t, err)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostVerified.UUID), "verified host should NOT be eligible")
|
|
|
|
// Test no-team host with app config recovery lock enabled
|
|
setAppConfigRecoveryLock(true)
|
|
hostNoTeam := test.NewHost(t, ds, "no-team-host", "1.2.5.9", "ntkey", "ntuuid", time.Now(),
|
|
test.WithPlatform("darwin"))
|
|
setHostCPUType(hostNoTeam.ID, "arm64e")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostNoTeam, false)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.True(t, slices.Contains(hosts, hostNoTeam.UUID), "no-team host should be eligible when app config enabled")
|
|
|
|
// Clean up - disable app config recovery lock
|
|
setAppConfigRecoveryLock(false)
|
|
|
|
// Now the no-team host should not be eligible
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostNoTeam.UUID), "no-team host should NOT be eligible when app config disabled")
|
|
|
|
// Create host with nano enrollment but MDM turned off (host_mdm.enrolled = 0)
|
|
// This tests that hosts are properly excluded after MDMTurnOff is called
|
|
teamUnenrolled := createTeamWithRecoveryLock("team-unenrolled", true)
|
|
hostUnenrolled := test.NewHost(t, ds, "unenrolled-host", "1.2.5.10", "uekey", "ueuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamUnenrolled.ID))
|
|
setHostCPUType(hostUnenrolled.ID, "arm64e")
|
|
nanoEnroll(t, ds, hostUnenrolled, false)
|
|
// Set host_mdm with enrolled = false (simulates MDM turn off)
|
|
err = ds.SetOrUpdateMDMData(ctx, hostUnenrolled.ID, false, false, "", false, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostUnenrolled.UUID), "host with MDM turned off should NOT be eligible")
|
|
|
|
// Test host in "pending remove" state is NOT picked up by GetHostsForRecoveryLockAction
|
|
// Instead, RestoreRecoveryLockForReenabledHosts should handle this case
|
|
// This tests the scenario where:
|
|
// 1. Feature is disabled, host goes to operation_type='remove', status='pending'
|
|
// 2. Feature is re-enabled
|
|
// 3. RestoreRecoveryLockForReenabledHosts restores it to "verified install"
|
|
// 4. GetHostsForRecoveryLockAction should NOT pick it up (it's already verified)
|
|
teamReEnable := createTeamWithRecoveryLock("team-reenable", true)
|
|
hostReEnable := test.NewHost(t, ds, "reenable-host", "1.2.5.11", "rekey", "reuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamReEnable.ID))
|
|
setHostCPUType(hostReEnable.ID, "arm64e")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostReEnable, false)
|
|
|
|
// Set and verify the password
|
|
reEnablePW := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: hostReEnable.UUID, Password: reEnablePW}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, hostReEnable.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Disable recovery lock for team (triggers pending remove state)
|
|
teamReEnable.Config.MDM.EnableRecoveryLockPassword = false
|
|
_, err = ds.SaveTeam(ctx, teamReEnable)
|
|
require.NoError(t, err)
|
|
|
|
// Claim for clear - this sets operation_type to "remove" and status to "pending"
|
|
_, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Host should NOT be eligible while feature is disabled
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostReEnable.UUID), "host in pending remove state should NOT be eligible while feature is disabled")
|
|
|
|
// Re-enable recovery lock for team
|
|
teamReEnable.Config.MDM.EnableRecoveryLockPassword = true
|
|
_, err = ds.SaveTeam(ctx, teamReEnable)
|
|
require.NoError(t, err)
|
|
|
|
// Host should still NOT be eligible for GetHostsForRecoveryLockAction
|
|
// (it needs to be restored first by RestoreRecoveryLockForReenabledHosts)
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostReEnable.UUID), "host in pending remove state should NOT be picked up by GetHostsForRecoveryLockAction")
|
|
|
|
// RestoreRecoveryLockForReenabledHosts should restore the host to "verified install"
|
|
restored, err := ds.RestoreRecoveryLockForReenabledHosts(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), restored, "should restore one host")
|
|
|
|
// Verify the host is now in "verified install" state
|
|
var opType, status string
|
|
err = ds.writer(ctx).GetContext(ctx, &opType, "SELECT operation_type FROM host_recovery_key_passwords WHERE host_uuid = ?", hostReEnable.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMOperationTypeInstall), opType)
|
|
|
|
err = ds.writer(ctx).GetContext(ctx, &status, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", hostReEnable.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), status)
|
|
|
|
// Host should STILL not be eligible (it's verified, not pending)
|
|
hosts, err = ds.GetHostsForRecoveryLockAction(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, slices.Contains(hosts, hostReEnable.UUID), "verified host should NOT be eligible")
|
|
|
|
// Test that RestoreRecoveryLockForReenabledHosts does NOT restore failed records
|
|
// This tests the scenario where:
|
|
// 1. Feature is disabled, host goes to operation_type='remove'
|
|
// 2. ClearRecoveryLock fails with terminal error (e.g., password mismatch)
|
|
// 3. Host is now in (remove, failed) state with error_message
|
|
// 4. Feature is re-enabled
|
|
// 5. RestoreRecoveryLockForReenabledHosts should NOT restore this host
|
|
// because it's a terminal error requiring admin intervention
|
|
teamFailed := createTeamWithRecoveryLock("team-failed", true)
|
|
hostFailed := test.NewHost(t, ds, "failed-host", "1.2.5.12", "failkey", "failuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(teamFailed.ID))
|
|
setHostCPUType(hostFailed.ID, "arm64e")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostFailed, false)
|
|
|
|
// Set and verify the password
|
|
failedPW := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: hostFailed.UUID, Password: failedPW}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Disable recovery lock for team
|
|
teamFailed.Config.MDM.EnableRecoveryLockPassword = false
|
|
_, err = ds.SaveTeam(ctx, teamFailed)
|
|
require.NoError(t, err)
|
|
|
|
// Claim for clear - sets operation_type to "remove" and status to "pending"
|
|
_, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate ClearRecoveryLock failing with terminal error (password mismatch)
|
|
err = ds.SetRecoveryLockFailed(ctx, hostFailed.UUID, "Password mismatch: The provided recovery password failed to validate.")
|
|
require.NoError(t, err)
|
|
|
|
// Verify host is in (remove, failed) state
|
|
var failedOpType, failedStatus, failedErrorMsg string
|
|
err = ds.writer(ctx).GetContext(ctx, &failedOpType, "SELECT operation_type FROM host_recovery_key_passwords WHERE host_uuid = ?", hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMOperationTypeRemove), failedOpType)
|
|
err = ds.writer(ctx).GetContext(ctx, &failedStatus, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryFailed), failedStatus)
|
|
err = ds.writer(ctx).GetContext(ctx, &failedErrorMsg, "SELECT error_message FROM host_recovery_key_passwords WHERE host_uuid = ?", hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, failedErrorMsg, "Password mismatch")
|
|
|
|
// Re-enable recovery lock for team
|
|
teamFailed.Config.MDM.EnableRecoveryLockPassword = true
|
|
_, err = ds.SaveTeam(ctx, teamFailed)
|
|
require.NoError(t, err)
|
|
|
|
// RestoreRecoveryLockForReenabledHosts should NOT restore the failed host
|
|
restored, err = ds.RestoreRecoveryLockForReenabledHosts(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), restored, "should NOT restore failed hosts")
|
|
|
|
// Verify host is STILL in (remove, failed) state with error_message preserved
|
|
err = ds.writer(ctx).GetContext(ctx, &failedOpType, "SELECT operation_type FROM host_recovery_key_passwords WHERE host_uuid = ?", hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMOperationTypeRemove), failedOpType, "operation_type should still be 'remove'")
|
|
err = ds.writer(ctx).GetContext(ctx, &failedStatus, "SELECT status FROM host_recovery_key_passwords WHERE host_uuid = ?", hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(fleet.MDMDeliveryFailed), failedStatus, "status should still be 'failed'")
|
|
err = ds.writer(ctx).GetContext(ctx, &failedErrorMsg, "SELECT error_message FROM host_recovery_key_passwords WHERE host_uuid = ?", hostFailed.UUID)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, failedErrorMsg, "Password mismatch", "error_message should be preserved")
|
|
}
|
|
|
|
func testClaimHostsForRecoveryLockClear(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Helper to create a team with recovery lock setting
|
|
createTeamWithRecoveryLock := func(t *testing.T, name string, enabled bool) *fleet.Team {
|
|
t.Helper()
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: name})
|
|
require.NoError(t, err)
|
|
|
|
team.Config.MDM.EnableRecoveryLockPassword = enabled
|
|
team, err = ds.SaveTeam(ctx, team)
|
|
require.NoError(t, err)
|
|
return team
|
|
}
|
|
|
|
// Helper to set app config recovery lock setting
|
|
setAppConfigRecoveryLock := func(t *testing.T, enabled bool) {
|
|
t.Helper()
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
ac.MDM.EnableRecoveryLockPassword = optjson.SetBool(enabled)
|
|
err = ds.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Helper to set host CPU type
|
|
setHostCPUType := func(t *testing.T, hostID uint, cpuType string) {
|
|
t.Helper()
|
|
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE hosts SET cpu_type = ? WHERE id = ?`, cpuType, hostID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Helper to get password record (excludes soft-deleted records)
|
|
getPasswordRecord := func(t *testing.T, hostUUID string) (opType, status string, found bool) {
|
|
t.Helper()
|
|
var rec struct {
|
|
OperationType string `db:"operation_type"`
|
|
Status *string `db:"status"`
|
|
}
|
|
err := sqlx.GetContext(ctx, ds.reader(ctx), &rec,
|
|
`SELECT operation_type, status FROM host_recovery_key_passwords WHERE host_uuid = ? AND deleted = 0`, hostUUID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return "", "", false
|
|
}
|
|
t.Fatalf("getPasswordRecord query failed: %v", err)
|
|
}
|
|
if rec.Status != nil {
|
|
status = *rec.Status
|
|
}
|
|
return rec.OperationType, status, true
|
|
}
|
|
|
|
t.Run("no hosts to clear returns empty", func(t *testing.T) {
|
|
hosts, err := ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, hosts)
|
|
})
|
|
|
|
t.Run("claims verified host when config disabled", func(t *testing.T) {
|
|
// Create team with recovery lock enabled initially
|
|
team := createTeamWithRecoveryLock(t, "removing-status-team", true)
|
|
host := test.NewHost(t, ds, "removing-rlp-host", "1.2.6.4", "removingrlpkey", "removingrlpuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, host.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, host, false)
|
|
|
|
// Set password and verify
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Disable recovery lock for team to trigger clear
|
|
team.Config.MDM.EnableRecoveryLockPassword = false
|
|
_, err = ds.SaveTeam(ctx, team)
|
|
require.NoError(t, err)
|
|
|
|
// Claim for clear - this sets operation_type to "remove" and status to "pending"
|
|
_, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify state is now operation_type=remove, status=pending
|
|
opType, status, found := getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "remove", opType)
|
|
assert.Equal(t, "pending", status)
|
|
})
|
|
|
|
t.Run("returns pending when operation_type is install and status is NULL", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "install-null-host", "1.2.6.5", "installnullkey", "installnulluuid", time.Now())
|
|
|
|
// Insert a record with operation_type=install and status=NULL directly
|
|
_, err := ds.writer(ctx).ExecContext(ctx,
|
|
`INSERT INTO host_recovery_key_passwords (host_uuid, encrypted_password, operation_type, status)
|
|
VALUES (?, 'test', 'install', NULL)`, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify state is operation_type=install, status=NULL
|
|
opType, status, found := getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "install", opType)
|
|
assert.Equal(t, "", status, "status should be empty (NULL) when operation_type is install and status is NULL")
|
|
})
|
|
|
|
t.Run("returns verified status", func(t *testing.T) {
|
|
// Create team with recovery lock enabled
|
|
team := createTeamWithRecoveryLock(t, "verified-status-team", true)
|
|
host := test.NewHost(t, ds, "verified-rlp-host", "1.2.6.3", "verifiedrlpkey", "verifiedrlpuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, host.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, host, false)
|
|
|
|
// Set password and mark as verified
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify initial state
|
|
opType, status, found := getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "install", opType)
|
|
assert.Equal(t, "verified", status)
|
|
|
|
// Should not be claimed while config is enabled
|
|
hosts, err := ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, hosts, host.UUID)
|
|
|
|
// Disable recovery lock for team
|
|
team.Config.MDM.EnableRecoveryLockPassword = false
|
|
_, err = ds.SaveTeam(ctx, team)
|
|
require.NoError(t, err)
|
|
|
|
// Now host should be claimed
|
|
hosts, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, hosts, host.UUID)
|
|
|
|
// Verify state changed to remove/pending
|
|
opType, status, found = getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "remove", opType)
|
|
assert.Equal(t, "pending", status)
|
|
|
|
// Should not be claimed again (already pending)
|
|
hosts, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, hosts, host.UUID)
|
|
})
|
|
|
|
t.Run("does not claim pending or failed hosts", func(t *testing.T) {
|
|
team := createTeamWithRecoveryLock(t, "clear-pending-team", false)
|
|
|
|
// Host with pending status
|
|
hostPending := test.NewHost(t, ds, "pending-clear", "1.2.6.2", "pendkey", "penduuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, hostPending.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostPending, false)
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: hostPending.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
// Status is pending from SetHostsRecoveryLockPasswords
|
|
|
|
// Host with failed status
|
|
hostFailed := test.NewHost(t, ds, "failed-clear", "1.2.6.3", "failkey", "failuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, hostFailed.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, hostFailed, false)
|
|
pw2 := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: hostFailed.UUID, Password: pw2}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockFailed(ctx, hostFailed.UUID, "test error")
|
|
require.NoError(t, err)
|
|
|
|
hosts, err := ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, hosts, hostPending.UUID, "pending host should not be claimed")
|
|
assert.NotContains(t, hosts, hostFailed.UUID, "failed host should not be claimed")
|
|
})
|
|
|
|
t.Run("claims no-team host when appconfig disabled", func(t *testing.T) {
|
|
// Enable recovery lock in appconfig
|
|
setAppConfigRecoveryLock(t, true)
|
|
|
|
host := test.NewHost(t, ds, "noteam-clear", "1.2.6.4", "ntkey", "ntuuid", time.Now(),
|
|
test.WithPlatform("darwin"))
|
|
setHostCPUType(t, host.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, host, false)
|
|
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Should not be claimed while appconfig enabled
|
|
hosts, err := ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, hosts, host.UUID)
|
|
|
|
// Disable recovery lock in appconfig
|
|
setAppConfigRecoveryLock(t, false)
|
|
|
|
// Now should be claimed
|
|
hosts, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, hosts, host.UUID)
|
|
})
|
|
|
|
t.Run("soft delete marks password record as deleted", func(t *testing.T) {
|
|
team := createTeamWithRecoveryLock(t, "delete-test-team", true)
|
|
host := test.NewHost(t, ds, "delete-host", "1.2.6.5", "delkey", "deluuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, host.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, host, false)
|
|
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify record exists
|
|
_, _, found := getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
|
|
// Soft delete the record
|
|
err = ds.DeleteHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify record is not found by normal queries (excludes deleted)
|
|
_, _, found = getPasswordRecord(t, host.UUID)
|
|
assert.False(t, found)
|
|
|
|
// Verify record still exists in DB but is marked as deleted and verified
|
|
var rec struct {
|
|
Deleted bool `db:"deleted"`
|
|
Status string `db:"status"`
|
|
}
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &rec,
|
|
`SELECT deleted, status FROM host_recovery_key_passwords WHERE host_uuid = ?`, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.True(t, rec.Deleted)
|
|
assert.Equal(t, "verified", rec.Status)
|
|
})
|
|
|
|
t.Run("get operation type", func(t *testing.T) {
|
|
team := createTeamWithRecoveryLock(t, "optype-test-team", false)
|
|
host := test.NewHost(t, ds, "optype-host", "1.2.6.6", "optkey", "optuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, host.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, host, false)
|
|
|
|
// No record - should return not found
|
|
_, err := ds.GetRecoveryLockOperationType(ctx, host.UUID)
|
|
require.Error(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
// Create record with install type
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
opType, err := ds.GetRecoveryLockOperationType(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fleet.MDMOperationTypeInstall, opType)
|
|
|
|
// Claim for clear - changes to remove type
|
|
_, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
|
|
opType, err = ds.GetRecoveryLockOperationType(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fleet.MDMOperationTypeRemove, opType)
|
|
|
|
// Soft delete - should return not found
|
|
err = ds.DeleteHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.GetRecoveryLockOperationType(ctx, host.UUID)
|
|
require.Error(t, err)
|
|
assert.True(t, fleet.IsNotFound(err), "soft-deleted record should return not found")
|
|
})
|
|
|
|
t.Run("retries failed clear attempts", func(t *testing.T) {
|
|
team := createTeamWithRecoveryLock(t, "retry-test-team", false)
|
|
host := test.NewHost(t, ds, "retry-host", "1.2.6.7", "retrykey", "retryuuid", time.Now(),
|
|
test.WithPlatform("darwin"), test.WithTeamID(team.ID))
|
|
setHostCPUType(t, host.ID, "arm64")
|
|
nanoEnrollAndSetHostMDMData(t, ds, host, false)
|
|
|
|
// Set password and mark as verified
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Claim for clear
|
|
hosts, err := ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, hosts, host.UUID)
|
|
|
|
// Verify state is remove/pending
|
|
opType, status, found := getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "remove", opType)
|
|
assert.Equal(t, "pending", status)
|
|
|
|
// Simulate failed enqueue - clear pending status back to NULL
|
|
err = ds.ClearRecoveryLockPendingStatus(ctx, []string{host.UUID})
|
|
require.NoError(t, err)
|
|
|
|
// Verify state is remove/NULL (retry state)
|
|
opType, status, found = getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "remove", opType)
|
|
assert.Equal(t, "", status) // NULL becomes empty string
|
|
|
|
// Should be claimed again on retry
|
|
hosts, err = ds.ClaimHostsForRecoveryLockClear(ctx)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, hosts, host.UUID, "host with remove/NULL should be retried")
|
|
|
|
// Verify state is back to remove/pending
|
|
opType, status, found = getPasswordRecord(t, host.UUID)
|
|
require.True(t, found)
|
|
assert.Equal(t, "remove", opType)
|
|
assert.Equal(t, "pending", status)
|
|
})
|
|
}
|
|
|
|
func testGetHostRecoveryLockPasswordStatus(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
t.Run("returns nil for host without recovery lock password", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "no-rlp-host", "1.2.6.1", "norlpkey", "norlpuuid", time.Now())
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, status)
|
|
})
|
|
|
|
t.Run("returns enforcing status for pending install", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "pending-rlp-host", "1.2.6.2", "pendingrlpkey", "pendingrlpuuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusPending, *status.Status)
|
|
assert.Empty(t, status.Detail)
|
|
assert.True(t, status.PasswordAvailable)
|
|
})
|
|
|
|
t.Run("returns verified status", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "verified-rlp-host", "1.2.6.3", "verifiedrlpkey", "verifiedrlpuuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusVerified, *status.Status)
|
|
assert.Empty(t, status.Detail)
|
|
assert.True(t, status.PasswordAvailable)
|
|
})
|
|
|
|
t.Run("returns failed status with error message", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "failed-rlp-host", "1.2.6.4", "failedrlpkey", "failedrlpuuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
errMsg := "SetRecoveryLock command failed: device rejected"
|
|
err = ds.SetRecoveryLockFailed(ctx, host.UUID, errMsg)
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusFailed, *status.Status)
|
|
assert.Equal(t, errMsg, status.Detail)
|
|
})
|
|
|
|
t.Run("returns verifying status", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "verifying-rlp-host", "1.2.6.5", "verifyingrlpkey", "verifyingrlpuuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
// Set status to verifying directly via SQL (since there's no SetRecoveryLockVerifying method)
|
|
_, err = ds.writer(ctx).ExecContext(ctx, `UPDATE host_recovery_key_passwords SET status = ? WHERE host_uuid = ?`,
|
|
fleet.MDMDeliveryVerifying, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusPending, *status.Status)
|
|
assert.Empty(t, status.Detail)
|
|
})
|
|
|
|
t.Run("returns enforcing status when status column is NULL (retry state)", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "null-status-host", "1.2.6.6", "nullstatuskey", "nullstatusuuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
// Clear status to NULL (simulates retry state after failed enqueue)
|
|
err = ds.ClearRecoveryLockPendingStatus(ctx, []string{host.UUID})
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
// NULL status is coalesced to pending, which becomes enforcing
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusPending, *status.Status)
|
|
assert.Empty(t, status.Detail)
|
|
})
|
|
|
|
t.Run("returns removing_enforcement status for pending removal after PopulateStatus", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "remove-pending-host", "1.2.6.7", "removependingkey", "removependinguuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
// Set operation_type to 'remove' and status to 'pending' (simulates pending removal)
|
|
_, err = ds.writer(ctx).ExecContext(ctx, `UPDATE host_recovery_key_passwords SET operation_type = ?, status = ? WHERE host_uuid = ?`,
|
|
fleet.MDMOperationTypeRemove, fleet.MDMDeliveryPending, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
|
|
// Before PopulateStatus, Status is nil (raw status is internal)
|
|
assert.Nil(t, status.Status)
|
|
|
|
// After PopulateStatus, Status is removing_enforcement
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusRemovingEnforcement, *status.Status)
|
|
})
|
|
|
|
t.Run("returns failed status when operation_type is remove and status is failed", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "remove-failed-host", "1.2.6.8", "removefailedkey", "removefaileduuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
// Set operation_type to 'remove' and status to 'failed'
|
|
errMsg := "ClearRecoveryLock command failed"
|
|
_, err = ds.writer(ctx).ExecContext(ctx, `UPDATE host_recovery_key_passwords SET operation_type = ?, status = ?, error_message = ? WHERE host_uuid = ?`,
|
|
fleet.MDMOperationTypeRemove, fleet.MDMDeliveryFailed, errMsg, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
status, err := ds.GetHostRecoveryLockPasswordStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
assert.Equal(t, errMsg, status.Detail)
|
|
|
|
// After PopulateStatus, Status is failed
|
|
status.PopulateStatus()
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, fleet.RecoveryLockStatusFailed, *status.Status)
|
|
})
|
|
}
|
|
|
|
func testRecoveryLockRotation(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Helper to set up a host with a verified recovery lock password
|
|
setupHostWithVerifiedPassword := func(t *testing.T, name, uuid string) *fleet.Host {
|
|
t.Helper()
|
|
host := test.NewHost(t, ds, name, "1.2.3."+uuid[:3], name+"key", uuid, time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
err = ds.SetRecoveryLockVerified(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
return host
|
|
}
|
|
|
|
// Helper to get pending rotation state
|
|
getPendingRotationState := func(t *testing.T, hostUUID string) (hasPending bool, pendingErr *string) {
|
|
t.Helper()
|
|
var result struct {
|
|
HasPending bool `db:"has_pending"`
|
|
PendingErr *string `db:"pending_err"`
|
|
}
|
|
err := ds.writer(ctx).GetContext(ctx, &result, `
|
|
SELECT
|
|
pending_encrypted_password IS NOT NULL AS has_pending,
|
|
pending_error_message AS pending_err
|
|
FROM host_recovery_key_passwords
|
|
WHERE host_uuid = ? AND deleted = 0`, hostUUID)
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
require.NoError(t, err)
|
|
return result.HasPending, result.PendingErr
|
|
}
|
|
|
|
t.Run("InitiateRecoveryLockRotation success", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "rotate-host", "rotateuuid1")
|
|
|
|
// Initiate rotation
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Verify pending password is set
|
|
hasPending, _ := getPendingRotationState(t, host.UUID)
|
|
assert.True(t, hasPending, "pending password should be set")
|
|
|
|
// Verify HasPendingRecoveryLockRotation returns true
|
|
pending, err := ds.HasPendingRecoveryLockRotation(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.True(t, pending)
|
|
})
|
|
|
|
t.Run("InitiateRecoveryLockRotation rejects if already pending", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "double-rotate-host", "doublerotuuid")
|
|
|
|
// Initiate first rotation
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Try to initiate second rotation - should fail
|
|
err = ds.InitiateRecoveryLockRotation(ctx, host.UUID, "another-password")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "rotation already pending")
|
|
})
|
|
|
|
t.Run("InitiateRecoveryLockRotation rejects pending status", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "pending-rotate-host", "1.2.3.100", "pendingrotkey", "pendingrotuuid", time.Now())
|
|
pw := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.SetHostsRecoveryLockPasswords(ctx, []fleet.HostRecoveryLockPasswordPayload{{HostUUID: host.UUID, Password: pw}})
|
|
require.NoError(t, err)
|
|
// Status is pending after SetHostsRecoveryLockPasswords
|
|
|
|
// Try to initiate rotation on pending status - should fail
|
|
err = ds.InitiateRecoveryLockRotation(ctx, host.UUID, "new-password")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not eligible for rotation")
|
|
})
|
|
|
|
t.Run("InitiateRecoveryLockRotation allows failed status", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "failed-rotate-host", "failedrotuuid")
|
|
|
|
// Set to failed status
|
|
err := ds.SetRecoveryLockFailed(ctx, host.UUID, "previous failure")
|
|
require.NoError(t, err)
|
|
|
|
// Should be able to initiate rotation on failed status
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
hasPending, _ := getPendingRotationState(t, host.UUID)
|
|
assert.True(t, hasPending)
|
|
})
|
|
|
|
t.Run("CompleteRecoveryLockRotation success", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "complete-rotate-host", "completerotuuid")
|
|
|
|
// Get original password
|
|
origPw, err := ds.GetHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Initiate rotation with new password
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Complete rotation
|
|
err = ds.CompleteRecoveryLockRotation(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify new password is now the active password
|
|
currentPw, err := ds.GetHostRecoveryLockPassword(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, origPw.Password, currentPw.Password)
|
|
assert.Equal(t, newPassword, currentPw.Password)
|
|
|
|
// Verify pending is cleared
|
|
hasPending, _ := getPendingRotationState(t, host.UUID)
|
|
assert.False(t, hasPending)
|
|
|
|
// Verify status is verified
|
|
status, err := ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), *status.Status)
|
|
})
|
|
|
|
t.Run("FailRecoveryLockRotation preserves pending password", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "fail-rotate-host", "failrotuuid")
|
|
|
|
// Initiate rotation
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Fail rotation
|
|
err = ds.FailRecoveryLockRotation(ctx, host.UUID, "rotation failed due to device error")
|
|
require.NoError(t, err)
|
|
|
|
// Verify pending password is still there (for potential retry)
|
|
hasPending, pendingErr := getPendingRotationState(t, host.UUID)
|
|
assert.True(t, hasPending, "pending password should still be set for retry")
|
|
require.NotNil(t, pendingErr)
|
|
assert.Equal(t, "rotation failed due to device error", *pendingErr)
|
|
|
|
// Verify rotation status shows the error
|
|
status, err := ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.True(t, status.HasPendingRotation)
|
|
require.NotNil(t, status.PendingErrorMessage)
|
|
assert.Equal(t, "rotation failed due to device error", *status.PendingErrorMessage)
|
|
})
|
|
|
|
t.Run("ClearRecoveryLockRotation removes pending", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "clear-rotate-host", "clearrotuuid")
|
|
|
|
// Initiate rotation
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err := ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Clear rotation
|
|
err = ds.ClearRecoveryLockRotation(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify pending is cleared
|
|
hasPending, _ := getPendingRotationState(t, host.UUID)
|
|
assert.False(t, hasPending)
|
|
|
|
// Verify HasPendingRecoveryLockRotation returns false
|
|
pending, err := ds.HasPendingRecoveryLockRotation(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.False(t, pending)
|
|
|
|
// Verify status restored to verified (since it was verified before rotation)
|
|
status, err := ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), *status.Status)
|
|
})
|
|
|
|
t.Run("ClearRecoveryLockRotation restores failed status", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "clear-failed-rotate-host", "clearfailedrotuuid")
|
|
|
|
// Set to failed status
|
|
err := ds.SetRecoveryLockFailed(ctx, host.UUID, "previous failure")
|
|
require.NoError(t, err)
|
|
|
|
// Initiate rotation from failed state
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Clear rotation
|
|
err = ds.ClearRecoveryLockRotation(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify pending is cleared
|
|
hasPending, _ := getPendingRotationState(t, host.UUID)
|
|
assert.False(t, hasPending)
|
|
|
|
// Verify status restored to failed (since error_message still exists from previous failure)
|
|
status, err := ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, string(fleet.MDMDeliveryFailed), *status.Status)
|
|
})
|
|
|
|
t.Run("GetRecoveryLockRotationStatus returns all fields", func(t *testing.T) {
|
|
host := setupHostWithVerifiedPassword(t, "status-rotate-host", "statusrotuuid")
|
|
|
|
// Get initial status
|
|
status, err := ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, host.UUID, status.HostUUID)
|
|
assert.True(t, status.HasPassword)
|
|
require.NotNil(t, status.Status)
|
|
assert.Equal(t, string(fleet.MDMDeliveryVerified), *status.Status)
|
|
assert.Equal(t, string(fleet.MDMOperationTypeInstall), status.OperationType)
|
|
assert.False(t, status.HasPendingRotation)
|
|
assert.Nil(t, status.PendingErrorMessage)
|
|
|
|
// Initiate rotation
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
err = ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword)
|
|
require.NoError(t, err)
|
|
|
|
// Check status now shows pending rotation
|
|
status, err = ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
require.NoError(t, err)
|
|
assert.True(t, status.HasPendingRotation)
|
|
})
|
|
|
|
t.Run("GetRecoveryLockRotationStatus not found", func(t *testing.T) {
|
|
_, err := ds.GetRecoveryLockRotationStatus(ctx, "non-existent-uuid")
|
|
require.Error(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
})
|
|
|
|
t.Run("HasPendingRecoveryLockRotation returns false for no record", func(t *testing.T) {
|
|
pending, err := ds.HasPendingRecoveryLockRotation(ctx, "non-existent-uuid")
|
|
require.NoError(t, err)
|
|
assert.False(t, pending)
|
|
})
|
|
}
|