package service import ( "context" "crypto/x509" "encoding/base64" "errors" "fmt" "strconv" "testing" "time" "github.com/WatchBeam/clock" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" 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/mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" nanodep_client "github.com/micromdm/nanodep/client" "github.com/micromdm/nanodep/tokenpki" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mozilla.org/pkcs7" ) func TestHostDetails(t *testing.T) { ds := new(mock.Store) svc := &Service{ds: ds} host := &fleet.Host{ID: 3} expectedLabels := []*fleet.Label{ { Name: "foobar", Description: "the foobar label", }, } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{}, nil } ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return expectedLabels, nil } expectedPacks := []*fleet.Pack{ { Name: "pack1", }, { Name: "pack2", }, } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return expectedPacks, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil } ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { return nil, nil } dsBats := []*fleet.HostBattery{{HostID: host.ID, SerialNumber: "a", CycleCount: 999, Health: "Check Battery"}, {HostID: host.ID, SerialNumber: "b", CycleCount: 1001, Health: "Good"}} ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) { return dsBats, nil } // Health should be replaced at the service layer with custom values determined by the cycle count. See https://github.com/fleetdm/fleet/issues/6763. expectedBats := []*fleet.HostBattery{{HostID: host.ID, SerialNumber: "a", CycleCount: 999, Health: "Normal"}, {HostID: host.ID, SerialNumber: "b", CycleCount: 1001, Health: "Replacement recommended"}} opts := fleet.HostDetailOptions{ IncludeCVEScores: false, IncludePolicies: false, } hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), host, opts) require.NoError(t, err) assert.Equal(t, expectedLabels, hostDetail.Labels) assert.Equal(t, expectedPacks, hostDetail.Packs) require.NotNil(t, hostDetail.Batteries) assert.Equal(t, expectedBats, *hostDetail.Batteries) require.Nil(t, hostDetail.MDM.MacOSSettings) } func TestHostDetailsMDMAppleDiskEncryption(t *testing.T) { ds := new(mock.Store) svc := &Service{ds: ds} ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil } ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return nil, nil } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return nil, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil } ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { return nil, nil } ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) { return nil, nil } cases := []struct { name string rawDecrypt *int fvProf *fleet.HostMDMAppleProfile wantState fleet.DiskEncryptionStatus wantAction fleet.ActionRequiredState wantStatus *fleet.MDMDeliveryStatus }{ {"no profile", ptr.Int(-1), nil, "", "", nil}, { "installed profile, no key", ptr.Int(-1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionActionRequired, fleet.ActionRequiredLogOut, &fleet.MDMDeliveryPending, }, { "installed profile, unknown decryptable", nil, &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionEnforcing, "", &fleet.MDMDeliveryPending, }, { "installed profile, not decryptable", ptr.Int(0), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionActionRequired, fleet.ActionRequiredRotateKey, &fleet.MDMDeliveryPending, }, { "installed profile, decryptable", ptr.Int(1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionVerifying, "", &fleet.MDMDeliveryVerifying, }, { "installed profile, decryptable, verified", ptr.Int(1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionVerified, "", &fleet.MDMDeliveryVerified, }, { "pending install, decryptable", ptr.Int(1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionEnforcing, "", &fleet.MDMDeliveryPending, }, { "pending install, unknown decryptable", nil, &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionEnforcing, "", &fleet.MDMDeliveryPending, }, { "pending install, no key", ptr.Int(-1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionEnforcing, "", &fleet.MDMDeliveryPending, }, { "failed install, no key", ptr.Int(-1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall, Detail: "some mdm profile install error", }, fleet.DiskEncryptionFailed, "", &fleet.MDMDeliveryFailed, }, { "failed install, not decryptable", ptr.Int(0), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall, }, fleet.DiskEncryptionFailed, "", &fleet.MDMDeliveryFailed, }, { "pending remove, decryptable", ptr.Int(1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove, }, fleet.DiskEncryptionRemovingEnforcement, "", &fleet.MDMDeliveryPending, }, { "pending remove, no key", ptr.Int(-1), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove, }, fleet.DiskEncryptionRemovingEnforcement, "", &fleet.MDMDeliveryPending, }, { "failed remove, unknown decryptable", nil, &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove, Detail: "some mdm profile removal error", }, fleet.DiskEncryptionFailed, "", &fleet.MDMDeliveryFailed, }, { "removed profile, not decryptable", ptr.Int(0), &fleet.HostMDMAppleProfile{ HostUUID: "abc", Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeRemove, }, "", "", &fleet.MDMDeliveryVerifying, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { var mdmData fleet.MDMHostData rawDecrypt := "null" if c.rawDecrypt != nil { rawDecrypt = strconv.Itoa(*c.rawDecrypt) } require.NoError(t, mdmData.Scan([]byte(fmt.Sprintf(`{"raw_decryptable": %s}`, rawDecrypt)))) host := &fleet.Host{ID: 3, MDM: mdmData, UUID: "abc", Platform: "darwin"} opts := fleet.HostDetailOptions{ IncludeCVEScores: false, IncludePolicies: false, } ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMAppleProfile, error) { if c.fvProf == nil { return nil, nil } return []fleet.HostMDMAppleProfile{*c.fvProf}, nil } hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), host, opts) require.NoError(t, err) require.NotNil(t, hostDetail.MDM.MacOSSettings) if c.wantState == "" { require.Nil(t, hostDetail.MDM.MacOSSettings.DiskEncryption) require.Nil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) require.Empty(t, hostDetail.MDM.OSSettings.DiskEncryption.Detail) } else { require.NotNil(t, hostDetail.MDM.MacOSSettings.DiskEncryption) require.Equal(t, c.wantState, *hostDetail.MDM.MacOSSettings.DiskEncryption) require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) require.Equal(t, c.wantState, *hostDetail.MDM.OSSettings.DiskEncryption.Status) require.Equal(t, c.fvProf.Detail, hostDetail.MDM.OSSettings.DiskEncryption.Detail) } if c.wantAction == "" { require.Nil(t, hostDetail.MDM.MacOSSettings.ActionRequired) } else { require.NotNil(t, hostDetail.MDM.MacOSSettings.ActionRequired) require.Equal(t, c.wantAction, *hostDetail.MDM.MacOSSettings.ActionRequired) } if c.wantStatus != nil { require.NotNil(t, hostDetail.MDM.Profiles) profs := *hostDetail.MDM.Profiles require.Equal(t, c.wantStatus, profs[0].Status) require.Equal(t, c.fvProf.Detail, profs[0].Detail) } else { require.Nil(t, *hostDetail.MDM.Profiles) } }) } } func TestHostDetailsOSSettings(t *testing.T) { ds := new(mock.Store) svc := &Service{ds: ds} ctx := context.Background() ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return nil, nil } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return nil, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil } ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { return nil, nil } ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) { return nil, nil } ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) { return nil, nil } type testCase struct { name string host *fleet.Host licenseTier string wantStatus fleet.DiskEncryptionStatus } cases := []testCase{ {"windows", &fleet.Host{ID: 42, Platform: "windows"}, fleet.TierPremium, fleet.DiskEncryptionEnforcing}, {"darwin", &fleet.Host{ID: 42, Platform: "darwin"}, fleet.TierPremium, ""}, {"ubuntu", &fleet.Host{ID: 42, Platform: "ubuntu"}, fleet.TierPremium, ""}, {"not premium", &fleet.Host{ID: 42, Platform: "windows"}, fleet.TierFree, ""}, } setupDS := func(c testCase) { ds.AppConfigFuncInvoked = false ds.GetMDMWindowsBitLockerStatusFuncInvoked = false ds.GetHostMDMAppleProfilesFuncInvoked = false ds.GetHostMDMWindowsProfilesFuncInvoked = false ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}, nil } ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) { if c.wantStatus == "" { return nil, nil } return &fleet.HostMDMDiskEncryption{Status: &c.wantStatus, Detail: ""}, nil } ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMAppleProfile, error) { return nil, nil } ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMWindowsProfile, error) { return nil, nil } } for _, c := range cases { t.Run(c.name, func(t *testing.T) { setupDS(c) ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: c.licenseTier}) hostDetail, err := svc.getHostDetails(test.UserContext(ctx, test.UserAdmin), c.host, fleet.HostDetailOptions{ IncludeCVEScores: false, IncludePolicies: false, }) require.NoError(t, err) require.NotNil(t, hostDetail) require.True(t, ds.AppConfigFuncInvoked) switch c.host.Platform { case "windows": require.False(t, ds.GetHostMDMAppleProfilesFuncInvoked) if c.wantStatus != "" { require.True(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) require.Equal(t, c.wantStatus, *hostDetail.MDM.OSSettings.DiskEncryption.Status) } else { require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) require.Nil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) } case "darwin": require.True(t, ds.GetHostMDMAppleProfilesFuncInvoked) require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) require.Nil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) default: require.False(t, ds.GetHostMDMAppleProfilesFuncInvoked) require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) } }) } } func TestHostDetailsOSSettingsWindowsOnly(t *testing.T) { ds := new(mock.Store) svc := &Service{ds: ds} ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return nil, nil } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return nil, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil } ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { return nil, nil } ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) { return nil, nil } ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) { return nil, nil } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}, nil } ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) { verified := fleet.DiskEncryptionVerified return &fleet.HostMDMDiskEncryption{Status: &verified, Detail: ""}, nil } ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMAppleProfile, error) { return nil, nil } ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMWindowsProfile, error) { return nil, nil } ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium}) hostDetail, err := svc.getHostDetails(test.UserContext(ctx, test.UserAdmin), &fleet.Host{ID: 42, Platform: "windows"}, fleet.HostDetailOptions{ IncludeCVEScores: false, IncludePolicies: false, }) require.NoError(t, err) require.NotNil(t, hostDetail) require.True(t, ds.AppConfigFuncInvoked) require.False(t, ds.GetHostMDMAppleProfilesFuncInvoked) require.True(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) require.Equal(t, fleet.DiskEncryptionVerified, *hostDetail.MDM.OSSettings.DiskEncryption.Status) } func TestHostAuth(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) teamHost := &fleet.Host{TeamID: ptr.Uint(1)} globalHost := &fleet.Host{} ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{}, nil } ds.DeleteHostFunc = func(ctx context.Context, hid uint) error { return nil } ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { if id == 1 { return teamHost, nil } return globalHost, nil } ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { if id == 1 { return teamHost, nil } return globalHost, nil } ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) { if identifier == "1" { return teamHost, nil } return globalHost, nil } ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { return nil, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil } ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return nil, nil } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) { return nil, nil } ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error { return nil } ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { return nil, nil } ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) { return nil, nil } ds.DeleteHostsFunc = func(ctx context.Context, ids []uint) error { return nil } ds.UpdateHostRefetchRequestedFunc = func(ctx context.Context, id uint, value bool) error { if id == 1 { teamHost.RefetchRequested = true } else { globalHost.RefetchRequested = true } return nil } ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error { return nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) { return nil, nil } ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id}, nil } ds.NewActivityFunc = func(ctx context.Context, u *fleet.User, a fleet.ActivityDetails) error { return nil } ds.ListHostsLiteByIDsFunc = func(ctx context.Context, ids []uint) ([]*fleet.Host, error) { return nil, nil } ds.SetOrUpdateCustomHostDeviceMappingFunc = func(ctx context.Context, hostID uint, email, source string) ([]*fleet.HostDeviceMapping, error) { return nil, nil } testCases := []struct { name string user *fleet.User shouldFailGlobalWrite bool shouldFailGlobalRead bool shouldFailTeamWrite bool shouldFailTeamRead bool }{ { "global admin", &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, false, false, false, false, }, { "global maintainer", &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, false, false, false, false, }, { "global observer", &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, true, false, true, false, }, { "team admin, belongs to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}, true, true, false, false, }, { "team maintainer, belongs to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}}, true, true, false, false, }, { "team observer, belongs to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}, true, true, true, false, }, { "team admin, DOES NOT belong to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}}, true, true, true, true, }, { "team maintainer, DOES NOT belong to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}}, true, true, true, true, }, { "team observer, DOES NOT belong to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}}, true, true, true, true, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) opts := fleet.HostDetailOptions{ IncludeCVEScores: false, IncludePolicies: false, } _, err := svc.GetHost(ctx, 1, opts) checkAuthErr(t, tt.shouldFailTeamRead, err) _, err = svc.GetHostLite(ctx, 1) checkAuthErr(t, tt.shouldFailTeamRead, err) _, err = svc.HostByIdentifier(ctx, "1", opts) checkAuthErr(t, tt.shouldFailTeamRead, err) _, err = svc.GetHost(ctx, 2, opts) checkAuthErr(t, tt.shouldFailGlobalRead, err) _, err = svc.GetHostLite(ctx, 2) checkAuthErr(t, tt.shouldFailGlobalRead, err) _, err = svc.HostByIdentifier(ctx, "2", opts) checkAuthErr(t, tt.shouldFailGlobalRead, err) err = svc.DeleteHost(ctx, 1) checkAuthErr(t, tt.shouldFailTeamWrite, err) err = svc.DeleteHost(ctx, 2) checkAuthErr(t, tt.shouldFailGlobalWrite, err) err = svc.DeleteHosts(ctx, []uint{1}, nil, nil) checkAuthErr(t, tt.shouldFailTeamWrite, err) err = svc.DeleteHosts(ctx, []uint{2}, &fleet.HostListOptions{}, nil) checkAuthErr(t, tt.shouldFailGlobalWrite, err) err = svc.AddHostsToTeam(ctx, ptr.Uint(1), []uint{1}, false) checkAuthErr(t, tt.shouldFailTeamWrite, err) err = svc.AddHostsToTeamByFilter(ctx, ptr.Uint(1), fleet.HostListOptions{}, nil) checkAuthErr(t, tt.shouldFailTeamWrite, err) err = svc.RefetchHost(ctx, 1) checkAuthErr(t, tt.shouldFailTeamRead, err) _, err = svc.SetCustomHostDeviceMapping(ctx, 1, "a@b.c") checkAuthErr(t, tt.shouldFailTeamWrite, err) _, err = svc.SetCustomHostDeviceMapping(ctx, 2, "a@b.c") checkAuthErr(t, tt.shouldFailGlobalWrite, err) }) } // List, GetHostSummary work for all } func TestListHosts(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { return []*fleet.Host{ {ID: 1}, }, nil } hosts, err := svc.ListHosts(test.UserContext(ctx, test.UserAdmin), fleet.HostListOptions{}) require.NoError(t, err) require.Len(t, hosts, 1) // a user is required _, err = svc.ListHosts(ctx, fleet.HostListOptions{}) require.Error(t, err) require.Contains(t, err.Error(), authz.ForbiddenErrorMessage) } func TestGetHostSummary(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) ds.GenerateHostStatusStatisticsFunc = func(ctx context.Context, filter fleet.TeamFilter, now time.Time, platform *string, lowDiskSpace *int) (*fleet.HostSummary, error) { return &fleet.HostSummary{ OnlineCount: 1, OfflineCount: 5, // offline hosts also includes mia hosts as of Fleet 4.15 MIACount: 3, NewCount: 4, TotalsHostsCount: 5, Platforms: []*fleet.HostSummaryPlatform{{Platform: "darwin", HostsCount: 1}, {Platform: "debian", HostsCount: 2}, {Platform: "centos", HostsCount: 3}, {Platform: "ubuntu", HostsCount: 4}}, }, nil } ds.LabelsSummaryFunc = func(ctx context.Context) ([]*fleet.LabelSummary, error) { return []*fleet.LabelSummary{{ID: 1, Name: "All hosts", Description: "All hosts enrolled in Fleet", LabelType: fleet.LabelTypeBuiltIn}, {ID: 10, Name: "Other label", Description: "Not a builtin label", LabelType: fleet.LabelTypeRegular}}, nil } summary, err := svc.GetHostSummary(test.UserContext(ctx, test.UserAdmin), nil, nil, nil) require.NoError(t, err) require.Nil(t, summary.TeamID) require.Equal(t, uint(1), summary.OnlineCount) require.Equal(t, uint(5), summary.OfflineCount) require.Equal(t, uint(3), summary.MIACount) require.Equal(t, uint(4), summary.NewCount) require.Equal(t, uint(5), summary.TotalsHostsCount) require.Len(t, summary.Platforms, 4) require.Equal(t, uint(9), summary.AllLinuxCount) require.Nil(t, summary.LowDiskSpaceCount) require.Len(t, summary.BuiltinLabels, 1) require.Equal(t, "All hosts", summary.BuiltinLabels[0].Name) // a user is required _, err = svc.GetHostSummary(ctx, nil, nil, nil) require.Error(t, err) require.Contains(t, err.Error(), authz.ForbiddenErrorMessage) } func TestDeleteHost(t *testing.T) { ds := mysql.CreateMySQLDS(t) defer ds.Close() svc, ctx := newTestService(t, ds, nil, nil) mockClock := clock.NewMockClock() host := test.NewHost(t, ds, "foo", "192.168.1.10", "1", "1", mockClock.Now()) assert.NotZero(t, host.ID) err := svc.DeleteHost(test.UserContext(ctx, test.UserAdmin), host.ID) assert.Nil(t, err) filter := fleet.TeamFilter{User: test.UserAdmin} hosts, err := ds.ListHosts(ctx, filter, fleet.HostListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 0) } func TestAddHostsToTeamByFilter(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) expectedHostIDs := []uint{1, 2, 4} expectedTeam := (*uint)(nil) ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { var hosts []*fleet.Host for _, id := range expectedHostIDs { hosts = append(hosts, &fleet.Host{ID: id}) } return hosts, nil } ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error { assert.Equal(t, expectedTeam, teamID) assert.Equal(t, expectedHostIDs, hostIDs) return nil } ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error { return nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) { return nil, nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } require.NoError(t, svc.AddHostsToTeamByFilter(test.UserContext(ctx, test.UserAdmin), expectedTeam, fleet.HostListOptions{}, nil)) assert.True(t, ds.ListHostsFuncInvoked) assert.True(t, ds.AddHostsToTeamFuncInvoked) } func TestAddHostsToTeamByFilterLabel(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) expectedHostIDs := []uint{6} expectedTeam := ptr.Uint(1) expectedLabel := ptr.Uint(2) ds.ListHostsInLabelFunc = func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) { assert.Equal(t, *expectedLabel, lid) var hosts []*fleet.Host for _, id := range expectedHostIDs { hosts = append(hosts, &fleet.Host{ID: id}) } return hosts, nil } ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error { assert.Equal(t, expectedHostIDs, hostIDs) return nil } ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error { return nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) { return nil, nil } ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id}, nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } require.NoError(t, svc.AddHostsToTeamByFilter(test.UserContext(ctx, test.UserAdmin), expectedTeam, fleet.HostListOptions{}, expectedLabel)) assert.True(t, ds.ListHostsInLabelFuncInvoked) assert.True(t, ds.AddHostsToTeamFuncInvoked) } func TestAddHostsToTeamByFilterEmptyHosts(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { return []*fleet.Host{}, nil } ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error { return nil } ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error { return nil } require.NoError(t, svc.AddHostsToTeamByFilter(test.UserContext(ctx, test.UserAdmin), nil, fleet.HostListOptions{}, nil)) assert.True(t, ds.ListHostsFuncInvoked) assert.False(t, ds.AddHostsToTeamFuncInvoked) } func TestRefetchHost(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) host := &fleet.Host{ID: 3} ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return host, nil } ds.UpdateHostRefetchRequestedFunc = func(ctx context.Context, id uint, value bool) error { assert.Equal(t, host.ID, id) assert.True(t, value) return nil } require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserAdmin), host.ID)) require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserObserver), host.ID)) require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserObserverPlus), host.ID)) require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserMaintainer), host.ID)) assert.True(t, ds.HostLiteFuncInvoked) assert.True(t, ds.UpdateHostRefetchRequestedFuncInvoked) } func TestRefetchHostUserInTeams(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) host := &fleet.Host{ID: 3, TeamID: ptr.Uint(4)} ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return host, nil } ds.UpdateHostRefetchRequestedFunc = func(ctx context.Context, id uint, value bool) error { assert.Equal(t, host.ID, id) assert.True(t, value) return nil } maintainer := &fleet.User{ Teams: []fleet.UserTeam{ { Team: fleet.Team{ID: 4}, Role: fleet.RoleMaintainer, }, }, } require.NoError(t, svc.RefetchHost(test.UserContext(ctx, maintainer), host.ID)) assert.True(t, ds.HostLiteFuncInvoked) assert.True(t, ds.UpdateHostRefetchRequestedFuncInvoked) ds.HostLiteFuncInvoked, ds.UpdateHostRefetchRequestedFuncInvoked = false, false observer := &fleet.User{ Teams: []fleet.UserTeam{ { Team: fleet.Team{ID: 4}, Role: fleet.RoleObserver, }, }, } require.NoError(t, svc.RefetchHost(test.UserContext(ctx, observer), host.ID)) assert.True(t, ds.HostLiteFuncInvoked) assert.True(t, ds.UpdateHostRefetchRequestedFuncInvoked) } func TestEmptyTeamOSVersions(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) testVersions := []fleet.OSVersion{{HostsCount: 1, Name: "macOS 12.1", Platform: "darwin"}} ds.TeamFunc = func(ctx context.Context, teamID uint) (*fleet.Team, error) { if teamID == 1 { return &fleet.Team{ Name: "team1", }, nil } if teamID == 2 { return &fleet.Team{ Name: "team2", }, nil } return nil, newNotFoundError() } ds.OSVersionsFunc = func(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) { if *teamID == 1 { return &fleet.OSVersions{CountsUpdatedAt: time.Now(), OSVersions: testVersions}, nil } if *teamID == 4 { return nil, errors.New("some unknown error") } return nil, newNotFoundError() } // team exists with stats vers, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(1), ptr.String("darwin"), nil, nil) require.NoError(t, err) assert.Len(t, vers.OSVersions, 1) // team exists but no stats vers, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(2), ptr.String("darwin"), nil, nil) require.NoError(t, err) assert.Empty(t, vers.OSVersions) // team does not exist _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(3), ptr.String("darwin"), nil, nil) require.Error(t, err) require.Equal(t, "not found", fmt.Sprint(err)) // some unknown error _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(4), ptr.String("darwin"), nil, nil) require.Error(t, err) require.Equal(t, "some unknown error", fmt.Sprint(err)) } func TestHostEncryptionKey(t *testing.T) { cases := []struct { name string host *fleet.Host allowedUsers []*fleet.User disallowedUsers []*fleet.User }{ { name: "global host", host: &fleet.Host{ ID: 1, Platform: "darwin", NodeKey: ptr.String("test_key"), Hostname: "test_hostname", UUID: "test_uuid", TeamID: nil, }, allowedUsers: []*fleet.User{ test.UserAdmin, test.UserMaintainer, test.UserObserver, test.UserObserverPlus, }, disallowedUsers: []*fleet.User{ test.UserTeamAdminTeam1, test.UserTeamMaintainerTeam1, test.UserTeamObserverTeam1, test.UserNoRoles, }, }, { name: "team host", host: &fleet.Host{ ID: 2, Platform: "darwin", NodeKey: ptr.String("test_key_2"), Hostname: "test_hostname_2", UUID: "test_uuid_2", TeamID: ptr.Uint(1), }, allowedUsers: []*fleet.User{ test.UserAdmin, test.UserMaintainer, test.UserObserver, test.UserObserverPlus, test.UserTeamAdminTeam1, test.UserTeamMaintainerTeam1, test.UserTeamObserverTeam1, test.UserTeamObserverPlusTeam1, }, disallowedUsers: []*fleet.User{ test.UserTeamAdminTeam2, test.UserTeamMaintainerTeam2, test.UserTeamObserverTeam2, test.UserTeamObserverPlusTeam2, test.UserNoRoles, }, }, } testBMToken := &nanodep_client.OAuth1Tokens{ ConsumerKey: "test_consumer", ConsumerSecret: "test_secret", AccessToken: "test_access_token", AccessSecret: "test_access_secret", AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), } testCert, testKey, err := apple_mdm.NewSCEPCACertKey() require.NoError(t, err) testCertPEM := tokenpki.PEMCertificate(testCert.Raw) testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey) fleetCfg := config.TestConfig() config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, testBMToken, "") recoveryKey := "AAA-BBB-CCC" encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{testCert}) require.NoError(t, err) base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey) wstep, _, _, err := fleetCfg.MDM.MicrosoftWSTEP() require.NoError(t, err) winEncryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{wstep.Leaf}) require.NoError(t, err) winBase64EncryptedKey := base64.StdEncoding.EncodeToString(winEncryptedKey) for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ds := new(mock.Store) ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil } svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { require.Equal(t, tt.host.ID, id) return tt.host, nil } ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { return &fleet.HostDiskEncryptionKey{ Base64Encrypted: base64EncryptedKey, Decryptable: ptr.Bool(true), }, nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { act := activity.(fleet.ActivityTypeReadHostDiskEncryptionKey) require.Equal(t, tt.host.ID, act.HostID) require.EqualValues(t, act.HostDisplayName, tt.host.DisplayName()) return nil } t.Run("allowed users", func(t *testing.T) { for _, u := range tt.allowedUsers { _, err := svc.HostEncryptionKey(test.UserContext(ctx, u), tt.host.ID) require.NoError(t, err) } }) t.Run("disallowed users", func(t *testing.T) { for _, u := range tt.disallowedUsers { _, err := svc.HostEncryptionKey(test.UserContext(ctx, u), tt.host.ID) require.Error(t, err) require.Contains(t, authz.ForbiddenErrorMessage, err.Error()) } }) t.Run("no user in context", func(t *testing.T) { _, err := svc.HostEncryptionKey(ctx, tt.host.ID) require.Error(t, err) require.Contains(t, authz.ForbiddenErrorMessage, err.Error()) }) }) } t.Run("test error cases", func(t *testing.T) { ds := new(mock.Store) ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil } svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) ctx = test.UserContext(ctx, test.UserAdmin) hostErr := errors.New("host error") ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return nil, hostErr } _, err := svc.HostEncryptionKey(ctx, 1) require.ErrorIs(t, err, hostErr) ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return &fleet.Host{}, nil } keyErr := errors.New("key error") ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { return nil, keyErr } _, err = svc.HostEncryptionKey(ctx, 1) require.ErrorIs(t, err, keyErr) ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { return &fleet.HostDiskEncryptionKey{Base64Encrypted: "key"}, nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return errors.New("activity error") } _, err = svc.HostEncryptionKey(ctx, 1) require.Error(t, err) }) t.Run("host platform mdm enabled", func(t *testing.T) { cases := []struct { hostPlatform string macMDMEnabled bool winMDMEnabled bool shouldFail bool }{ {"windows", true, false, true}, {"windows", false, true, false}, {"windows", true, true, false}, {"darwin", true, false, false}, {"darwin", false, true, true}, {"darwin", true, true, false}, } for _, c := range cases { t.Run(fmt.Sprintf("%s: mac mdm: %t; win mdm: %t", c.hostPlatform, c.macMDMEnabled, c.winMDMEnabled), func(t *testing.T) { ds := new(mock.Store) ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: c.macMDMEnabled, WindowsEnabledAndConfigured: c.winMDMEnabled}}, nil } ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return &fleet.Host{Platform: c.hostPlatform}, nil } ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { key := base64EncryptedKey if c.hostPlatform == "windows" { key = winBase64EncryptedKey } return &fleet.HostDiskEncryptionKey{ Base64Encrypted: key, Decryptable: ptr.Bool(true), }, nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) ctx = test.UserContext(ctx, test.UserAdmin) _, err := svc.HostEncryptionKey(ctx, 1) if c.shouldFail { require.Error(t, err) require.ErrorContains(t, err, fleet.ErrMDMNotConfigured.Error()) } else { require.NoError(t, err) } }) } }) } func TestHostMDMProfileDetail(t *testing.T) { ds := new(mock.Store) testBMToken := &nanodep_client.OAuth1Tokens{ ConsumerKey: "test_consumer", ConsumerSecret: "test_secret", AccessToken: "test_access_token", AccessSecret: "test_access_secret", AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), } testCert, testKey, err := apple_mdm.NewSCEPCACertKey() require.NoError(t, err) testCertPEM := tokenpki.PEMCertificate(testCert.Raw) testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey) fleetCfg := config.TestConfig() config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, testBMToken, "") svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) ctx = test.UserContext(ctx, test.UserAdmin) ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return &fleet.Host{ ID: 1, Platform: "darwin", }, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil } ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return nil, nil } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return nil, nil } ds.ListHostBatteriesFunc = func(ctx context.Context, hid uint) ([]*fleet.HostBattery, error) { return nil, nil } ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) { return nil, nil } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{ MDM: fleet.MDM{ EnabledAndConfigured: true, }, }, nil } cases := []struct { name string storedDetail string expectedDetail string }{ { name: "no detail", storedDetail: "", expectedDetail: "", }, { name: "other detail", storedDetail: "other detail", expectedDetail: "other detail", }, { name: "failed was verifying", storedDetail: string(fleet.HostMDMProfileDetailFailedWasVerifying), expectedDetail: fleet.HostMDMProfileDetailFailedWasVerifying.Message(), }, { name: "failed was verified", storedDetail: string(fleet.HostMDMProfileDetailFailedWasVerified), expectedDetail: fleet.HostMDMProfileDetailFailedWasVerified.Message(), }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, host_uuid string) ([]fleet.HostMDMAppleProfile, error) { return []fleet.HostMDMAppleProfile{ { Name: "test", Identifier: "test", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryFailed, Detail: tt.storedDetail, }, }, nil } h, err := svc.GetHost(ctx, uint(1), fleet.HostDetailOptions{}) require.NoError(t, err) require.NotNil(t, h.MDM.Profiles) profs := *h.MDM.Profiles require.Len(t, profs, 1) require.Equal(t, tt.expectedDetail, profs[0].Detail) }) } }