48093 auld api gitops latest os version (#50213)

**Related issue:** Resolves #48093

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests

- [x] QA'd all new/changed functionality manually


## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [x] Verified that the setting is exported via `fleetctl
generate-gitops`

- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added “latest” version enforcement for macOS, iOS, and iPadOS updates
using required `deadline_days`.
* Updates dynamically target each device’s available OS version and
deadline.
  * Configuration and GitOps outputs now include `deadline_days`.

* **Bug Fixes**
* Improved validation when switching update modes or omitting deadline
settings.
* GitOps updates now clear previously stored deadline values when
omitted.
  * Changes to `deadline_days` are detected and applied consistently.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Magnus Jensen <magnus@fleetdm.com>
This commit is contained in:
Andrew Mellor
2026-08-05 12:37:11 +01:00
committed by GitHub
co-authored by Magnus Jensen
parent 5e95589554
commit 192ac4eb51
38 changed files with 1383 additions and 53 deletions
File diff suppressed because one or more lines are too long
+5
View File
@@ -869,20 +869,25 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) {
mdm, err := ds.TeamMDMConfig(ctx, team.ID)
require.NoError(t, err)
// The config round-trips through JSON, which always carries
// deadline_days, so it reads back set-but-null rather than unset.
assert.Equal(t, &fleet.TeamMDM{
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("10.15.0"),
Deadline: optjson.SetString("2025-10-01"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("11.11.11"),
Deadline: optjson.SetString("2024-04-04"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("12.12.12"),
Deadline: optjson.SetString("2023-03-03"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
WindowsUpdates: fleet.WindowsUpdates{
+41
View File
@@ -398,15 +398,56 @@ type AppleOSUpdateSettings struct {
// Deadline the required installation date for Nudge to enforce the required
// operating system version.
Deadline optjson.String `json:"deadline"`
// DeadlineDays is the number of days after an OS version's release date
// before the update is enforced. It is only valid when MinimumVersion is
// "latest", where the deadline is relative to each version's release rather
// than a fixed calendar date.
DeadlineDays optjson.Int `json:"deadline_days"`
}
// AppleOSUpdateLatestVersion is the sentinel MinimumVersion value meaning
// "enforce the newest version Apple offers for each host's hardware". The
// target version is resolved per host, and the deadline is derived from that
// version's release date plus DeadlineDays rather than being a fixed date.
const AppleOSUpdateLatestVersion = "latest"
// EnforcesLatestVersion returns whether these settings enforce the latest
// available OS version rather than a specific one.
func (m AppleOSUpdateSettings) EnforcesLatestVersion() bool {
return m.MinimumVersion.Value == AppleOSUpdateLatestVersion
}
// Configured returns a boolean indicating if updates are configured
func (m AppleOSUpdateSettings) Configured() bool {
if m.EnforcesLatestVersion() {
// In "latest" mode the deadline is relative to each version's release
// date, so DeadlineDays stands in for Deadline.
return m.DeadlineDays.Valid && m.DeadlineDays.Value > 0
}
return m.Deadline.Value != "" &&
m.MinimumVersion.Value != ""
}
func (m AppleOSUpdateSettings) Validate() error {
if m.EnforcesLatestVersion() {
if m.Deadline.Value != "" {
return errors.New(`deadline cannot be set when minimum_version is set to "latest". Use deadline_days instead`)
}
if !m.DeadlineDays.Valid {
return errors.New(`deadline_days is required when minimum_version is set to "latest"`)
}
if m.DeadlineDays.Value < 1 {
return errors.New("deadline_days must be greater than 0")
}
return nil
}
// DeadlineDays is meaningless without a version to resolve it against, so
// reject it for a specific version and when no version is provided at all.
if m.DeadlineDays.Valid {
return errors.New(`deadline_days can only be set when minimum_version is set to "latest". Use deadline instead`)
}
// if no settings are provided it's okay to skip further validation
if m.MinimumVersion.Value == "" && m.Deadline.Value == "" {
// if one is set and empty, the other must be set and empty too, otherwise
+129 -13
View File
@@ -119,6 +119,107 @@ func TestMacOSUpdatesValidate(t *testing.T) {
})
}
func TestAppleOSUpdatesLatestValidate(t *testing.T) {
t.Run("valid", func(t *testing.T) {
cases := []struct {
name string
m AppleOSUpdateSettings
}{
{
"latest with deadline_days",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
DeadlineDays: optjson.SetInt(14),
},
},
{
"latest with deadline_days of 1",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
DeadlineDays: optjson.SetInt(1),
},
},
{
"latest with explicitly empty deadline",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
Deadline: optjson.SetString(""),
DeadlineDays: optjson.SetInt(14),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.NoError(t, tc.m.Validate())
})
}
})
t.Run("invalid", func(t *testing.T) {
cases := []struct {
name string
m AppleOSUpdateSettings
}{
{
"latest without deadline_days",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
},
},
{
"latest with null deadline_days",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
DeadlineDays: optjson.Int{Set: true, Valid: false},
},
},
{
"latest with deadline",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
Deadline: optjson.SetString("2026-09-01"),
DeadlineDays: optjson.SetInt(14),
},
},
{
"latest with zero deadline_days",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
DeadlineDays: optjson.SetInt(0),
},
},
{
"latest with negative deadline_days",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("latest"),
DeadlineDays: optjson.SetInt(-1),
},
},
{
"specific version with deadline_days",
AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("15.1"),
Deadline: optjson.SetString("2026-09-01"),
DeadlineDays: optjson.SetInt(14),
},
},
{
"deadline_days with no version",
AppleOSUpdateSettings{
DeadlineDays: optjson.SetInt(14),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Error(t, tc.m.Validate())
})
}
})
}
func TestWindowsUpdatesValidate(t *testing.T) {
cases := []struct {
name string
@@ -174,24 +275,39 @@ func TestWindowsUpdatesEqual(t *testing.T) {
}
func TestMacOSUpdatesConfigured(t *testing.T) {
// nullDeadlineDays is what `"deadline_days": null` unmarshals to: the key was
// present but carried no value.
nullDeadlineDays := optjson.Int{Set: true, Valid: false}
cases := []struct {
version string
deadline string
out bool
name string
version string
deadline string
deadlineDays optjson.Int
out bool
}{
{"", "", false},
{"", "", false},
{"12.3", "", false},
{"", "12-03-2022", false},
{"12.3", "12-03-2022", true},
{"empty", "", "", optjson.Int{}, false},
{"version only", "12.3", "", optjson.Int{}, false},
{"deadline only", "", "12-03-2022", optjson.Int{}, false},
{"version and deadline", "12.3", "12-03-2022", optjson.Int{}, true},
// "latest" mode: DeadlineDays stands in for Deadline.
{"latest with deadline_days", AppleOSUpdateLatestVersion, "", optjson.SetInt(14), true},
{"latest without deadline_days", AppleOSUpdateLatestVersion, "", optjson.Int{}, false},
{"latest with null deadline_days", AppleOSUpdateLatestVersion, "", nullDeadlineDays, false},
{"latest with zero deadline_days", AppleOSUpdateLatestVersion, "", optjson.SetInt(0), false},
{"cleared", "", "", nullDeadlineDays, false},
}
for _, tc := range cases {
m := AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(tc.version),
Deadline: optjson.SetString(tc.deadline),
}
require.Equal(t, tc.out, m.Configured())
t.Run(tc.name, func(t *testing.T) {
m := AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(tc.version),
Deadline: optjson.SetString(tc.deadline),
DeadlineDays: tc.deadlineDays,
}
require.Equal(t, tc.out, m.Configured())
})
}
}
+9
View File
@@ -79,6 +79,15 @@ const (
FleetVarHostUUID FleetVarName = "HOST_UUID"
FleetVarHostPlatform FleetVarName = "HOST_PLATFORM"
// FleetVarHostTargetOSVersion and FleetVarHostTargetOSDeadline are
// Fleet-internal: they are only ever placed in Fleet's own OS-update
// declaration when the platform's minimum_version is "latest", and are
// resolved per host at declaration fetch time from host_mdm_apple_os_updates.
// They are deliberately absent from the lists of variables admins may use in
// their own profiles and declarations.
FleetVarHostTargetOSVersion FleetVarName = "HOST_TARGET_OS_VERSION"
FleetVarHostTargetOSDeadline FleetVarName = "HOST_TARGET_OS_DEADLINE"
// FleetVarPSSODeviceRegistrationToken is the admin-facing variable placed in
// the RegistrationToken key of a Fleet com.apple.extensiblesso (Platform SSO
// v2) payload. It resolves to the FLEET_HOST_SECRET_ placeholder of the same
+12 -4
View File
@@ -1901,7 +1901,15 @@ func ValidateMDMSettingsAppleSupportedOSVersion[T fleet.MDM | fleet.TeamMDM](set
return nil, errors.New("invalid settings type")
}
if macOSUpdates.MinimumVersion.Value == "" && iOSUpdates.MinimumVersion.Value == "" && iPadOSUpdates.MinimumVersion.Value == "" {
// "latest" is a sentinel, not a version: the concrete target is resolved per
// host from Apple's published versions later on, so there is nothing to look
// up here.
needsVersionCheck := func(s fleet.AppleOSUpdateSettings) bool {
return s.MinimumVersion.Value != "" && !s.EnforcesLatestVersion()
}
if !needsVersionCheck(macOSUpdates) && !needsVersionCheck(iOSUpdates) && !needsVersionCheck(iPadOSUpdates) {
// nothing to validate, so don't pay for the round trip to Apple.
return nil, nil
}
@@ -1914,12 +1922,12 @@ func ValidateMDMSettingsAppleSupportedOSVersion[T fleet.MDM | fleet.TeamMDM](set
}
invalid := make(map[string]string, 3)
if macOSUpdates.MinimumVersion.Value != "" {
if needsVersionCheck(macOSUpdates) {
if ok := am.IsSupportedMacOSVersion(macOSUpdates.MinimumVersion.Value, excludeNonPublicAssetSets); !ok {
invalid["macos"] = fleet.AppleOSVersionUnsupportedMessage
}
}
if iOSUpdates.MinimumVersion.Value != "" {
if needsVersionCheck(iOSUpdates) {
// NOTE: iPod generally falls in the category of iOS in Fleet, but we're only validating against iPhone here
// because we assume Apple will eventually remove iPod versions from the Apple Software Lookup Service
// and we want to avoid breaking workflows for users in that event
@@ -1927,7 +1935,7 @@ func ValidateMDMSettingsAppleSupportedOSVersion[T fleet.MDM | fleet.TeamMDM](set
invalid["ios"] = fleet.AppleOSVersionUnsupportedMessage
}
}
if iPadOSUpdates.MinimumVersion.Value != "" {
if needsVersionCheck(iPadOSUpdates) {
if ok := am.IsSupportedIOSVersion(iPadOSUpdates.MinimumVersion.Value, "ipad", excludeNonPublicAssetSets); !ok {
invalid["ipados"] = fleet.AppleOSVersionUnsupportedMessage
}
+72
View File
@@ -794,6 +794,68 @@ func TestValidateMDMSettingsAppleSupportedOSVersion(t *testing.T) {
})
})
t.Run("latest", func(t *testing.T) {
// "latest" is a sentinel resolved per host later, so it must never be
// looked up against Apple's published versions.
t.Run("accepted on every platform", func(t *testing.T) {
t.Run("app config mdm settings", func(t *testing.T) {
ac := mockAppConfigMDM()
ac.MacOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
ac.IOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
ac.IPadOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
got, err := ValidateMDMSettingsAppleSupportedOSVersion(ac, false)
require.NoError(t, err)
assert.Empty(t, got, "expect latest to be accepted when including non-public asset sets")
got, err = ValidateMDMSettingsAppleSupportedOSVersion(ac, true)
require.NoError(t, err)
assert.Empty(t, got, "expect latest to be accepted when excluding non-public asset sets")
})
t.Run("team mdm settings", func(t *testing.T) {
tm := mockTeamMDM()
tm.MacOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
tm.IOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
tm.IPadOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
got, err := ValidateMDMSettingsAppleSupportedOSVersion(tm, false)
require.NoError(t, err)
assert.Empty(t, got, "expect latest to be accepted when including non-public asset sets")
got, err = ValidateMDMSettingsAppleSupportedOSVersion(tm, true)
require.NoError(t, err)
assert.Empty(t, got, "expect latest to be accepted when excluding non-public asset sets")
})
})
t.Run("mixed with a real version still validates that version", func(t *testing.T) {
t.Run("app config mdm settings", func(t *testing.T) {
ac := mockAppConfigMDM()
ac.MacOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
// only supported for Apple Watch, so iOS should still be flagged
ac.IOSUpdates.MinimumVersion = optjson.SetString("5.3.9")
ac.IPadOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
got, err := ValidateMDMSettingsAppleSupportedOSVersion(ac, false)
require.NoError(t, err)
checkErr("ios", fleet.AppleOSVersionUnsupportedMessage, got, "expect only the concrete version to be validated")
})
t.Run("team mdm settings", func(t *testing.T) {
tm := mockTeamMDM()
tm.MacOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
// only supported for Apple Watch, so iOS should still be flagged
tm.IOSUpdates.MinimumVersion = optjson.SetString("5.3.9")
tm.IPadOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
got, err := ValidateMDMSettingsAppleSupportedOSVersion(tm, false)
require.NoError(t, err)
checkErr("ios", fleet.AppleOSVersionUnsupportedMessage, got, "expect only the concrete version to be validated")
})
})
})
// These subtests are placed last so that the dev_mode override cleanup for the error server
// doesn't interfere with the earlier subtests that rely on the valid mock server.
t.Run("GetAssetMetadata error", func(t *testing.T) {
@@ -815,6 +877,16 @@ func TestValidateMDMSettingsAppleSupportedOSVersion(t *testing.T) {
got, err = ValidateMDMSettingsAppleSupportedOSVersion(tm, false)
require.Error(t, err)
assert.Nil(t, got)
// With every platform set to "latest" there is nothing to look up, so the
// broken metadata endpoint must never be contacted.
ac = mockAppConfigMDM()
ac.MacOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
ac.IOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
ac.IPadOSUpdates.MinimumVersion = optjson.SetString(fleet.AppleOSUpdateLatestVersion)
got, err = ValidateMDMSettingsAppleSupportedOSVersion(ac, false)
require.NoError(t, err, "latest-only settings must not fetch Apple metadata")
assert.Nil(t, got)
})
}
+60 -5
View File
@@ -691,6 +691,10 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
appConfig.MDM.IOSUpdates.UpdateNewHosts = optjson.Bool{}
appConfig.MDM.IPadOSUpdates.UpdateNewHosts = optjson.Bool{}
clearStaleAppleOSUpdateDeadline(&appConfig.MDM.MacOSUpdates, newAppConfig.MDM.MacOSUpdates)
clearStaleAppleOSUpdateDeadline(&appConfig.MDM.IOSUpdates, newAppConfig.MDM.IOSUpdates)
clearStaleAppleOSUpdateDeadline(&appConfig.MDM.IPadOSUpdates, newAppConfig.MDM.IPadOSUpdates)
// Handle Google Calendar API key preservation/replacement.
// The custom GoogleCalendarApiKey type handles unmarshaling "********" as masked.
if newAppConfig.Integrations.GoogleCalendar != nil {
@@ -1833,7 +1837,10 @@ func (svc *Service) processAppleOSUpdateSettings(
newOSUpdateSettings fleet.AppleOSUpdateSettings,
) error {
if oldOSUpdateSettings.MinimumVersion.Value != newOSUpdateSettings.MinimumVersion.Value ||
oldOSUpdateSettings.Deadline.Value != newOSUpdateSettings.Deadline.Value {
oldOSUpdateSettings.Deadline.Value != newOSUpdateSettings.Deadline.Value ||
// Valid as well as Value: going from unset to 0, or 14 to unset, is a change.
oldOSUpdateSettings.DeadlineDays.Value != newOSUpdateSettings.DeadlineDays.Value ||
oldOSUpdateSettings.DeadlineDays.Valid != newOSUpdateSettings.DeadlineDays.Valid {
if lic.IsPremium() {
if err := svc.EnterpriseOverrides.MDMAppleEditedAppleOSUpdates(ctx, nil, appleDevice, newOSUpdateSettings); err != nil {
return ctxerr.Wrap(ctx, err, "update DDM profile after Apple OS updates change")
@@ -1956,6 +1963,33 @@ func diffStringSlices(old, current []string) (added, removed []string) {
return added, removed
}
// clearStaleAppleOSUpdateDeadline drops whichever deadline field belongs to the
// mode a PATCH is leaving. The two modes are mutually exclusive — "latest"
// derives its deadline from deadline_days, a specific version uses deadline —
// and Validate rejects the wrong one being present. Because the payload is
// merged over the stored config, a mode switch that doesn't mention the old
// field keeps it and fails validation, forcing callers to send an explicit null
// or empty string just to change modes.
//
// merged is the stored config with the payload already applied; incoming is the
// payload on its own, so its Set flags say what the caller actually sent. A
// value the caller supplied is left alone, so a genuine mismatch still fails
// validation with the error that explains it.
func clearStaleAppleOSUpdateDeadline(merged *fleet.AppleOSUpdateSettings, incoming fleet.AppleOSUpdateSettings) {
if merged.EnforcesLatestVersion() {
if !incoming.Deadline.Set {
// SetString("") rather than the zero value so this still marshals as
// "" — deadline has always been a string on the wire, and null would
// be a breaking change for API consumers.
merged.Deadline = optjson.SetString("")
}
return
}
if !incoming.DeadlineDays.Set {
merged.DeadlineDays = optjson.Int{}
}
}
func (svc *Service) validateMDM(
ctx context.Context,
lic *fleet.LicenseInfo,
@@ -2081,24 +2115,45 @@ func (svc *Service) validateMDM(
mdm.MacOSUpdates.MinimumVersion != oldMdm.MacOSUpdates.MinimumVersion
updatingMacOSDeadline := mdm.MacOSUpdates.Deadline.Value != "" &&
mdm.MacOSUpdates.Deadline != oldMdm.MacOSUpdates.Deadline
// deadline_days is the "latest" mode counterpart of deadline, so it has to
// gate on the license too: without it a lapsed-premium instance that already
// enforces "latest" could still edit the deadline.
updatingMacOSDeadlineDays := mdm.MacOSUpdates.DeadlineDays.Valid &&
mdm.MacOSUpdates.DeadlineDays != oldMdm.MacOSUpdates.DeadlineDays
// IOSUpdates
updatingIOSVersion := mdm.IOSUpdates.MinimumVersion.Value != "" &&
mdm.IOSUpdates.MinimumVersion != oldMdm.IOSUpdates.MinimumVersion
updatingIOSDeadline := mdm.IOSUpdates.Deadline.Value != "" &&
mdm.IOSUpdates.Deadline != oldMdm.IOSUpdates.Deadline
updatingIOSDeadlineDays := mdm.IOSUpdates.DeadlineDays.Valid &&
mdm.IOSUpdates.DeadlineDays != oldMdm.IOSUpdates.DeadlineDays
// IPadOSUpdates
updatingIPadOSVersion := mdm.IPadOSUpdates.MinimumVersion.Value != "" &&
mdm.IPadOSUpdates.MinimumVersion != oldMdm.IPadOSUpdates.MinimumVersion
updatingIPadOSDeadline := mdm.IPadOSUpdates.Deadline.Value != "" &&
mdm.IPadOSUpdates.Deadline != oldMdm.IPadOSUpdates.Deadline
updatingIPadOSDeadlineDays := mdm.IPadOSUpdates.DeadlineDays.Valid &&
mdm.IPadOSUpdates.DeadlineDays != oldMdm.IPadOSUpdates.DeadlineDays
if updatingMacOSVersion || updatingMacOSDeadline ||
updatingIOSVersion || updatingIOSDeadline ||
updatingIPadOSVersion || updatingIPadOSDeadline {
updatingMacOS := updatingMacOSVersion || updatingMacOSDeadline || updatingMacOSDeadlineDays
updatingIOS := updatingIOSVersion || updatingIOSDeadline || updatingIOSDeadlineDays
updatingIPadOS := updatingIPadOSVersion || updatingIPadOSDeadline || updatingIPadOSDeadlineDays
if updatingMacOS || updatingIOS || updatingIPadOS {
// TODO: Should we validate MDM configured on here too?
if !lic.IsPremium() {
invalid.Append("macos_updates.minimum_version", ErrMissingLicense.Error())
// The gate is shared by all three platforms, so a fixed field name
// would report macOS for an iOS-only edit.
field := "macos_updates.minimum_version"
switch {
case updatingMacOS:
case updatingIOS:
field = "ios_updates.minimum_version"
default:
field = "ipados_updates.minimum_version"
}
invalid.Append(field, ErrMissingLicense.Error())
return nil
}
}
+316 -3
View File
@@ -21,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
mdmtest "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
"github.com/fleetdm/fleet/v4/server/mock"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/ptr"
@@ -1219,9 +1220,9 @@ func TestMDMConfig(t *testing.T) {
Script: optjson.String{Set: true},
ManualAgentInstall: optjson.Bool{Set: true},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}, UpdateNewHosts: optjson.Bool{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}},
MacOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}, DeadlineDays: optjson.Int{Set: true}, UpdateNewHosts: optjson.Bool{Set: true}},
IOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}, DeadlineDays: optjson.Int{Set: true}},
IPadOSUpdates: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}, DeadlineDays: optjson.Int{Set: true}},
VolumePurchasingProgram: optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{Set: true, Value: []fleet.MDMAppleVolumePurchasingProgramInfo{}},
WindowsUpdates: fleet.WindowsUpdates{DeadlineDays: optjson.Int{Set: true}, GracePeriodDays: optjson.Int{Set: true}},
WindowsSettings: fleet.WindowsSettings{
@@ -1298,6 +1299,50 @@ func TestMDMConfig(t *testing.T) {
m.DeprecatedAppleBMDefaultTeam = "foobar"
}),
},
{
// A lapsed-premium instance can still have "latest" stored, so editing
// only deadline_days must hit the license gate like any other OS update
// change would.
name: "deadlineDaysFree",
licenseTier: "free",
oldMDM: fleet.MDM{MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(14),
}},
newMDM: fleet.MDM{MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(21),
}},
expectedError: "macos_updates.minimum_version " + licenseErr,
},
{
// The license gate is shared by the three Apple platforms, so the
// reported field has to follow the one that changed.
name: "deadlineDaysFreeIOS",
licenseTier: "free",
oldMDM: fleet.MDM{IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(14),
}},
newMDM: fleet.MDM{IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(21),
}},
expectedError: "ios_updates.minimum_version " + licenseErr,
},
{
name: "deadlineDaysFreeIPadOS",
licenseTier: "free",
oldMDM: fleet.MDM{IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(14),
}},
newMDM: fleet.MDM{IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(21),
}},
expectedError: "ipados_updates.minimum_version " + licenseErr,
},
{
name: "ssoFree",
licenseTier: "free",
@@ -1580,6 +1625,11 @@ func TestMDMConfig(t *testing.T) {
*dsAppConfig = *conf
return nil
}
// Reached whenever OS updates are configured, including "latest" mode,
// before the license gate runs.
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(context.Context, uint) (bool, error) {
return false, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if tt.findTeam {
return &fleet.Team{}, nil
@@ -1643,6 +1693,185 @@ func TestMDMConfig(t *testing.T) {
}
}
// A sparse PATCH that switches mode doesn't mention the outgoing mode's
// deadline field, so the merged config keeps the stale value and validation
// rejects it. Both directions are affected: "latest" rejects a deadline, a
// specific version rejects deadline_days. TestMDMConfig can't cover either: it
// builds payloads with json.Marshal of a whole fleet.MDM, and optjson emits an
// explicit null for every unset field, which clears the value on the way in.
func TestModifyAppConfigClearsStaleAppleOSUpdateDeadline(t *testing.T) {
admin := &fleet.User{GlobalRole: new(fleet.RoleAdmin)}
latest := fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: optjson.SetInt(14),
}
// 14.6.1 is a macOS version present in the GDMF fixture.
specific := fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2026-09-01"),
}
setup := func(t *testing.T, stored fleet.MDM) (fleet.Service, context.Context) {
// validateMDM checks minimum_version against GDMF unconditionally, so
// serve Apple's asset list from the local fixture. Without this the
// subtests reach out to Apple and start failing whenever a version stops
// being published.
mdmtest.StartNewAppleGDMFTestServer(t)
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
dsAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Test"},
ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
MDM: stored,
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return dsAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error {
*dsAppConfig = *conf
return nil
}
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(context.Context, uint) (bool, error) {
return false, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
// changing OS updates reconciles the reserved software-update
// declaration, so the write path has to be stubbed for the success cases
// to get past validation.
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, tmFilter fleet.TeamFilter) (map[string]uint, error) {
ids := make(map[string]uint, len(names))
for i, name := range names {
ids[name] = uint(i + 1) //nolint:gosec // G115: small test values
}
return ids, nil
}
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
return d, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
return svc, ctx
}
t.Run("macOS switching to a specific version", func(t *testing.T) {
svc, ctx := setup(t, fleet.MDM{MacOSUpdates: latest})
modified, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"macos_updates":{"minimum_version":"14.6.1","deadline":"2026-09-01"}}}`),
fleet.ApplySpecOptions{})
require.NoError(t, err)
require.Equal(t, "14.6.1", modified.MDM.MacOSUpdates.MinimumVersion.Value)
require.Equal(t, "2026-09-01", modified.MDM.MacOSUpdates.Deadline.Value)
require.False(t, modified.MDM.MacOSUpdates.DeadlineDays.Valid)
})
t.Run("switching into latest mode drops the stored deadline", func(t *testing.T) {
// the mirror case: "latest" derives its deadline from deadline_days, so a
// stored deadline is what's stale here.
svc, ctx := setup(t, fleet.MDM{MacOSUpdates: specific})
modified, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"macos_updates":{"minimum_version":"latest","deadline_days":14}}}`),
fleet.ApplySpecOptions{})
require.NoError(t, err)
require.Equal(t, fleet.AppleOSUpdateLatestVersion, modified.MDM.MacOSUpdates.MinimumVersion.Value)
require.Equal(t, 14, modified.MDM.MacOSUpdates.DeadlineDays.Value)
require.Empty(t, modified.MDM.MacOSUpdates.Deadline.Value)
// deadline has always serialized as a string, so the cleared value has to
// stay "" rather than becoming null.
raw, err := json.Marshal(modified.MDM.MacOSUpdates)
require.NoError(t, err)
require.Contains(t, string(raw), `"deadline":""`)
})
t.Run("an explicitly supplied deadline is still rejected in latest mode", func(t *testing.T) {
svc, ctx := setup(t, fleet.MDM{MacOSUpdates: specific})
_, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"macos_updates":{"minimum_version":"latest","deadline":"2026-09-01","deadline_days":14}}}`),
fleet.ApplySpecOptions{})
require.Error(t, err)
require.ErrorContains(t, err, `deadline cannot be set when minimum_version is set to "latest"`)
})
t.Run("clearing enforcement entirely", func(t *testing.T) {
// turning enforcement off also leaves "latest" mode, so the stored
// deadline_days must not block it either.
svc, ctx := setup(t, fleet.MDM{MacOSUpdates: latest})
modified, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"macos_updates":{"minimum_version":"","deadline":""}}}`),
fleet.ApplySpecOptions{})
require.NoError(t, err)
require.Empty(t, modified.MDM.MacOSUpdates.MinimumVersion.Value)
require.False(t, modified.MDM.MacOSUpdates.DeadlineDays.Valid)
})
// the clearing is wired up per platform, so cover the other two. They clear
// enforcement rather than set a version to keep Apple's supported-version
// list out of it.
t.Run("iOS clearing enforcement", func(t *testing.T) {
svc, ctx := setup(t, fleet.MDM{IOSUpdates: latest})
modified, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"ios_updates":{"minimum_version":"","deadline":""}}}`),
fleet.ApplySpecOptions{})
require.NoError(t, err)
require.False(t, modified.MDM.IOSUpdates.DeadlineDays.Valid)
})
t.Run("iPadOS clearing enforcement", func(t *testing.T) {
svc, ctx := setup(t, fleet.MDM{IPadOSUpdates: latest})
modified, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"ipados_updates":{"minimum_version":"","deadline":""}}}`),
fleet.ApplySpecOptions{})
require.NoError(t, err)
require.False(t, modified.MDM.IPadOSUpdates.DeadlineDays.Valid)
})
t.Run("an explicitly supplied deadline_days is still rejected", func(t *testing.T) {
// the caller sent it, so this is a real mistake and has to keep failing
// with the error that explains the constraint.
svc, ctx := setup(t, fleet.MDM{MacOSUpdates: latest})
_, err := svc.ModifyAppConfig(ctx,
[]byte(`{"mdm":{"macos_updates":{"minimum_version":"14.6.1","deadline":"2026-09-01","deadline_days":14}}}`),
fleet.ApplySpecOptions{})
require.Error(t, err)
require.ErrorContains(t, err, `deadline_days can only be set when minimum_version is set to "latest"`)
})
t.Run("latest mode is untouched when the payload omits the platform", func(t *testing.T) {
svc, ctx := setup(t, fleet.MDM{MacOSUpdates: latest})
modified, err := svc.ModifyAppConfig(ctx,
[]byte(`{"org_info":{"org_name":"Renamed"}}`),
fleet.ApplySpecOptions{})
require.NoError(t, err)
require.Equal(t, fleet.AppleOSUpdateLatestVersion, modified.MDM.MacOSUpdates.MinimumVersion.Value)
require.Equal(t, 14, modified.MDM.MacOSUpdates.DeadlineDays.Value)
})
}
func TestModifyAppConfigWindowsEntraClientIDNormalization(t *testing.T) {
ds := new(mock.Store)
admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
@@ -3195,6 +3424,90 @@ func TestModifyAppConfigManagedLocalAccount(t *testing.T) {
}
}
func TestProcessAppleOSUpdateSettingsDeadlineDays(t *testing.T) {
ctx := context.Background()
lic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
// sentinel is returned by the override so the change is observable without
// standing up the activity service: reaching the override means the settings
// were considered changed.
sentinel := errors.New("override invoked")
newSvc := func(called *bool) *Service {
svc := &Service{ds: new(mock.Store)}
svc.SetEnterpriseOverrides(fleet.EnterpriseOverrides{
MDMAppleEditedAppleOSUpdates: func(ctx context.Context, teamID *uint, appleDevice fleet.AppleDevice,
updates fleet.AppleOSUpdateSettings,
) error {
*called = true
return sentinel
},
})
return svc
}
latest := func(days optjson.Int) fleet.AppleOSUpdateSettings {
return fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString(fleet.AppleOSUpdateLatestVersion),
DeadlineDays: days,
}
}
cases := []struct {
name string
old fleet.AppleOSUpdateSettings
new fleet.AppleOSUpdateSettings
wantUpdated bool
}{
{
name: "deadline_days changed",
old: latest(optjson.SetInt(14)),
new: latest(optjson.SetInt(7)),
wantUpdated: true,
},
{
name: "deadline_days set from unset",
old: latest(optjson.Int{}),
new: latest(optjson.SetInt(14)),
wantUpdated: true,
},
{
name: "deadline_days cleared to null",
old: latest(optjson.SetInt(14)),
new: latest(optjson.Int{Set: true, Valid: false}),
wantUpdated: true,
},
{
name: "nothing changed",
old: latest(optjson.SetInt(14)),
new: latest(optjson.SetInt(14)),
wantUpdated: false,
},
{
name: "minimum_version changed",
old: latest(optjson.SetInt(14)),
new: fleet.AppleOSUpdateSettings{MinimumVersion: optjson.SetString("15.7.8"), Deadline: optjson.SetString("2026-09-01")},
wantUpdated: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var called bool
svc := newSvc(&called)
err := svc.processAppleOSUpdateSettings(ctx, lic, fleet.MacOS, tc.old, tc.new)
if tc.wantUpdated {
require.ErrorIs(t, err, sentinel, "expected the OS updates change to be detected")
require.True(t, called)
} else {
require.NoError(t, err)
require.False(t, called, "expected no update for unchanged settings")
}
})
}
}
func TestModifyAppConfigWindowsEnrollment(t *testing.T) {
admin := &fleet.User{GlobalRole: new(fleet.RoleAdmin)}
teamID := uint(7)
+25
View File
@@ -8666,6 +8666,31 @@ func TestValidateDeclarationFleetVariables(t *testing.T) {
require.Error(t, err)
require.ErrorContains(t, err, "Fleet variable $FLEET_VAR_DIGICERT_DATA_myCA is not supported in DDM profiles")
})
// The OS update target variables are Fleet-internal: they are placed only in
// Fleet's own OS update declaration and resolved per host. An admin must not
// be able to reference them in a declaration of their own, so they are
// deliberately absent from fleetVarsSupportedInDDMDeclarations. Adding them
// there would silently break that.
t.Run("Fleet-internal OS update variables are rejected", func(t *testing.T) {
for _, v := range []fleet.FleetVarName{
fleet.FleetVarHostTargetOSVersion,
fleet.FleetVarHostTargetOSDeadline,
} {
// Both reference forms, since Fleet's own declaration uses each of them.
for form, value := range map[string]string{
"bare": fmt.Sprintf("$FLEET_VAR_%s", v),
"braces": fmt.Sprintf("${FLEET_VAR_%s}", v),
} {
t.Run(string(v)+"/"+form, func(t *testing.T) {
_, err := validateDeclarationFleetVariables(makeDecl(value), premiumLic)
require.Error(t, err)
require.ErrorContains(t, err,
fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in DDM profiles", v))
})
}
}
})
}
func TestJSONEscapeString(t *testing.T) {
+19 -1
View File
@@ -2666,6 +2666,12 @@ func (c *Client) DoGitOps(
if deadline, ok := macOSUpdates["deadline"]; !ok || deadline == nil {
macOSUpdates["deadline"] = ""
}
// Send an explicit null when the file omits deadline_days, otherwise the
// PATCH would leave a previously stored value in place and the YAML would
// stop being the source of truth.
if _, ok := macOSUpdates["deadline_days"]; !ok {
macOSUpdates["deadline_days"] = nil
}
// When update_new_hosts isn't explicitly set, derive it from whether OS updates
// are configured: default to true when both minimum_version and deadline are set
@@ -2673,7 +2679,13 @@ func (c *Client) DoGitOps(
// updates aren't configured prevents a previously stored "true" from sticking
// around once minimum_version/deadline are cleared.
if macOSUpdates["update_new_hosts"] == nil {
macOSUpdates["update_new_hosts"] = macOSUpdates["minimum_version"] != "" && macOSUpdates["deadline"] != ""
// "latest" mode has no deadline — deadline_days replaces it — so the
// deadline check alone would read as "not configured" and silently
// leave new hosts unenforced.
enforcingLatest := macOSUpdates["minimum_version"] == fleet.AppleOSUpdateLatestVersion &&
macOSUpdates["deadline_days"] != nil
macOSUpdates["update_new_hosts"] = enforcingLatest ||
(macOSUpdates["minimum_version"] != "" && macOSUpdates["deadline"] != "")
}
// Put in default values for ios_updates
@@ -2689,6 +2701,9 @@ func (c *Client) DoGitOps(
if deadline, ok := iOSUpdates["deadline"]; !ok || deadline == nil {
iOSUpdates["deadline"] = ""
}
if _, ok := iOSUpdates["deadline_days"]; !ok {
iOSUpdates["deadline_days"] = nil
}
// update_new_hosts is only used for macOS so ignore any values posted for iOS
iOSUpdates["update_new_hosts"] = nil
@@ -2705,6 +2720,9 @@ func (c *Client) DoGitOps(
if deadline, ok := iPadOSUpdates["deadline"]; !ok || deadline == nil {
iPadOSUpdates["deadline"] = ""
}
if _, ok := iPadOSUpdates["deadline_days"]; !ok {
iPadOSUpdates["deadline_days"] = nil
}
// update_new_hosts is only used for macOS so ignore any values posted for iPadOS
iPadOSUpdates["update_new_hosts"] = nil
+16 -1
View File
@@ -317,16 +317,19 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2021-01-01"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.SetBool(true),
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-07-23"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-08-24"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
WindowsUpdates: fleet.WindowsUpdates{
@@ -450,16 +453,19 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2021-01-01"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.SetBool(true),
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-07-23"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-08-24"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
WindowsUpdates: fleet.WindowsUpdates{
@@ -491,16 +497,19 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2021-01-01"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.SetBool(true),
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-07-23"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-08-24"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
WindowsUpdates: fleet.WindowsUpdates{
@@ -534,16 +543,19 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2021-01-01"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.SetBool(true),
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-07-23"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("2024-08-24"),
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
WindowsUpdates: fleet.WindowsUpdates{
@@ -3509,16 +3521,19 @@ func (s *integrationEnterpriseTestSuite) TestWindowsUpdatesTeamConfig() {
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.String{Set: true},
Deadline: optjson.String{Set: true},
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.String{Set: true},
Deadline: optjson.String{Set: true},
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.String{Set: true},
Deadline: optjson.String{Set: true},
DeadlineDays: optjson.Int{Set: true},
UpdateNewHosts: optjson.Bool{Set: true, Valid: false, Value: false},
},
WindowsUpdates: fleet.WindowsUpdates{
@@ -5006,7 +5021,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMAppleOSUpdates() {
// get the appconfig, nothing changed
acResp = appConfigResponse{}
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
require.Equal(t, fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}, UpdateNewHosts: optjson.SetBool(false)}, acResp.MDM.MacOSUpdates)
require.Equal(t, fleet.AppleOSUpdateSettings{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}, DeadlineDays: optjson.Int{Set: true}, UpdateNewHosts: optjson.SetBool(false)}, acResp.MDM.MacOSUpdates)
// no activity got created
activitiesResp = listActivitiesResponse{}