diff --git a/changes/5138-mdm-migration-err b/changes/5138-mdm-migration-err new file mode 100644 index 0000000000..9db0aa201f --- /dev/null +++ b/changes/5138-mdm-migration-err @@ -0,0 +1,2 @@ +- Fixes issue where a bad request response from a 3rd party MDM solution would result in a 500 error + in Fleet during MDM migration. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 495a0a1e15..afcf2ce089 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -6196,6 +6196,75 @@ func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhook() { require.False(t, webhookCalled) } +func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhookErrors() { + t := s.T() + + h := createHostAndDeviceToken(t, s.ds, "good-token") + + var webhookCalled bool + webhookSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + webhookCalled = true + w.WriteHeader(http.StatusBadRequest) + })) + defer webhookSrv.Close() + + // patch app config with webhook url + acResp := fleet.AppConfig{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{ + "mdm": { + "macos_migration": { + "enable": true, + "mode": "voluntary", + "webhook_url": "%s/test_mdm_migration" + } + } + }`, webhookSrv.URL)), http.StatusOK, &acResp) + require.True(t, acResp.MDM.MacOSMigration.Enable) + + isServer, enrolled, installedFromDEP := true, true, true + mdmName := "ExampleMDM" + mdmURL := "https://mdm.example.com" + + // host is enrolled to a third-party MDM but hasn't been assigned in + // ABM yet, so migration is not allowed + require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, !isServer, enrolled, mdmURL, installedFromDEP, mdmName, "")) + s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest) + require.False(t, webhookCalled) + + // simulate that the device is assigned to Fleet in ABM + s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + switch r.URL.Path { + case "/session": + _, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`)) + case "/profile": + encoder := json.NewEncoder(w) + err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"}) + require.NoError(t, err) + case "/server/devices", "/devices/sync": + encoder := json.NewEncoder(w) + err := encoder.Encode(godep.DeviceResponse{ + Devices: []godep.Device{ + { + SerialNumber: h.HardwareSerial, + Model: "Mac Mini", + OS: "osx", + OpType: "added", + }, + }, + }) + require.NoError(t, err) + } + })) + s.runDEPSchedule() + + // hosts meets all requirements, webhook is run but returns an error, server should respond with + // the same status code + require.False(t, webhookCalled) + s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest) + require.True(t, webhookCalled) +} + func (s *integrationMDMTestSuite) TestMDMMacOSSetup() { t := s.T() diff --git a/server/utils.go b/server/utils.go index 76643ee4fd..4d8edbae81 100644 --- a/server/utils.go +++ b/server/utils.go @@ -37,6 +37,22 @@ func httpSuccessStatus(statusCode int) bool { return statusCode >= 200 && statusCode <= 299 } +// errWithStatus is an error with a particular status code. +type errWithStatus struct { + err string + statusCode int +} + +// Error implements the error interface +func (e *errWithStatus) Error() string { + return e.err +} + +// StatusCode implements the StatusCoder interface for returning custom status codes. +func (e *errWithStatus) StatusCode() int { + return e.statusCode +} + func PostJSONWithTimeout(ctx context.Context, url string, v interface{}) error { jsonBytes, err := json.Marshal(v) if err != nil { @@ -59,7 +75,7 @@ func PostJSONWithTimeout(ctx context.Context, url string, v interface{}) error { if !httpSuccessStatus(resp.StatusCode) { body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("error posting to %s: %d. %s", MaskSecretURLParams(url), resp.StatusCode, string(body)) + return &errWithStatus{err: fmt.Sprintf("error posting to %s: %d. %s", MaskSecretURLParams(url), resp.StatusCode, string(body)), statusCode: resp.StatusCode} } return nil diff --git a/website/api/controllers/webhooks/receive-from-customer-fleet-instance.js b/website/api/controllers/webhooks/receive-from-customer-fleet-instance.js index 92b808557a..98ee042d2d 100644 --- a/website/api/controllers/webhooks/receive-from-customer-fleet-instance.js +++ b/website/api/controllers/webhooks/receive-from-customer-fleet-instance.js @@ -41,6 +41,10 @@ module.exports = { unauthorized: { responseType: 'unauthorized', description: 'This webhook request could not be verified.', + }, + badRequest: { + responseType: 'badRequest', + description: 'Invalid MDM migration request.' } }, @@ -102,7 +106,7 @@ module.exports = { if(err.raw.statusCode === 404){ return new Error(`When sending a request to unenroll a host from a Workspace One instance (Host information: Serial number: ${host.hardware_serial}, id: ${host.id}, uuid: ${host.uuid}), the specified host was not found on the customer's Workspace One instance. Full error: ${err.stack}`); } else if(err.raw.statusCode === 400) { - return new Error(`When sending a request to unenroll a host from a Workspace One instance (Host information: Serial number: ${host.hardware_serial}, id: ${host.id}, uuid: ${host.uuid}), the Workspace One instance could not unenroll the specified host. Full error: ${err.stack}`); + return { badRequest: `When sending a request to unenroll a host from a Workspace One instance (Host information: Serial number: ${host.hardware_serial}, id: ${host.id}, uuid: ${host.uuid}), the Workspace One instance could not unenroll the specified host. Full error: ${err.stack}` }; } else { return new Error(`When sending a request to unenroll a host from a Workspace One instance (Host information: Serial number: ${host.hardware_serial}, id: ${host.id}, uuid: ${host.uuid}), an error occured. Full error: ${err.stack}`); }