- Remotely encourage the installation of software updates on hosts
- assigned to this team.
+ Remotely enforce software updates.
diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx
index 7381dd9868..6f9955606a 100644
--- a/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx
+++ b/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx
@@ -3,7 +3,6 @@ import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
import TabNav from "components/TabNav";
import TabText from "components/TabText";
import CustomLink from "components/CustomLink";
-import { SUPPORT_LINK } from "utilities/constants";
import EndUserOSRequirementPreview from "../EndUserOSRequirementPreview";
import WindowsTargetForm from "../WindowsTargetForm";
@@ -162,11 +161,12 @@ const PlatformTabs = ({
- Android updates are coming soon.
-
-
- Need to encourage installation of Android updates?{" "}
-
+ Add a{" "}
+ {" "}
+ (configuration profile) to enforce Android OS updates.
diff --git a/server/fleet/android.go b/server/fleet/android.go
index b949a1aa2a..4f779b1573 100644
--- a/server/fleet/android.go
+++ b/server/fleet/android.go
@@ -44,7 +44,6 @@ var AndroidForbiddenJSONKeys = map[string]string{
"uninstallAppsDisabled": `Android configuration profile can't include "uninstallAppsDisabled" setting. Software management is coming soon.`,
"blockApplicationsEnabled": `Android configuration profile can't include "blockApplicationsEnabled" setting. Software management is coming soon.`,
"appAutoUpdatePolicy": `Android configuration profile can't include "appAutoUpdatePolicy" setting. Software management is coming soon.`,
- "systemUpdate": `Android configuration profile can't include "systemUpdate" setting. OS updates are coming soon.`,
"kioskCustomLauncherEnabled": `Android configuration profile can't include "kioskCustomLauncherEnabled" setting. Currently, only personal hosts are supported.`,
"kioskCustomization": `Android configuration profile can't include "kioskCustomization" setting. Currently, only personal hosts are supported.`,
"persistentPreferredActivities": `Android configuration profile can't include "persistentPreferredActivities" setting. Currently, only personal hosts are supported.`,
@@ -52,7 +51,13 @@ var AndroidForbiddenJSONKeys = map[string]string{
"encryptionPolicy": `Android configuration profile can't include "encryptionPolicy" setting. Currently, disk encryption isn't supported.`,
}
-func (m *MDMAndroidConfigProfile) ValidateUserProvided() error {
+// AndroidPremiumOnlyJSONKeys are keys that may not be included in user-provided Android
+// configuration profiles for non-Premium licenses and associated error messages when they are included
+var AndroidPremiumOnlyJSONKeys = map[string]string{
+ "systemUpdate": `Android OS updates ("systemUpdate") is Fleet Premium only.`,
+}
+
+func (m *MDMAndroidConfigProfile) ValidateUserProvided(isPremium bool) error {
if len(bytes.TrimSpace(m.RawJSON)) == 0 {
return errors.New("The file should include valid JSON.")
}
@@ -75,6 +80,12 @@ func (m *MDMAndroidConfigProfile) ValidateUserProvided() error {
return errors.New(errMsg)
}
+ if !isPremium {
+ if errMsg, ok := AndroidPremiumOnlyJSONKeys[key]; ok {
+ return errors.New(errMsg)
+ }
+ }
+
if !IsAndroidPolicyFieldValid(key) {
return fmt.Errorf("Invalid JSON payload. Unknown key %q", key)
}
diff --git a/server/service/mdm.go b/server/service/mdm.go
index 772ea3ef76..661f4cbd4b 100644
--- a/server/service/mdm.go
+++ b/server/service/mdm.go
@@ -1630,7 +1630,7 @@ func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint,
Name: profileName,
RawJSON: data,
}
- if err := cp.ValidateUserProvided(); err != nil {
+ if err := cp.ValidateUserProvided(license.IsPremium(ctx)); err != nil {
err := &fleet.BadRequestError{Message: "Couldn't add. " + err.Error()}
return nil, ctxerr.Wrap(ctx, err, "validate profile")
}
@@ -2443,6 +2443,7 @@ func getAndroidProfiles(ctx context.Context,
appCfg *fleet.AppConfig,
profiles map[int]fleet.MDMProfileBatchPayload,
labelMap map[string]fleet.ConfigurationProfileLabel,
+ // isPremium bool,
) (map[int]*fleet.MDMAndroidConfigProfile, error) {
profs := make(map[int]*fleet.MDMAndroidConfigProfile, len(profiles))
for i, profile := range profiles {
@@ -2484,7 +2485,7 @@ func getAndroidProfiles(ctx context.Context,
}
}
- if err := mdmProf.ValidateUserProvided(); err != nil {
+ if err := mdmProf.ValidateUserProvided(license.IsPremium(ctx)); err != nil {
msg := err.Error()
return nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%s]", profile.Name), msg))
diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go
index c5b1fe11eb..ffce1478a1 100644
--- a/server/service/mdm_test.go
+++ b/server/service/mdm_test.go
@@ -1796,6 +1796,28 @@ func TestMDMBatchSetProfiles(t *testing.T) {
},
"duplicate json by name",
},
+ {
+ "premium-only android profile without premium license",
+ &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
+ false,
+ nil,
+ nil,
+ []fleet.MDMProfileBatchPayload{
+ {Name: "systemUpdate", Contents: json.RawMessage([]byte(`{"systemUpdate": {"type": "AUTOMATIC"}}`))},
+ },
+ `Android OS updates ("systemUpdate") is Fleet Premium only.`,
+ },
+ {
+ "premium-only android profile with premium license",
+ &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
+ true,
+ nil,
+ nil,
+ []fleet.MDMProfileBatchPayload{
+ {Name: "systemUpdate", Contents: json.RawMessage([]byte(`{"systemUpdate": {"type": "AUTOMATIC"}}`))},
+ },
+ "",
+ },
}
for _, tt := range testCases {
@@ -2589,3 +2611,100 @@ func TestUploadMDMAppleAPNSCertReplacesFileVaultProfile(t *testing.T) {
require.EqualValues(t, 2, deleteCalls)
require.EqualValues(t, 2, newActivityCalls) // Only enabled Disk encryption activities, we don't want to log disable right before enabling.
}
+
+func TestNewMDMProfilePremiumOnlyAndroid(t *testing.T) {
+ require.Len(t, fleet.AndroidPremiumOnlyJSONKeys, 1, "update this test with any new premium-only key for android profiles")
+ require.Contains(t, fleet.AndroidPremiumOnlyJSONKeys, "systemUpdate", "update this test with any new premium-only key for android profiles")
+
+ ds := new(mock.Store)
+ svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
+
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return &fleet.AppConfig{
+ OrgInfo: fleet.OrgInfo{
+ OrgName: "Foo Inc.",
+ },
+ ServerSettings: fleet.ServerSettings{
+ ServerURL: "https://foo.example.com",
+ },
+ MDM: fleet.MDM{
+ EnabledAndConfigured: true,
+ WindowsEnabledAndConfigured: true,
+ AndroidEnabledAndConfigured: true,
+ },
+ }, nil
+ }
+ ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
+ return &fleet.Team{ID: 1, Name: name}, nil
+ }
+ ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
+ return &fleet.Team{ID: id, Name: "team"}, nil
+ }
+ ds.NewActivityFunc = func(
+ ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
+ ) error {
+ return nil
+ }
+ ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
+ return nil
+ }
+ ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
+ return document, nil, nil
+ }
+ ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
+ return &fleet.GroupedCertificateAuthorities{}, nil
+ }
+ ds.NewMDMAndroidConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error) {
+ return &fleet.MDMAndroidConfigProfile{}, nil
+ }
+
+ testCases := []struct {
+ name string
+ user *fleet.User
+ premium bool
+ teamID uint
+ profile string
+ wantErr string
+ }{
+ {
+ "premium-only android profile without premium license",
+ &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
+ false,
+ 0,
+ `{"systemUpdate": {"type": "AUTOMATIC"}}`,
+ `Android OS updates ("systemUpdate") is Fleet Premium only.`,
+ },
+ {
+ "premium-only android profile with premium license",
+ &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
+ true,
+ 0,
+ `{"systemUpdate": {"type": "AUTOMATIC"}}`,
+ "",
+ },
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ defer func() { ds.NewMDMAndroidConfigProfileFuncInvoked = false }()
+
+ // prepare the context with the user and license
+ ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
+ tier := fleet.TierFree
+ if tt.premium {
+ tier = fleet.TierPremium
+ }
+ ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
+
+ _, err := svc.NewMDMAndroidConfigProfile(ctx, tt.teamID, tt.name, []byte(tt.profile), nil, fleet.LabelsIncludeAll)
+ if tt.wantErr == "" {
+ require.NoError(t, err)
+ require.True(t, ds.NewMDMAndroidConfigProfileFuncInvoked)
+ return
+ }
+ require.Error(t, err)
+ require.ErrorContains(t, err, tt.wantErr)
+ require.False(t, ds.NewMDMAndroidConfigProfileFuncInvoked)
+ })
+ }
+}