mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
Add support for Android systemUpdate profile (#35791)
This commit is contained in:
parent
03e8a35854
commit
ecac38f8ef
6 changed files with 143 additions and 12 deletions
1
changes/25896-android-os-updates
Normal file
1
changes/25896-android-os-updates
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Added support for Android `systemUpdate` profiles in Fleet Premium.
|
||||
|
|
@ -128,8 +128,7 @@ const OSUpdates = ({ router, teamIdForApi, queryParams }: IOSUpdates) => {
|
|||
return (
|
||||
<div className={baseClass}>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Remotely encourage the installation of software updates on hosts
|
||||
assigned to this team.
|
||||
Remotely enforce software updates.
|
||||
</p>
|
||||
<>
|
||||
<div className={`${baseClass}__current-version-container`}>
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
<TabPanel className={`${baseClass}__tab-panel`}>
|
||||
<div className={`${baseClass}__coming-soon`}>
|
||||
<p>
|
||||
<b>Android updates are coming soon.</b>
|
||||
</p>
|
||||
<p>
|
||||
Need to encourage installation of Android updates?{" "}
|
||||
<CustomLink url={SUPPORT_LINK} text="Let us know" newTab />
|
||||
Add a{" "}
|
||||
<CustomLink
|
||||
url={`/controls/os-settings/custom-settings?team_id=${currentTeamId}`}
|
||||
text="custom setting"
|
||||
/>{" "}
|
||||
(configuration profile) to enforce Android OS updates.
|
||||
</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue