From 707f71c14dd557299778b070fdb134bee4c165b4 Mon Sep 17 00:00:00 2001 From: Jonathan Katz <44128041+jkatz01@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:18:06 -0500 Subject: [PATCH] Put Android profiles in pending immediately on upload (#39330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #35613 Android profiles will now show up in the UI as "pending" immediately on a profile upload, to match Apple profiles behavior. Previously, you would have to wait until the `mdm_android_profile_manager` cron job for them to show up and actually install them. This could cost a little bit of performance, but is the existing behavior for Apple profiles. # 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. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [ ] 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: - Adding an Android profile adds it to null in host_mdm_android_profiles, which makes it show up as “pending” immediately in the UI. When `mdm_android_profile_manager` runs it sets them to pending in the database instead of null. - Deleting a profile will also cause all profiles to become pending immediately.
 --- changes/35613-android-profiles-pending | 1 + ...tion_android_certificate_templates_test.go | 2 +- .../service/integration_mdm_profiles_test.go | 46 +++++++++++++++++++ server/service/mdm.go | 3 ++ server/service/mdm_test.go | 3 ++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changes/35613-android-profiles-pending diff --git a/changes/35613-android-profiles-pending b/changes/35613-android-profiles-pending new file mode 100644 index 0000000000..7117e8f64c --- /dev/null +++ b/changes/35613-android-profiles-pending @@ -0,0 +1 @@ +Updated Android MDM profiles to show up as pending on upload, the same as Apple MDM profiles. diff --git a/server/service/integration_android_certificate_templates_test.go b/server/service/integration_android_certificate_templates_test.go index faa7acb70f..63e7a61a90 100644 --- a/server/service/integration_android_certificate_templates_test.go +++ b/server/service/integration_android_certificate_templates_test.go @@ -138,7 +138,7 @@ func (s *integrationMDMTestSuite) createEnrolledAndroidHost(t *testing.T, ctx co AppliedPolicyID: ptr.String("1"), }, } - androidHostInput.SetNodeKey(enterpriseID) + androidHostInput.SetNodeKey("android/" + hostUUID) createdAndroidHost, err := s.ds.NewAndroidHost(ctx, androidHostInput) require.NoError(t, err) diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index 002e2d6d6a..8fc6bd196f 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -8691,3 +8691,49 @@ func (s *integrationMDMTestSuite) TestWindowsProfileRetry() { require.Len(t, cmds, 1) // only ack returned }) } + +func (s *integrationMDMTestSuite) TestHostMDMAndroidProfilesStatus() { + t := s.T() + ctx := context.Background() + s.setSkipWorkerJobs(t) + + testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"}) + require.NoError(t, err) + + // Ensure MDM is turned on + appConfig, err := s.ds.AppConfig(ctx) + require.NoError(t, err) + appConfig.MDM.AndroidEnabledAndConfigured = true + err = s.ds.SaveAppConfig(ctx, appConfig) + require.NoError(t, err) + enterpriseID := s.enableAndroidMDM(t) + + s.createEnrolledAndroidHost(t, ctx, enterpriseID, &testTeam.ID, "host-1") + s.createEnrolledAndroidHost(t, ctx, enterpriseID, &testTeam.ID, "host-2") + + var hosts listHostsResponse + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &hosts) + assert.Len(t, hosts.Hosts, 2) + + bytes := []byte(`{ + "removeUserDisabled": false +}`) + + fields := make(map[string][]string) + fields["team_id"] = []string{fmt.Sprintf("%d", testTeam.ID)} + body, headers := generateNewProfileMultipartRequest(t, "remove-user-disabled.json", bytes, s.token, fields) + res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusOK, headers) + require.NotNil(t, res) + + // profiles should be added with NULL status even before the cron job (s.awaitTriggerAndroidProfileSchedule(t)) + var profiles []fleet.HostMDMAndroidProfile + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + err := sqlx.SelectContext(ctx, q, &profiles, "SELECT host_uuid, status, operation_type FROM host_mdm_android_profiles") + require.NoError(t, err) + return nil + }) + + require.Len(t, profiles, 2) + require.Nil(t, profiles[0].Status) + require.Nil(t, profiles[1].Status) +} diff --git a/server/service/mdm.go b/server/service/mdm.go index 6e8dc125ea..97fce5573d 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1801,6 +1801,9 @@ func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, } return nil, ctxerr.Wrap(ctx, err) } + if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newCP.ProfileUUID}, nil); err != nil { + return nil, ctxerr.Wrap(ctx, err, "bulk set pending host profiles") + } var ( actTeamID *uint diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index c057890468..772790eb13 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -2669,6 +2669,9 @@ func TestNewMDMProfilePremiumOnlyAndroid(t *testing.T) { ds.NewMDMAndroidConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error) { return &fleet.MDMAndroidConfigProfile{}, nil } + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil + } testCases := []struct { name string