Fixed Android pubsub panic when host was deleted

This commit is contained in:
Victor Lyuboslavsky 2026-04-20 12:33:45 -05:00
parent ca1ab21cbc
commit 137ec3f406
No known key found for this signature in database
3 changed files with 75 additions and 0 deletions

View file

@ -0,0 +1 @@
- Fixed a server panic (502) when an Android pubsub status report arrived for a host that had been deleted from Fleet.

View file

@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
@ -210,6 +211,16 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
svc.logger.DebugContext(ctx, "Error re-enrolling Android host", "data", rawData)
return ctxerr.Wrap(ctx, err, "re-enrolling deleted Android host")
}
// Re-fetch the host so the subsequent updateHost/updateHostSoftware calls have a
// non-nil host. Force primary: enrollHost just INSERTed the host on the writer,
// and the default reader can be on a replica that hasn't caught up yet.
host, err = svc.getExistingHost(ctxdb.RequirePrimary(ctx, true), &device)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting re-enrolled Android host")
}
if host == nil {
return ctxerr.New(ctx, "re-enrolled Android host not found")
}
}
err = svc.updateHost(ctx, &device, host, false)
if err != nil {

View file

@ -14,6 +14,7 @@ import (
"time"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
@ -1699,3 +1700,65 @@ func TestStatusReportAppInstallVerification(t *testing.T) {
require.True(t, mockDS.GetPastActivityDataForAndroidVPPAppInstallFuncInvoked)
})
}
// Status report arrives for an Android host that was deleted from Fleet: the
// handler must re-enroll the host and then read it back from primary (issue #42494).
func TestPubSubStatusReportHostDeletedFromFleet(t *testing.T) {
svc, mockDS := createAndroidService(t)
mockDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{AndroidEnabledAndConfigured: true}}, nil
}
// AndroidHostLite returns not-found initially (host was deleted from Fleet) and
// returns the created host only after enrollHost has run.
var createdHost *fleet.AndroidHost
mockDS.AndroidHostLiteFunc = func(ctx context.Context, enterpriseSpecificID string) (*fleet.AndroidHost, error) {
if createdHost != nil && ctxdb.IsPrimaryRequired(ctx) {
return createdHost, nil
}
return nil, common_mysql.NotFound("android host lite mock")
}
mockDS.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
return &fleet.EnrollSecret{Secret: "global"}, nil
}
mockDS.NewAndroidHostFunc = func(ctx context.Context, host *fleet.AndroidHost, companyOwned bool) (*fleet.AndroidHost, error) {
createdHost = host
return host, nil
}
mockDS.UpdateAndroidHostFunc = func(ctx context.Context, host *fleet.AndroidHost, fromEnroll, companyOwned bool) error {
return nil
}
// Minimal device: no AppliedPolicyName / ApplicationReports, so the status report
// handler stays on the simple path (skips policy + software verification).
// EnrollmentTokenData is present because production AMAPI payloads include it;
// without it, enrollHost would fail to unmarshal and never exercise the fix.
device := androidmanagement.Device{
Name: createAndroidDeviceId("deleted-from-fleet"),
EnrollmentTokenData: `{"enroll_secret": "global"}`,
HardwareInfo: &androidmanagement.HardwareInfo{
EnterpriseSpecificId: strings.ToUpper(uuid.New().String()),
Brand: "TestBrand",
Model: "TestModel",
SerialNumber: "test-serial",
Hardware: "test-hardware",
},
SoftwareInfo: &androidmanagement.SoftwareInfo{AndroidBuildNumber: "test-build", AndroidVersion: "1"},
MemoryInfo: &androidmanagement.MemoryInfo{
TotalRam: int64(8 * 1024 * 1024 * 1024),
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024),
},
}
data, err := json.Marshal(device)
require.NoError(t, err)
statusReport := &android.PubSubMessage{
Attributes: map[string]string{"notificationType": string(android.PubSubStatusReport)},
Data: base64.StdEncoding.EncodeToString(data),
}
err = svc.ProcessPubSubPush(context.Background(), "value", statusReport)
require.NoError(t, err)
require.True(t, mockDS.NewAndroidHostFuncInvoked, "re-enrollment should create the host")
require.True(t, mockDS.UpdateAndroidHostFuncInvoked, "status report update should run against the re-enrolled host")
}