fleet/server/service/hosts_test.go

2793 lines
99 KiB
Go
Raw Normal View History

package service
import (
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"testing"
"time"
2021-12-14 21:34:11 +00:00
"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/capabilities"
"github.com/fleetdm/fleet/v4/server/contexts/license"
2021-12-14 21:34:11 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"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/nanodep/tokenpki"
"github.com/fleetdm/fleet/v4/server/mock"
2021-12-14 21:34:11 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/smallstep/pkcs7"
2021-12-14 21:34:11 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2021-12-14 21:34:11 +00:00
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
}
2021-12-14 21:34:11 +00:00
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
}
2022-06-01 16:06:57 +00:00
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
2021-12-14 21:34:11 +00:00
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: "Normal"}, {HostID: host.ID, SerialNumber: "b", CycleCount: 1001, Health: "Service recommended"}}
ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) {
return dsBats, nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, nil
}
2021-12-14 21:34:11 +00:00
opts := fleet.HostDetailOptions{
IncludeCVEScores: false,
IncludePolicies: false,
}
hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), host, opts)
2021-12-14 21:34:11 +00:00
require.NoError(t, err)
assert.Equal(t, expectedLabels, hostDetail.Labels)
assert.Equal(t, expectedPacks, hostDetail.Packs)
require.NotNil(t, hostDetail.Batteries)
assert.Equal(t, dsBats, *hostDetail.Batteries)
require.Nil(t, hostDetail.MDM.MacOSSettings)
}
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
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
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
return nil, nil, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, 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.ActionRequiredRotateKey,
&fleet.MDMDeliveryPending,
},
{
"installed profile, unknown decryptable",
nil,
&fleet.HostMDMAppleProfile{
HostUUID: "abc",
Identifier: mobileconfig.FleetFileVaultPayloadIdentifier,
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
},
fleet.DiskEncryptionVerifying,
"",
&fleet.MDMDeliveryVerifying,
},
{
"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)
}
})
}
2021-12-14 21:34:11 +00:00
}
func TestHostDetailsMDMTimestamps(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, WindowsEnabledAndConfigured: 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
}
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, 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
}
Implement BitLocker "action required" status (#31451) for #31182 # Details This PR implements the "Action Required" state for Windows host disk encryption. This includes updates to reporting for: * disk encryption summary (`GET /fleet/disk_encryption`) * config profiles summary (`GET /configuration_profiles/summary`) * config profile status ( `GET /configuration_profiles/{profile_uuid}/status`) For disk encryption summary, the statuses are now determined according to [the rules in the Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0). TL;DR if the criteria for "verified" or "verifying" are set, but a required PIN is not set, we report a host as "action required". For profiles, I followed what seems to be the existing pattern and set the profile status to "pending" if the disk encryption status is "action required". This is what we do for hosts with the "enforcing" or "removing enforcement" statuses. A lot of the changes in these files are due to the creation of the `fleet.DiskEncryptionConfig` struct to hold info about disk encryption config, and passing variables of that type to various functions instead of passing a `bool` to indicate whether encryption is enabled. Other than that, the functional changes are constrained to a few files. > Note: to get the "require bitlocker pin" UI, compile the front end with: ``` SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch ``` # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Changelog will be added when feature is complete. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually Could use some help testing this end-to-end. I was able to test the banners showing up correctly, but testing the Disk Encryption table requires some Windows-MDM-fu (I just get all zeroes). ## Database migrations - [X] Checked table schema to confirm autoupdate - [X] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [X] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [X] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
ds.GetConfigEnableDiskEncryptionFunc = func(ctx context.Context, teamID *uint) (fleet.DiskEncryptionConfig, error) {
return fleet.DiskEncryptionConfig{}, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, nil
}
ts1 := time.Now().Add(-1 * time.Hour).UTC()
ts2 := time.Now().Add(-2 * time.Hour).UTC()
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
return &ts1, &ts2, nil
}
cases := []struct {
platform string
platformIsApple bool
}{
{"darwin", true},
{"ios", true},
{"ipados", true},
{"windows", false},
{"ubuntu", false},
{"centos", false},
{"rhel", false},
{"debian", false},
}
for _, testcase := range cases {
t.Run("test MDM timestamps on platform "+testcase.platform, func(t *testing.T) {
ds.GetNanoMDMEnrollmentTimesFuncInvoked = false
host := &fleet.Host{ID: 3, MDM: fleet.MDMHostData{}, Platform: testcase.platform, UUID: "abc123"}
opts := fleet.HostDetailOptions{
IncludeCVEScores: false,
IncludePolicies: false,
ExcludeSoftware: true,
IncludeCriticalVulnerabilitiesCount: false,
}
hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), host, opts)
require.NoError(t, err)
if testcase.platformIsApple {
assert.True(t, ds.GetNanoMDMEnrollmentTimesFuncInvoked)
require.NotNil(t, hostDetail.LastMDMEnrolledAt)
assert.Equal(t, *hostDetail.LastMDMEnrolledAt, ts1)
require.NotNil(t, hostDetail.LastMDMCheckedInAt)
assert.Equal(t, *hostDetail.LastMDMCheckedInAt, ts2)
} else {
assert.False(t, ds.GetNanoMDMEnrollmentTimesFuncInvoked)
assert.Nil(t, hostDetail.LastMDMEnrolledAt)
assert.Nil(t, hostDetail.LastMDMCheckedInAt)
}
})
}
}
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
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
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{}, nil
}
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
return nil, nil, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, 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, ""},
// TeamID necessary to check whether disk encryption is enabled for Linux hosts, in lieu of
// MDM-related logic which doesn't apply to Linux hosts
{"ubuntu", &fleet.Host{ID: 42, Platform: "ubuntu", TeamID: ptr.Uint(1)}, 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.GetHostMDMFuncInvoked = false
ds.GetConfigEnableDiskEncryptionFuncInvoked = 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
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
hmdm := fleet.HostMDM{Enrolled: true, IsServer: false}
return &hmdm, nil
}
Implement BitLocker "action required" status (#31451) for #31182 # Details This PR implements the "Action Required" state for Windows host disk encryption. This includes updates to reporting for: * disk encryption summary (`GET /fleet/disk_encryption`) * config profiles summary (`GET /configuration_profiles/summary`) * config profile status ( `GET /configuration_profiles/{profile_uuid}/status`) For disk encryption summary, the statuses are now determined according to [the rules in the Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0). TL;DR if the criteria for "verified" or "verifying" are set, but a required PIN is not set, we report a host as "action required". For profiles, I followed what seems to be the existing pattern and set the profile status to "pending" if the disk encryption status is "action required". This is what we do for hosts with the "enforcing" or "removing enforcement" statuses. A lot of the changes in these files are due to the creation of the `fleet.DiskEncryptionConfig` struct to hold info about disk encryption config, and passing variables of that type to various functions instead of passing a `bool` to indicate whether encryption is enabled. Other than that, the functional changes are constrained to a few files. > Note: to get the "require bitlocker pin" UI, compile the front end with: ``` SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch ``` # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Changelog will be added when feature is complete. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually Could use some help testing this end-to-end. I was able to test the banners showing up correctly, but testing the Disk Encryption table requires some Windows-MDM-fu (I just get all zeroes). ## Database migrations - [X] Checked table schema to confirm autoupdate - [X] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [X] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [X] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
ds.GetConfigEnableDiskEncryptionFunc = func(ctx context.Context, teamID *uint) (fleet.DiskEncryptionConfig, error) {
// testing API response when not enabled
Implement BitLocker "action required" status (#31451) for #31182 # Details This PR implements the "Action Required" state for Windows host disk encryption. This includes updates to reporting for: * disk encryption summary (`GET /fleet/disk_encryption`) * config profiles summary (`GET /configuration_profiles/summary`) * config profile status ( `GET /configuration_profiles/{profile_uuid}/status`) For disk encryption summary, the statuses are now determined according to [the rules in the Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0). TL;DR if the criteria for "verified" or "verifying" are set, but a required PIN is not set, we report a host as "action required". For profiles, I followed what seems to be the existing pattern and set the profile status to "pending" if the disk encryption status is "action required". This is what we do for hosts with the "enforcing" or "removing enforcement" statuses. A lot of the changes in these files are due to the creation of the `fleet.DiskEncryptionConfig` struct to hold info about disk encryption config, and passing variables of that type to various functions instead of passing a `bool` to indicate whether encryption is enabled. Other than that, the functional changes are constrained to a few files. > Note: to get the "require bitlocker pin" UI, compile the front end with: ``` SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch ``` # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Changelog will be added when feature is complete. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually Could use some help testing this end-to-end. I was able to test the banners showing up correctly, but testing the Disk Encryption table requires some Windows-MDM-fu (I just get all zeroes). ## Database migrations - [X] Checked table schema to confirm autoupdate - [X] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [X] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [X] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
return fleet.DiskEncryptionConfig{}, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, 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.licenseTier == fleet.TierPremium {
require.True(t, ds.GetHostMDMFuncInvoked)
} else {
require.False(t, ds.GetHostMDMFuncInvoked)
}
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 "ubuntu":
require.False(t, ds.GetHostMDMAppleProfilesFuncInvoked)
require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked)
// service should call this function to check whether disk encryption is enabled for a Linux host
require.True(t, ds.GetConfigEnableDiskEncryptionFuncInvoked)
// `hostDetail.MDM.OSSettings` and `hostDetail.MDM.OSSettings.DiskEncryption` will actually not
// be `nil` here due to the way those fields are initialized by `svc.ds.Host`, so we can't
// expect them to be `nil` in these tests. However, since the relevant struct tags are set to
// `omitempty`, the resulting API response WILL omit these fields/subfields when empty,
// which is confirmed at the integration layer.
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)
}
})
}
}
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
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
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, 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
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
hmdm := fleet.HostMDM{Enrolled: true, IsServer: false}
return &hmdm, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, 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)
}
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2021-12-14 21:34:11 +00:00
func TestHostAuth(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
2021-12-14 21:34:11 +00:00
teamHost := &fleet.Host{TeamID: ptr.Uint(1)}
globalHost := &fleet.Host{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
2021-12-14 21:34:11 +00:00
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) {
2021-12-14 21:34:11 +00:00
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
}
2022-06-01 16:06:57 +00:00
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
2021-12-14 21:34:11 +00:00
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, params *fleet.AddHostsToTeamParams) error {
2021-12-14 21:34:11 +00:00
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
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
2021-12-14 21:34:11 +00:00
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,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, 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, details []byte, createdAt time.Time) 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
}
ds.ListHostUpcomingActivitiesFunc = func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.UpcomingActivity, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.ListHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, opts fleet.HostSoftwareTitleListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
return true, nil
}
ds.ListHostCertificatesFunc = func(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificateRecord, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
2021-12-14 21:34:11 +00:00
ds.GetCategoriesForSoftwareTitlesFunc = func(ctx context.Context, softwareTitleIDs []uint, team_id *uint) (map[uint][]string, error) {
return map[uint][]string{}, nil
}
ds.UpdateHostIssuesFailingPoliciesFunc = func(ctx context.Context, hostIDs []uint) error {
return nil
}
Prevent deadlocks by adding FOR UPDATE locks (#32173) Fixes #31173 Reproduced and fixed in loadtest environment. Uncovered another source of deadlocks, filed as a separate: https://github.com/fleetdm/fleet/issues/32201 - Also, still seeing some deadlocks (a lot fewer) in DB, and they are hidden from the API results by retries. They may still be happening because locks happen row by row and not all at once. A potential fix would be to lock the whole policy_membership table. Additional frontend fix, which is needed to prevent potential timeouts: https://github.com/fleetdm/fleet/pull/32212 Backend + frontend fix should be a sufficient fix for this issue (ignoring the issue with the long software transaction). Also, this PR contains some refactoring to split out the 1-host use case. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * Bug Fixes * Resolved rare deadlocks during concurrent policy updates and bulk automations. * Correctly clears stale MDM data and actions on host re-enrollment and platform changes. * Performance Improvements * Optimized policy issue recalculation with per-host updates to reduce contention. * Improved concurrency handling for bulk policy updates to avoid lock contention. * Reliability * More robust host enrollment: updates seen time, display name, and label membership consistently. * Ensures accurate policy-issue counts after membership changes and re-enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-22 17:36:03 +00:00
ds.UpdateHostIssuesFailingPoliciesForSingleHostFunc = func(ctx context.Context, hostID uint) error {
return nil
}
ds.GetHostIssuesLastUpdatedFunc = func(ctx context.Context, hostId uint) (time.Time, error) {
return time.Time{}, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, nil
}
2021-12-14 21:34:11 +00:00
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,
},
2021-12-14 21:34:11 +00:00
{
"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,
},
2021-12-14 21:34:11 +00:00
{
"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,
}
2021-12-14 21:34:11 +00:00
_, err := svc.GetHost(ctx, 1, opts)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailTeamRead, err)
2023-12-11 22:33:31 +00:00
_, err = svc.GetHostLite(ctx, 1)
checkAuthErr(t, tt.shouldFailTeamRead, err)
_, err = svc.HostByIdentifier(ctx, "1", opts)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailTeamRead, err)
_, _, err = svc.ListHostUpcomingActivities(ctx, 1, fleet.ListOptions{})
checkAuthErr(t, tt.shouldFailTeamRead, err)
_, err = svc.GetHost(ctx, 2, opts)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailGlobalRead, err)
2023-12-11 22:33:31 +00:00
_, err = svc.GetHostLite(ctx, 2)
checkAuthErr(t, tt.shouldFailGlobalRead, err)
_, err = svc.HostByIdentifier(ctx, "2", opts)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailGlobalRead, err)
_, _, err = svc.ListHostUpcomingActivities(ctx, 2, fleet.ListOptions{})
checkAuthErr(t, tt.shouldFailGlobalRead, err)
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailTeamWrite, err)
err = svc.DeleteHosts(ctx, []uint{2}, nil)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
err = svc.AddHostsToTeam(ctx, ptr.Uint(1), []uint{1}, false)
2021-12-14 21:34:11 +00:00
checkAuthErr(t, tt.shouldFailTeamWrite, err)
emptyFilter := make(map[string]interface{})
err = svc.AddHostsToTeamByFilter(ctx, ptr.Uint(1), &emptyFilter)
2021-12-14 21:34:11 +00:00
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)
_, _, err = svc.ListHostSoftware(ctx, 1, fleet.HostSoftwareTitleListOptions{})
checkAuthErr(t, tt.shouldFailTeamRead, err)
_, _, err = svc.ListHostSoftware(ctx, 2, fleet.HostSoftwareTitleListOptions{})
checkAuthErr(t, tt.shouldFailGlobalRead, err)
_, _, err = svc.ListHostCertificates(ctx, 1, fleet.ListOptions{})
checkAuthErr(t, tt.shouldFailTeamRead, err)
_, _, err = svc.ListHostCertificates(ctx, 2, fleet.ListOptions{})
checkAuthErr(t, tt.shouldFailGlobalRead, err)
2021-12-14 21:34:11 +00:00
})
}
// List, GetHostSummary work for all
2021-12-14 21:34:11 +00:00
}
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
}
userContext := test.UserContext(ctx, test.UserAdmin)
hosts, err := svc.ListHosts(userContext, 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)
var shouldIncludeCVEScores bool
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
require.Equal(t, shouldIncludeCVEScores, includeCVEScores)
return nil
}
// free license disallows getting vuln details
hosts, err = svc.ListHosts(userContext, fleet.HostListOptions{PopulateSoftware: true, PopulateSoftwareVulnerabilityDetails: true})
require.NoError(t, err)
require.Len(t, hosts, 1)
require.True(t, ds.LoadHostSoftwareFuncInvoked)
ds.LoadHostSoftwareFuncInvoked = false
// you're allowed to skip vuln details on Premium
userContext = license.NewContext(userContext, &fleet.LicenseInfo{Tier: fleet.TierPremium})
hosts, err = svc.ListHosts(userContext, fleet.HostListOptions{PopulateSoftware: true, PopulateSoftwareVulnerabilityDetails: false})
require.NoError(t, err)
require.Len(t, hosts, 1)
require.True(t, ds.LoadHostSoftwareFuncInvoked)
ds.LoadHostSoftwareFuncInvoked = false
// you're allowed to retrieve vuln details on Premium
shouldIncludeCVEScores = true
hosts, err = svc.ListHosts(userContext, fleet.HostListOptions{PopulateSoftware: true, PopulateSoftwareVulnerabilityDetails: true})
require.NoError(t, err)
require.Len(t, hosts, 1)
require.True(t, ds.LoadHostSoftwareFuncInvoked)
}
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)
}
2021-12-14 21:34:11 +00:00
func TestDeleteHost(t *testing.T) {
ds := mysql.CreateMySQLDS(t)
defer ds.Close()
svc, ctx := newTestService(t, ds, nil, nil)
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
assert.Nil(t, err)
filter := fleet.TeamFilter{User: test.UserAdmin}
hosts, err := ds.ListHosts(ctx, filter, fleet.HostListOptions{})
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
expectedHostIDs := []uint{1, 2, 4}
expectedTeam := (*uint)(nil)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
2021-12-14 21:34:11 +00:00
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, params *fleet.AddHostsToTeamParams) error {
assert.Equal(t, expectedTeam, params.TeamID)
assert.Equal(t, expectedHostIDs, params.HostIDs)
2021-12-14 21:34:11 +00:00
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, 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, details []byte, createdAt time.Time,
) error {
return nil
}
2021-12-14 21:34:11 +00:00
emptyRequest := &map[string]interface{}{}
require.NoError(t, svc.AddHostsToTeamByFilter(test.UserContext(ctx, test.UserAdmin), expectedTeam, emptyRequest))
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
expectedHostIDs := []uint{6}
expectedTeam := ptr.Uint(1)
expectedLabel := float64(2)
2021-12-14 21:34:11 +00:00
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
2021-12-14 21:34:11 +00:00
ds.ListHostsInLabelFunc = func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
assert.Equal(t, uint(expectedLabel), lid)
2021-12-14 21:34:11 +00:00
var hosts []*fleet.Host
for _, id := range expectedHostIDs {
hosts = append(hosts, &fleet.Host{ID: id})
}
return hosts, nil
}
ds.AddHostsToTeamFunc = func(ctx context.Context, params *fleet.AddHostsToTeamParams) error {
assert.Equal(t, expectedHostIDs, params.HostIDs)
2021-12-14 21:34:11 +00:00
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, 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, details []byte, createdAt time.Time,
) error {
return nil
}
2021-12-14 21:34:11 +00:00
filter := &map[string]interface{}{"label_id": expectedLabel}
require.NoError(t, svc.AddHostsToTeamByFilter(test.UserContext(ctx, test.UserAdmin), expectedTeam, filter))
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
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, params *fleet.AddHostsToTeamParams) error {
2021-12-14 21:34:11 +00:00
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
2021-12-14 21:34:11 +00:00
emptyFilter := &map[string]interface{}{}
require.NoError(t, svc.AddHostsToTeamByFilter(test.UserContext(ctx, test.UserAdmin), nil, emptyFilter))
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
host := &fleet.Host{ID: 3}
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
2021-12-14 21:34:11 +00:00
return host, nil
}
ds.UpdateHostRefetchRequestedFunc = func(ctx context.Context, id uint, value bool) error {
assert.Equal(t, host.ID, id)
assert.True(t, value)
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
}
func TestRefetchHostUserInTeams(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
2021-12-14 21:34:11 +00:00
host := &fleet.Host{ID: 3, TeamID: ptr.Uint(4)}
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
2021-12-14 21:34:11 +00:00
return host, nil
}
ds.UpdateHostRefetchRequestedFunc = func(ctx context.Context, id uint, value bool) error {
assert.Equal(t, host.ID, id)
assert.True(t, value)
2021-12-14 21:34:11 +00:00
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
2021-12-14 21:34:11 +00:00
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)
2021-12-14 21:34:11 +00:00
}
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.TeamExistsFunc = func(ctx context.Context, teamID uint) (bool, error) {
if teamID == 3 {
return false, nil
}
return true, nil
}
ds.OSVersionsFunc = func(
ctx context.Context, teamFilter *fleet.TeamFilter, platform *string, name *string, version *string,
) (*fleet.OSVersions, error) {
if *teamFilter.TeamID == 1 {
return &fleet.OSVersions{CountsUpdatedAt: time.Now(), OSVersions: testVersions}, nil
}
if *teamFilter.TeamID == 4 {
return nil, errors.New("some unknown error")
}
Add UUID to Fleet errors and clean up error msgs (#10411) #8129 Apart from fixing the issue in #8129, this change also introduces UUIDs to Fleet errors. To be able to match a returned error from the API to a error in the Fleet logs. See https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for more context. Samples with the changes in this PR: ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d '' { "message": "Bad request", "errors": [ { "name": "base", "reason": "Expected JSON Body" } ], "uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0" } ``` ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd' { "message": "Bad request", "errors": [ { "name": "base", "reason": "json decoder error" } ], "uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d" } ``` ``` curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams" { "message": "Authentication required", "errors": [ { "name": "base", "reason": "Authentication required" } ], "uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Authorization header required", "errors": [ { "name": "base", "reason": "Authorization header required" } ], "uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Permission Denied", "uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06" } ``` - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil, newNotFoundError()
}
Fixed MySQL DB performance regressions (#33184) Resolves #33147 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Resolved MySQL performance regressions from 4.73.0/4.73.1 affecting OS versions and software titles views, improving load times and reducing timeouts. - Refactor - Optimized OS vulnerabilities fetching by batching multiple OS versions in a single request. - Added a supporting database index to speed kernel-related vulnerability queries. - Tests - Added comprehensive tests for multi-OS vulnerability retrieval, CVSS enrichment, team-scoped data, and service endpoint behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 20:35:05 +00:00
ds.ListVulnsByMultipleOSVersionsFunc = func(ctx context.Context, osVersions []fleet.OSVersion, includeCVSS bool,
2025-10-01 18:11:27 +00:00
teamID *uint,
) (map[string]fleet.Vulnerabilities, error) {
Fixed MySQL DB performance regressions (#33184) Resolves #33147 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Resolved MySQL performance regressions from 4.73.0/4.73.1 affecting OS versions and software titles views, improving load times and reducing timeouts. - Refactor - Optimized OS vulnerabilities fetching by batching multiple OS versions in a single request. - Added a supporting database index to speed kernel-related vulnerability queries. - Tests - Added comprehensive tests for multi-OS vulnerability retrieval, CVSS enrichment, team-scoped data, and service endpoint behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 20:35:05 +00:00
return nil, nil
}
// team exists with stats
vers, _, _, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(1), ptr.String("darwin"), nil, nil, fleet.ListOptions{}, false)
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, fleet.ListOptions{}, false)
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, fleet.ListOptions{}, false)
require.Error(t, err)
require.Contains(t, fmt.Sprint(err), "does not exist")
// some unknown error
_, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(4), ptr.String("darwin"), nil, nil, fleet.ListOptions{}, false)
require.Error(t, err)
require.Equal(t, "some unknown error", fmt.Sprint(err))
}
func TestOSVersionsListOptions(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
testVersions := []fleet.OSVersion{
{HostsCount: 4, NameOnly: "Windows 11 Pro 22H2", Platform: "windows"},
{HostsCount: 1, NameOnly: "macOS 12.1", Platform: "darwin"},
{HostsCount: 2, NameOnly: "macOS 12.2", Platform: "darwin"},
{HostsCount: 3, NameOnly: "Windows 11 Pro 21H2", Platform: "windows"},
{HostsCount: 5, NameOnly: "Ubuntu 20.04", Platform: "ubuntu"},
{HostsCount: 6, NameOnly: "Ubuntu 21.04", Platform: "ubuntu"},
}
2025-10-01 18:11:27 +00:00
now := time.Now()
ds.OSVersionsFunc = func(
ctx context.Context, teamFilter *fleet.TeamFilter, platform *string, name *string, version *string,
) (*fleet.OSVersions, error) {
2025-10-01 18:11:27 +00:00
return &fleet.OSVersions{CountsUpdatedAt: now, OSVersions: testVersions}, nil
}
Fixed MySQL DB performance regressions (#33184) Resolves #33147 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Resolved MySQL performance regressions from 4.73.0/4.73.1 affecting OS versions and software titles views, improving load times and reducing timeouts. - Refactor - Optimized OS vulnerabilities fetching by batching multiple OS versions in a single request. - Added a supporting database index to speed kernel-related vulnerability queries. - Tests - Added comprehensive tests for multi-OS vulnerability retrieval, CVSS enrichment, team-scoped data, and service endpoint behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 20:35:05 +00:00
ds.ListVulnsByMultipleOSVersionsFunc = func(ctx context.Context, osVersions []fleet.OSVersion, includeCVSS bool,
2025-10-01 18:11:27 +00:00
teamID *uint,
) (map[string]fleet.Vulnerabilities, error) {
Fixed MySQL DB performance regressions (#33184) Resolves #33147 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Resolved MySQL performance regressions from 4.73.0/4.73.1 affecting OS versions and software titles views, improving load times and reducing timeouts. - Refactor - Optimized OS vulnerabilities fetching by batching multiple OS versions in a single request. - Added a supporting database index to speed kernel-related vulnerability queries. - Tests - Added comprehensive tests for multi-OS vulnerability retrieval, CVSS enrichment, team-scoped data, and service endpoint behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 20:35:05 +00:00
return nil, nil
}
// test default descending count sort
opts := fleet.ListOptions{}
vers, _, _, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 6)
assert.Equal(t, "Ubuntu 21.04", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Ubuntu 20.04", vers.OSVersions[1].NameOnly)
assert.Equal(t, "Windows 11 Pro 22H2", vers.OSVersions[2].NameOnly)
assert.Equal(t, "Windows 11 Pro 21H2", vers.OSVersions[3].NameOnly)
assert.Equal(t, "macOS 12.2", vers.OSVersions[4].NameOnly)
assert.Equal(t, "macOS 12.1", vers.OSVersions[5].NameOnly)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
// test ascending count sort
opts = fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderAscending}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 6)
assert.Equal(t, "macOS 12.1", vers.OSVersions[0].NameOnly)
assert.Equal(t, "macOS 12.2", vers.OSVersions[1].NameOnly)
assert.Equal(t, "Windows 11 Pro 21H2", vers.OSVersions[2].NameOnly)
assert.Equal(t, "Windows 11 Pro 22H2", vers.OSVersions[3].NameOnly)
assert.Equal(t, "Ubuntu 20.04", vers.OSVersions[4].NameOnly)
assert.Equal(t, "Ubuntu 21.04", vers.OSVersions[5].NameOnly)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
// pagination
opts = fleet.ListOptions{Page: 0, PerPage: 2}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 2)
assert.Equal(t, "Ubuntu 21.04", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Ubuntu 20.04", vers.OSVersions[1].NameOnly)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
opts = fleet.ListOptions{Page: 1, PerPage: 2}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 2)
assert.Equal(t, "Windows 11 Pro 22H2", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Windows 11 Pro 21H2", vers.OSVersions[1].NameOnly)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
// pagination + ascending hosts_count sort
opts = fleet.ListOptions{Page: 0, PerPage: 2, OrderKey: "hosts_count", OrderDirection: fleet.OrderAscending}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 2)
assert.Equal(t, "macOS 12.1", vers.OSVersions[0].NameOnly)
assert.Equal(t, "macOS 12.2", vers.OSVersions[1].NameOnly)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
// per page too high
opts = fleet.ListOptions{Page: 0, PerPage: 1000}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 6)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
// Page number too high
opts = fleet.ListOptions{Page: 1000, PerPage: 2}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 0)
2025-10-01 18:11:27 +00:00
assert.Equal(t, now, vers.CountsUpdatedAt)
}
func TestOSVersionsDefaultPagination(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
testVersions := []fleet.OSVersion{}
for i := range 50 {
testVersions = append(testVersions, fleet.OSVersion{NameOnly: fmt.Sprintf("Version %02d", i), HostsCount: i, Platform: "windows"})
}
ds.OSVersionsFunc = func(
ctx context.Context, teamFilter *fleet.TeamFilter, platform *string, name *string, version *string,
) (*fleet.OSVersions, error) {
return &fleet.OSVersions{CountsUpdatedAt: time.Now(), OSVersions: testVersions}, nil
}
ds.ListVulnsByMultipleOSVersionsFunc = func(ctx context.Context, osVersions []fleet.OSVersion, includeCVSS bool,
teamID *uint,
) (map[string]fleet.Vulnerabilities, error) {
return nil, nil
}
// test default descending count sort + default pagination (page 0, per_page 20)
opts := fleet.ListOptions{}
vers, _, _, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 20)
assert.Equal(t, "Version 49", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Version 30", vers.OSVersions[19].NameOnly)
}
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,
},
},
}
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, "")
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.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
act := activity.(fleet.ActivityTypeReadHostDiskEncryptionKey)
require.Equal(t, tt.host.ID, act.HostID)
require.Equal(t, []uint{tt.host.ID}, act.HostIDs())
require.EqualValues(t, act.HostDisplayName, tt.host.DisplayName())
return nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM},
}, 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
}
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM},
}, nil
}
_, 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, details []byte, createdAt time.Time,
) 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.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM},
}, 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)
if c.macMDMEnabled && !c.winMDMEnabled && c.hostPlatform == "windows" {
require.ErrorContains(t, err, fleet.ErrWindowsMDMNotConfigured.Error())
} else {
require.ErrorContains(t, err, fleet.ErrMDMNotConfigured.Error())
}
} else {
require.NoError(t, err)
}
})
}
})
t.Run("Linux encryption", func(t *testing.T) {
ds := new(mock.Store)
host := &fleet.Host{ID: 1, Platform: "ubuntu"}
symmetricKey := "this_is_a_32_byte_symmetric_key!"
passphrase := "this_is_a_passphrase"
base64EncryptedKey, err := mdm.EncryptAndEncode(passphrase, symmetricKey)
require.NoError(t, err)
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
return host, nil
}
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { // needed for new activity
return &fleet.AppConfig{}, nil
}
// error when no server private key
fleetCfg.Server.PrivateKey = ""
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
key, err := svc.HostEncryptionKey(ctx, 1)
require.Error(t, err, "private key is unavailable")
require.Nil(t, key)
// error when key is not set
ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{}, nil
}
fleetCfg.Server.PrivateKey = symmetricKey
svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
key, err = svc.HostEncryptionKey(ctx, 1)
require.Error(t, err, "host encryption key is not set")
require.Nil(t, key)
// error when key is not set
ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{
Base64Encrypted: "thisIsWrong",
Decryptable: ptr.Bool(true),
}, nil
}
svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
key, err = svc.HostEncryptionKey(ctx, 1)
require.Error(t, err, "decrypt host encryption key")
require.Nil(t, key)
// happy path
ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{
Base64Encrypted: base64EncryptedKey,
Decryptable: ptr.Bool(true),
}, nil
}
svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
key, err = svc.HostEncryptionKey(ctx, 1)
require.NoError(t, err)
require.Equal(t, passphrase, key.DecryptedValue)
})
}
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
func TestHostMDMProfileDetail(t *testing.T) {
ds := new(mock.Store)
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, "")
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
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
},
}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
return nil, nil, nil
}
ds.UpdateHostIssuesFailingPoliciesFunc = func(ctx context.Context, hostIDs []uint) error {
return nil
}
Prevent deadlocks by adding FOR UPDATE locks (#32173) Fixes #31173 Reproduced and fixed in loadtest environment. Uncovered another source of deadlocks, filed as a separate: https://github.com/fleetdm/fleet/issues/32201 - Also, still seeing some deadlocks (a lot fewer) in DB, and they are hidden from the API results by retries. They may still be happening because locks happen row by row and not all at once. A potential fix would be to lock the whole policy_membership table. Additional frontend fix, which is needed to prevent potential timeouts: https://github.com/fleetdm/fleet/pull/32212 Backend + frontend fix should be a sufficient fix for this issue (ignoring the issue with the long software transaction). Also, this PR contains some refactoring to split out the 1-host use case. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * Bug Fixes * Resolved rare deadlocks during concurrent policy updates and bulk automations. * Correctly clears stale MDM data and actions on host re-enrollment and platform changes. * Performance Improvements * Optimized policy issue recalculation with per-host updates to reduce contention. * Improved concurrency handling for bulk policy updates to avoid lock contention. * Reliability * More robust host enrollment: updates seen time, display name, and label membership consistently. * Ensures accurate policy-issue counts after membership changes and re-enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-22 17:36:03 +00:00
ds.UpdateHostIssuesFailingPoliciesForSingleHostFunc = func(ctx context.Context, hostID uint) error {
return nil
}
ds.GetHostIssuesLastUpdatedFunc = func(ctx context.Context, hostId uint) (time.Time, error) {
return time.Time{}, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, 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)
})
}
}
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
func TestHostMDMProfileScopes(t *testing.T) {
ds := new(mock.Store)
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, "")
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
appleHost := &fleet.Host{
ID: 1,
UUID: "apple-host-uuid",
Platform: "darwin",
}
windowsHost := &fleet.Host{
ID: 2,
UUID: "windows-host-uuid",
Platform: "windows",
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
if id == appleHost.ID {
return appleHost, nil
}
require.Equal(t, id, windowsHost.ID, "Host should only be called with Apple or Windows host IDs")
return windowsHost, 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.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.UpdateHostIssuesFailingPoliciesForSingleHostFunc = func(ctx context.Context, hostID uint) error {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
},
}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
return nil, nil, nil
}
ds.UpdateHostIssuesFailingPoliciesFunc = func(ctx context.Context, hostIDs []uint) error {
return nil
}
ds.GetHostIssuesLastUpdatedFunc = func(ctx context.Context, hostId uint) (time.Time, error) {
return time.Time{}, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, nil
}
appleCases := []struct {
name string
storedProfiles []fleet.HostMDMAppleProfile
expectedProfiles []fleet.HostMDMProfile
}{
{
name: "no profiles",
storedProfiles: nil,
expectedProfiles: nil,
},
{
name: "system scoped profile",
storedProfiles: []fleet.HostMDMAppleProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: fleet.PayloadScopeSystem}},
expectedProfiles: []fleet.HostMDMProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: ptr.String("device"), ManagedLocalAccount: ptr.String("")}},
},
{
name: "User scoped profile with username",
storedProfiles: []fleet.HostMDMAppleProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: fleet.PayloadScopeUser, ManagedLocalAccount: "fleetie"}},
expectedProfiles: []fleet.HostMDMProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: ptr.String("user"), ManagedLocalAccount: ptr.String("fleetie")}},
},
{
name: "User scoped profile without username for some reason",
storedProfiles: []fleet.HostMDMAppleProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: fleet.PayloadScopeUser}},
expectedProfiles: []fleet.HostMDMProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: ptr.String("user"), ManagedLocalAccount: ptr.String("")}},
},
{
name: "system + user scoped profiles",
storedProfiles: []fleet.HostMDMAppleProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: fleet.PayloadScopeSystem}, {OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid2", Name: "Profile2", Status: &fleet.MDMDeliveryVerified, Scope: fleet.PayloadScopeUser, ManagedLocalAccount: "fleetie"}},
expectedProfiles: []fleet.HostMDMProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified, Scope: ptr.String("device"), ManagedLocalAccount: ptr.String("")}, {OperationType: fleet.MDMOperationTypeInstall, HostUUID: appleHost.UUID, ProfileUUID: "profile-uuid2", Name: "Profile2", Status: &fleet.MDMDeliveryVerified, Scope: ptr.String("user"), ManagedLocalAccount: ptr.String("fleetie")}},
},
}
windowsCases := []struct {
name string
storedProfiles []fleet.HostMDMWindowsProfile
expectedProfiles []fleet.HostMDMProfile
}{
{
name: "no profiles",
storedProfiles: nil,
expectedProfiles: nil,
},
// Windows does not support scopes or managed local accounts yet but we should not error and
// should set these to nil which is checked below
{
name: "example profile",
storedProfiles: []fleet.HostMDMWindowsProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: windowsHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified}},
expectedProfiles: []fleet.HostMDMProfile{{OperationType: fleet.MDMOperationTypeInstall, HostUUID: windowsHost.UUID, ProfileUUID: "profile-uuid1", Name: "Profile1", Status: &fleet.MDMDeliveryVerified}},
},
}
for _, tt := range appleCases {
t.Run(tt.name, func(t *testing.T) {
ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, host_uuid string) ([]fleet.HostMDMAppleProfile, error) {
return tt.storedProfiles, nil
}
h, err := svc.GetHost(ctx, appleHost.ID, fleet.HostDetailOptions{})
require.NoError(t, err)
if tt.storedProfiles == nil {
require.NotNil(t, h.MDM.Profiles)
require.Empty(t, *h.MDM.Profiles)
return
}
profs := *h.MDM.Profiles
require.Len(t, profs, len(tt.expectedProfiles))
for i := range profs {
require.Equal(t, tt.expectedProfiles[i].OperationType, profs[i].OperationType)
require.Equal(t, tt.expectedProfiles[i].HostUUID, profs[i].HostUUID)
require.Equal(t, tt.expectedProfiles[i].ProfileUUID, profs[i].ProfileUUID)
require.Equal(t, tt.expectedProfiles[i].Name, profs[i].Name)
require.Equal(t, tt.expectedProfiles[i].Status, profs[i].Status)
require.NotNil(t, profs[i].Scope)
require.Equal(t, *tt.expectedProfiles[i].Scope, *profs[i].Scope)
require.NotNil(t, profs[i].ManagedLocalAccount)
require.Equal(t, *tt.expectedProfiles[i].ManagedLocalAccount, *profs[i].ManagedLocalAccount)
}
})
}
for _, tt := range windowsCases {
t.Run(tt.name, func(t *testing.T) {
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, host_uuid string) ([]fleet.HostMDMWindowsProfile, error) {
return tt.storedProfiles, nil
}
h, err := svc.GetHost(ctx, windowsHost.ID, fleet.HostDetailOptions{})
require.NoError(t, err)
if tt.storedProfiles == nil {
require.NotNil(t, h.MDM.Profiles)
require.Empty(t, *h.MDM.Profiles)
return
}
profs := *h.MDM.Profiles
require.Len(t, profs, len(tt.expectedProfiles))
for i := range profs {
require.Equal(t, tt.expectedProfiles[i].OperationType, profs[i].OperationType)
require.Equal(t, tt.expectedProfiles[i].HostUUID, profs[i].HostUUID)
require.Equal(t, tt.expectedProfiles[i].ProfileUUID, profs[i].ProfileUUID)
require.Equal(t, tt.expectedProfiles[i].Name, profs[i].Name)
require.Equal(t, tt.expectedProfiles[i].Status, profs[i].Status)
require.Nil(t, profs[i].Scope)
require.Nil(t, profs[i].ManagedLocalAccount)
}
})
}
}
func TestLockUnlockWipeHostAuth(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
const (
teamHostID = 1
globalHostID = 2
)
teamHost := &fleet.Host{TeamID: ptr.Uint(1), Platform: "darwin"}
globalHost := &fleet.Host{Platform: "darwin"}
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
if identifier == fmt.Sprint(teamHostID) {
return teamHost, nil
}
return globalHost, nil
}
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
return nil
}
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
return nil, nil
}
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
return nil, nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820) ## Addresses full stack for #18554 - Add new `timezone` column to `calendar_events` table - When fetched from Google's API, save calendar user's timezone in this new column along with rest of event data - Implement datastore method to retrieve the start time and timezone for a host's next calendar event as a `HostMaintenanceWindow` - Localize and add UTC offset to the `HostMaintenanceWindow`'s start time according to its `timezone` - Include the processed `HostMaintenanceWindow`, if present, in the response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}` endpoints - Implement UI on the host details page to display this data - Add new and update existing UI, core integration, datastore, and `fleetctl` tests - Update `date-fns` package to the latest version <img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM" src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed"> # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified tables for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
return nil, nil
}
ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) {
return nil, nil
}
ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) {
return nil, nil
}
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
return nil, nil
}
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.LockHostViaScriptFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload, platform string) error {
return nil
}
// Some functions use Host, others HostLite. For our purposes either is fine
ds.HostFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
if hostID == teamHostID {
return teamHost, nil
}
return globalHost, nil
}
ds.HostLiteFunc = mock.HostLiteFunc(ds.HostFunc)
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
return nil, nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
return &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, platform string, ts time.Time) error {
return nil
}
ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
return true, nil
}
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
return nil, nil, nil
}
cases := []struct {
name string
user *fleet.User
shouldFailGlobalWrite bool
shouldFailTeamWrite bool
}{
{
name: "global observer",
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "team observer",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "global observer plus",
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "team observer plus",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "global admin",
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
shouldFailGlobalWrite: false,
shouldFailTeamWrite: false,
},
{
name: "team admin",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: false,
},
{
name: "global maintainer",
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
shouldFailGlobalWrite: false,
shouldFailTeamWrite: false,
},
{
name: "team maintainer",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: false,
},
{
name: "team admin wrong team",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 42}, Role: fleet.RoleAdmin}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "team maintainer wrong team",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 42}, Role: fleet.RoleMaintainer}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "global gitops",
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
{
name: "team gitops",
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
shouldFailGlobalWrite: true,
shouldFailTeamWrite: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true},
ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}, // scripts being disabled shouldn't stop lock/unlock/wipe
}, nil
}
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
_, err := svc.LockHost(ctx, globalHostID, false)
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
_, err = svc.LockHost(ctx, teamHostID, false)
checkAuthErr(t, tt.shouldFailTeamWrite, err)
// Pretend we locked the host
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{HostFleetPlatform: host.FleetPlatform(), LockMDMCommand: &fleet.MDMCommand{}, LockMDMCommandResult: &fleet.MDMCommandResult{Status: fleet.MDMAppleStatusAcknowledged}}, nil
}
_, err = svc.UnlockHost(ctx, globalHostID)
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
_, err = svc.UnlockHost(ctx, teamHostID)
checkAuthErr(t, tt.shouldFailTeamWrite, err)
// Reset so we're now pretending host is unlocked
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
err = svc.WipeHost(ctx, globalHostID, nil)
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
err = svc.WipeHost(ctx, teamHostID, nil)
checkAuthErr(t, tt.shouldFailTeamWrite, err)
})
}
}
func TestBulkOperationFilterValidation(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
viewerCtx := test.UserContext(ctx, test.UserAdmin)
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
return []*fleet.Host{}, nil
}
ds.ListHostsInLabelFunc = func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
return []*fleet.Host{}, nil
}
// TODO(sarah): Future improvement to auto-generate a list of all possible filter values
// from `fleet.HostListOptions` and iterate to test that only a limited subset of filter (i.e.
// label_id, team_id, status, query) are allowed for bulk operations.
tc := []struct {
name string
filters *map[string]interface{}
has400Err bool
}{
{
name: "valid status filter",
filters: &map[string]interface{}{
"status": "new",
},
},
{
name: "invalid status",
filters: &map[string]interface{}{
"status": "invalid",
},
has400Err: true,
},
{
name: "empty status is invalid",
filters: &map[string]interface{}{
"status": "",
},
has400Err: true,
},
{
name: "valid team filter",
filters: &map[string]interface{}{
"team_id": float64(1), // json unmarshals to float64
},
},
{
name: "invalid team_id type",
filters: &map[string]interface{}{
"team_id": "invalid",
},
has400Err: true,
},
{
name: "valid label_id filter",
filters: &map[string]interface{}{
"label_id": float64(1),
},
},
{
name: "invalid label_id type",
filters: &map[string]interface{}{
"label_id": "invalid",
},
has400Err: true,
},
{
name: "invalid status type",
filters: &map[string]interface{}{
"status": float64(1),
},
has400Err: true,
},
{
name: "empty filter",
filters: &map[string]interface{}{},
},
{
name: "valid query filter",
filters: &map[string]interface{}{
"query": "test",
},
},
{
name: "invalid query type",
filters: &map[string]interface{}{
"query": float64(1),
},
has400Err: true,
},
{
name: "empty query is invalid",
filters: &map[string]interface{}{
"query": "",
},
has400Err: true,
},
{
name: "multiple valid filters",
filters: &map[string]interface{}{
"status": "new",
"team_id": float64(1),
"query": "test",
},
},
{
name: "mixed valid and invalid filters",
filters: &map[string]interface{}{
"status": "new",
"team_id": "invalid",
},
has400Err: true,
},
{
name: "mixed invalid filters and valid filters (different order)",
filters: &map[string]interface{}{
"status": "invalid",
"team_id": 1,
},
has400Err: true,
},
{
name: "mixed valid and unknown filters",
filters: &map[string]interface{}{
"status": "new",
"unknown": "filter",
},
has400Err: true,
},
{
name: "unknown filter",
filters: &map[string]interface{}{
"unknown": "filter",
},
has400Err: true,
},
}
checkErr := func(t *testing.T, err error, has400Err bool) {
if has400Err {
require.Error(t, err)
var be *fleet.BadRequestError
require.ErrorAs(t, err, &be)
} else {
require.NoError(t, err)
}
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
checkErr(t, svc.AddHostsToTeamByFilter(viewerCtx, nil, tt.filters), tt.has400Err)
checkErr(t, svc.DeleteHosts(viewerCtx, nil, tt.filters), tt.has400Err)
})
}
}
func TestSetDiskEncryptionNotifications(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
svc := &Service{ds: ds, logger: kitlog.NewNopLogger()}
tests := []struct {
name string
host *fleet.Host
appConfig *fleet.AppConfig
diskEncryptionConfigured bool
isConnectedToFleetMDM bool
mdmInfo *fleet.HostMDM
getHostDiskEncryptionKey func(context.Context, uint) (*fleet.HostDiskEncryptionKey, error)
expectedNotifications *fleet.OrbitConfigNotifications
expectedError bool
disableCapability bool
}{
{
name: "no MDM configured",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: false},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "not connected to Fleet MDM",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: false,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "host not enrolled in osquery",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: nil},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "disk encryption not configured",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: false,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "darwin with decryptable key",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{Decryptable: ptr.Bool(true)}, nil
},
expectedNotifications: &fleet.OrbitConfigNotifications{
RotateDiskEncryptionKey: false,
},
expectedError: false,
},
{
name: "darwin needs rotation but client is old",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{Decryptable: ptr.Bool(false)}, nil
},
expectedNotifications: &fleet.OrbitConfigNotifications{
RotateDiskEncryptionKey: true,
},
expectedError: false,
disableCapability: true,
},
{
name: "darwin needs rotation",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{Decryptable: ptr.Bool(false)}, nil
},
expectedNotifications: &fleet.OrbitConfigNotifications{
RotateDiskEncryptionKey: true,
},
expectedError: false,
},
{
name: "windows server with no encryption needed",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(true), OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: true},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: false,
},
expectedError: false,
},
{
name: "windows with encryption enabled but key missing",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(true), OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: false},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: true,
},
expectedError: false,
},
{
name: "darwin with missing encryption key",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
RotateDiskEncryptionKey: false,
},
expectedError: false,
},
{
name: "windows with encryption key and not decryptable",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(true), OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: false},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{Decryptable: ptr.Bool(false)}, nil
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: true,
},
expectedError: false,
},
{
name: "windows with enforce BitLocker",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(false), OsqueryHostID: ptr.String("foo")},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: false},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: true,
},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.getHostDiskEncryptionKey != nil {
ds.GetHostDiskEncryptionKeyFunc = tt.getHostDiskEncryptionKey
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return tt.appConfig, nil
}
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
return &fleet.HostArchivedDiskEncryptionKey{}, nil
}
if !tt.disableCapability {
r := http.Request{
Header: http.Header{fleet.CapabilitiesHeader: []string{string(fleet.CapabilityEscrowBuddy)}},
}
ctx = capabilities.NewContext(ctx, &r)
}
notifs := &fleet.OrbitConfigNotifications{}
err := svc.setDiskEncryptionNotifications(
ctx,
notifs,
tt.host,
tt.appConfig,
tt.diskEncryptionConfigured,
tt.isConnectedToFleetMDM,
tt.mdmInfo,
)
if tt.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expectedNotifications.RotateDiskEncryptionKey, notifs.RotateDiskEncryptionKey)
})
}
}
func TestGetHostDetailsExcludeSoftwareFlag(t *testing.T) {
ds := new(mock.Store)
svc := &Service{ds: ds}
baseHost := &fleet.Host{ID: 42}
// common DS mocks
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 nil, nil
}
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
return nil, 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.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
return nil, nil
}
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
return false, nil
}
t.Run("ExcludeSoftware=true returns empty slice", func(t *testing.T) {
ds.LoadHostSoftwareFuncInvoked = false
ds.LoadHostSoftwareFunc = func(ctx context.Context, h *fleet.Host, includeCVEScores bool) error {
t.Fatalf("LoadHostSoftwareFunc should not be called when ExcludeSoftware is true")
return nil
}
opts := fleet.HostDetailOptions{ExcludeSoftware: true}
hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), baseHost, opts)
require.NoError(t, err)
require.NotNil(t, hostDetail.Software, "Software slice should not be nil")
assert.Len(t, hostDetail.Software, 0, "Software slice should be empty when excluded")
})
t.Run("ExcludeSoftware=false returns filled slice", func(t *testing.T) {
expectedSoftware := []fleet.HostSoftwareEntry{
{
Software: fleet.Software{
ID: 1,
Name: "test-app",
Version: "1.0.0",
Source: "apps",
},
InstalledPaths: []string{"/Applications/test-app.app"},
},
{
Software: fleet.Software{
ID: 2,
Name: "another-app",
Version: "2.3.4",
Source: "apps",
},
InstalledPaths: []string{"/Applications/another-app.app"},
},
}
ds.LoadHostSoftwareFuncInvoked = false
ds.LoadHostSoftwareFunc = func(ctx context.Context, h *fleet.Host, includeCVEScores bool) error {
h.HostSoftware.Software = expectedSoftware
return nil
}
opts := fleet.HostDetailOptions{ExcludeSoftware: false}
hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), baseHost, opts)
require.NoError(t, err)
require.NotNil(t, hostDetail.Software)
assert.Equal(t, expectedSoftware, hostDetail.Software)
assert.True(t, ds.LoadHostSoftwareFuncInvoked, "LoadHostSoftwareFunc should have been called")
})
}