Don't create a nudge config if macos is above version 14 (#18020)

#17418
This commit is contained in:
Dante Catalfamo 2024-04-05 10:11:49 -04:00 committed by GitHub
parent 5c7783eca0
commit fc4557746e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 298 additions and 9 deletions

View file

@ -0,0 +1 @@
* macOS 14 and higher no longer display nudge notifications

View file

@ -45,6 +45,10 @@ func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint,
})
}
func (ds *Datastore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
return getHostOperatingSystemDB(ctx, ds.reader(ctx), hostID)
}
// getOrGenerateOperatingSystemDB queries the `operating_systems` table with the
// name, version, arch, and kernel_version of the given operating system. If found,
// it returns the record including the associated ID. If not found, it returns a call
@ -168,11 +172,11 @@ func getIDHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID
// getIDHostOperatingSystemDB queries the `operating_systems` table and returns the
// operating system record associated with the given host ID based on a subquery
// of the `host_operating_system` table.
func getHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint) (*fleet.OperatingSystem, error) {
func getHostOperatingSystemDB(ctx context.Context, tx sqlx.QueryerContext, hostID uint) (*fleet.OperatingSystem, error) {
var os fleet.OperatingSystem
stmt := "SELECT id, name, version, arch, kernel_version, platform, display_version, os_version_id FROM operating_systems WHERE id = (SELECT os_id FROM host_operating_system WHERE host_id = ?)"
if err := sqlx.GetContext(ctx, tx, &os, stmt, hostID); err != nil {
return nil, err
return nil, ctxerr.Wrap(ctx, err, "getting host os")
}
return &os, nil
}

View file

@ -3,6 +3,7 @@ package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"sync"
"testing"
@ -295,6 +296,9 @@ func TestGetHostOperatingSystem(t *testing.T) {
_, err = getHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID)
require.ErrorIs(t, err, sql.ErrNoRows)
_, err = ds.GetHostOperatingSystem(ctx, testHostID)
require.ErrorIs(t, err, sql.ErrNoRows)
// insert test host and os id
err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[0].ID)
require.NoError(t, err)
@ -302,6 +306,10 @@ func TestGetHostOperatingSystem(t *testing.T) {
require.NoError(t, err)
require.Equal(t, osList[0], *os)
os, err = ds.GetHostOperatingSystem(ctx, testHostID)
require.NoError(t, err)
require.Equal(t, osList[0], *os)
// update test host with new os id
err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[1].ID)
require.NoError(t, err)
@ -309,12 +317,20 @@ func TestGetHostOperatingSystem(t *testing.T) {
require.NoError(t, err)
require.Equal(t, osList[1], *os)
os, err = ds.GetHostOperatingSystem(ctx, testHostID)
require.NoError(t, err)
require.Equal(t, osList[1], *os)
// no change
err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[1].ID)
require.NoError(t, err)
os, err = getHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID)
require.NoError(t, err)
require.Equal(t, osList[1], *os)
os, err = ds.GetHostOperatingSystem(ctx, testHostID)
require.NoError(t, err)
require.Equal(t, osList[1], *os)
}
func TestCleanupHostOperatingSystems(t *testing.T) {
@ -352,7 +368,7 @@ func TestCleanupHostOperatingSystems(t *testing.T) {
assertDeletedHostOS := func(expectDeletedIDs []uint) {
for _, h := range testHosts {
os, err := getHostOperatingSystemDB(ctx, ds.writer(ctx), h.ID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
require.Contains(t, expectDeletedIDs, h.ID)
return
}

View file

@ -530,6 +530,9 @@ type Datastore interface {
///////////////////////////////////////////////////////////////////////////////
// OperatingSystemsStore
// GetHostOperatingSystem returns the operating system information
// for a given host.
GetHostOperatingSystem(ctx context.Context, hostID uint) (*OperatingSystem, error)
// ListOperationsSystems returns all operating systems (id, name, version)
ListOperatingSystems(ctx context.Context) ([]OperatingSystem, error)
// ListOperatingSystemsForPlatform returns all operating systems for the given platform.

View file

@ -1,6 +1,10 @@
package fleet
import "strings"
import (
"fmt"
"strconv"
"strings"
)
// OperatingSystem is an operating system uniquely identified according to its name and version.
type OperatingSystem struct {
@ -27,3 +31,23 @@ type OperatingSystem struct {
func (os OperatingSystem) IsWindows() bool {
return strings.ToLower(os.Platform) == "windows"
}
// RequiresNudge returns whether the target platform is darwin and
// below version 14. Starting at macOS 14 nudge is no longer required,
// as the mechanism to notify users about updates is built in.
func (os *OperatingSystem) RequiresNudge() (bool, error) {
if os.Platform != "darwin" {
return false, nil
}
versionFloat, err := strconv.ParseFloat(os.Version, 32)
if err != nil {
return false, fmt.Errorf("parsing macos version \"%s\": %w", os.Version, err)
}
if float32(versionFloat) < 14 {
return true, nil
}
return false, nil
}

View file

@ -21,3 +21,35 @@ func TestOperatingSystemIsWindows(t *testing.T) {
require.Equal(t, tc.isWindows, sut.IsWindows())
}
}
func TestOperatingSystemRequiresNudge(t *testing.T) {
testCases := []struct {
platform string
version string
requiresNudge bool
parseError bool
}{
{platform: "chrome"},
{platform: "chrome", version: "12.1"},
{platform: "chrome", version: "15"},
{platform: "darwin", parseError: true},
{platform: "darwin", version: "12.0", requiresNudge: true},
{platform: "darwin", version: "11", requiresNudge: true},
{platform: "darwin", version: "14.0"},
{platform: "darwin", version: "14.3"},
{platform: "windows"},
{platform: "windows", version: "12.2"},
{platform: "windows", version: "15.4"},
}
for _, tc := range testCases {
os := OperatingSystem{Platform: tc.platform, Version: tc.version}
req, err := os.RequiresNudge()
if tc.parseError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.requiresNudge, req)
}
}

View file

@ -392,6 +392,8 @@ type InsertCVEMetaFunc func(ctx context.Context, cveMeta []fleet.CVEMeta) error
type ListCVEsFunc func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error)
type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error)
type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error)
type ListOperatingSystemsForPlatformFunc func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error)
@ -1460,6 +1462,9 @@ type DataStore struct {
ListCVEsFunc ListCVEsFunc
ListCVEsFuncInvoked bool
GetHostOperatingSystemFunc GetHostOperatingSystemFunc
GetHostOperatingSystemFuncInvoked bool
ListOperatingSystemsFunc ListOperatingSystemsFunc
ListOperatingSystemsFuncInvoked bool
@ -3531,6 +3536,13 @@ func (s *DataStore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]fleet
return s.ListCVEsFunc(ctx, maxAge)
}
func (s *DataStore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
s.mu.Lock()
s.GetHostOperatingSystemFuncInvoked = true
s.mu.Unlock()
return s.GetHostOperatingSystemFunc(ctx, hostID)
}
func (s *DataStore) ListOperatingSystems(ctx context.Context) ([]fleet.OperatingSystem, error) {
s.mu.Lock()
s.ListOperatingSystemsFuncInvoked = true

View file

@ -7374,6 +7374,10 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() {
// nudge config is empty if macos_updates is not set, and Windows MDM notifications are unset
h := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
err := s.ds.UpdateHostOperatingSystem(context.Background(), h.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"})
require.NoError(t, err)
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
require.Empty(t, resp.NudgeConfig)
@ -7402,7 +7406,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() {
})
mdmDevice.SerialNumber = h.HardwareSerial
mdmDevice.UUID = h.UUID
err := mdmDevice.Enroll()
err = mdmDevice.Enroll()
require.NoError(t, err)
resp = orbitGetConfigResponse{}
@ -7459,12 +7463,54 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() {
mdmDevice.UUID = h2.UUID
err = mdmDevice.Enroll()
require.NoError(t, err)
err = s.ds.UpdateHostOperatingSystem(context.Background(), h2.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"})
require.NoError(t, err)
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h2.OrbitNodeKey)), http.StatusOK, &resp)
wantCfg, err = fleet.NewNudgeConfig(fleet.MacOSUpdates{Deadline: optjson.SetString("2022-01-04"), MinimumVersion: optjson.SetString("12.1.3")})
require.NoError(t, err)
require.Equal(t, wantCfg, resp.NudgeConfig)
require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC")
// host on macos > 14, shouldn't be receiving nudge configs
h3 := createOrbitEnrolledHost(t, "darwin", "h3", s.ds)
mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
MDMURL: s.server.URL + apple_mdm.MDMPath,
})
mdmDevice.SerialNumber = h3.HardwareSerial
mdmDevice.UUID = h3.UUID
err = mdmDevice.Enroll()
require.NoError(t, err)
err = s.ds.UpdateHostOperatingSystem(context.Background(), h3.ID, fleet.OperatingSystem{Platform: "darwin", Version: "14.1"})
require.NoError(t, err)
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h3.OrbitNodeKey)), http.StatusOK, &resp)
require.Nil(t, resp.NudgeConfig)
// host is available for nudge, but has not had details query run
// yet, so we don't know the os version.
h4 := createOrbitEnrolledHost(t, "darwin", "h4", s.ds)
mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
MDMURL: s.server.URL + apple_mdm.MDMPath,
})
mdmDevice.SerialNumber = h4.HardwareSerial
mdmDevice.UUID = h4.UUID
err = mdmDevice.Enroll()
require.NoError(t, err)
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h4.OrbitNodeKey)), http.StatusOK, &resp)
require.Nil(t, resp.NudgeConfig)
}
func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() {
@ -8935,7 +8981,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
stmt := `
SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, COALESCE(label_id, 0) as label_id
FROM mdm_configuration_profile_labels
FROM mdm_configuration_profile_labels
UNION SELECT apple_declaration_uuid as profile_uuid, label_name, COALESCE(label_id, 0) as label_id
FROM mdm_declaration_labels ORDER BY profile_uuid, label_name;`
return sqlx.SelectContext(context.Background(), q, &profileLabels, stmt)

View file

@ -2,6 +2,7 @@ package service
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
@ -268,10 +269,24 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
if appConfig.MDM.EnabledAndConfigured &&
mdmConfig != nil &&
mdmConfig.MacOSUpdates.EnabledForHost(host) {
nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates)
hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID)
if errors.Is(err, sql.ErrNoRows) {
// host os has not been collected yet (no details query)
hostOS = &fleet.OperatingSystem{}
} else if err != nil {
return fleet.OrbitConfig{}, err
}
requiresNudge, err := hostOS.RequiresNudge()
if err != nil {
return fleet.OrbitConfig{}, err
}
if requiresNudge {
nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates)
if err != nil {
return fleet.OrbitConfig{}, err
}
}
}
if mdmConfig.EnableDiskEncryption &&
@ -313,10 +328,25 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
var nudgeConfig *fleet.NudgeConfig
if appConfig.MDM.EnabledAndConfigured &&
appConfig.MDM.MacOSUpdates.EnabledForHost(host) {
nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates)
hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID)
if errors.Is(err, sql.ErrNoRows) {
// host os has not been collected yet (no details query)
hostOS = &fleet.OperatingSystem{}
} else if err != nil {
return fleet.OrbitConfig{}, err
}
requiresNudge, err := hostOS.RequiresNudge()
if err != nil {
return fleet.OrbitConfig{}, err
}
if requiresNudge {
nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates)
if err != nil {
return fleet.OrbitConfig{}, err
}
}
}
if appConfig.MDM.WindowsEnabledAndConfigured &&

View file

@ -22,11 +22,19 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return appCfg, nil
}
os := &fleet.OperatingSystem{
Platform: "darwin",
Version: "12.2",
}
ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
return os, nil
}
ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) {
return nil, nil
}
ctx = test.HostContext(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test"),
ID: 1,
MDMInfo: &fleet.HostMDM{
IsServer: false,
InstalledFromDep: true,
@ -65,7 +73,13 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return appCfg, nil
}
os := &fleet.OperatingSystem{
Platform: "darwin",
Version: "12.2",
}
ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
return os, nil
}
team := fleet.Team{ID: 1}
teamMDM := fleet.TeamMDM{}
ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
@ -81,6 +95,7 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ctx = test.HostContext(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test"),
ID: 1,
TeamID: ptr.Uint(team.ID),
MDMInfo: &fleet.HostMDM{
IsServer: false,
@ -120,6 +135,13 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
os := &fleet.OperatingSystem{
Platform: "darwin",
Version: "12.2",
}
ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
return os, nil
}
appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01")
appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("2022-04-01")
@ -193,4 +215,103 @@ func TestGetOrbitConfigNudge(t *testing.T) {
}})
})
t.Run("no-nudge on macos versions greater than 14", func(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
os := &fleet.OperatingSystem{
Platform: "darwin",
Version: "12.2",
}
host := &fleet.Host{
OsqueryHostID: ptr.String("test"),
ID: 1,
MDMInfo: &fleet.HostMDM{
IsServer: false,
InstalledFromDep: true,
Enrolled: true,
Name: fleet.WellKnownMDMFleet,
}}
team := fleet.Team{ID: 1}
teamMDM := fleet.TeamMDM{}
teamMDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01")
teamMDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.1")
ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
require.Equal(t, team.ID, teamID)
return &teamMDM, nil
}
ds.TeamAgentOptionsFunc = func(ctx context.Context, id uint) (*json.RawMessage, error) {
return ptr.RawMessage(json.RawMessage(`{}`)), nil
}
ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) {
return nil, nil
}
appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01")
appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.3")
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return appCfg, nil
}
ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) {
return nil, nil
}
ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
return os, nil
}
ctx = test.HostContext(ctx, host)
// Version < 14 gets nudge
host.ID = 1
cfg, err := svc.GetOrbitConfig(ctx)
require.NoError(t, err)
require.NotEmpty(t, cfg.NudgeConfig)
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
// Version > 14 gets no nudge
os.Version = "14.1"
ds.GetHostOperatingSystemFuncInvoked = false
cfg, err = svc.GetOrbitConfig(ctx)
require.NoError(t, err)
require.Empty(t, cfg.NudgeConfig)
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
// windows gets no nudge
os.Platform = "windows"
ds.GetHostOperatingSystemFuncInvoked = false
cfg, err = svc.GetOrbitConfig(ctx)
require.NoError(t, err)
require.Empty(t, cfg.NudgeConfig)
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
//// team section below
host.TeamID = ptr.Uint(team.ID)
os.Platform = "darwin"
os.Version = "12.1"
// Version < 14 gets nudge
host.ID = 1
cfg, err = svc.GetOrbitConfig(ctx)
require.NoError(t, err)
require.NotEmpty(t, cfg.NudgeConfig)
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
// Version > 14 gets no nudge
os.Version = "14.1"
ds.GetHostOperatingSystemFuncInvoked = false
cfg, err = svc.GetOrbitConfig(ctx)
require.NoError(t, err)
require.Empty(t, cfg.NudgeConfig)
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
// windows gets no nudge
os.Platform = "windows"
ds.GetHostOperatingSystemFuncInvoked = false
cfg, err = svc.GetOrbitConfig(ctx)
require.NoError(t, err)
require.Empty(t, cfg.NudgeConfig)
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
})
}