diff --git a/changes/19447-ios-ipados-software b/changes/19447-ios-ipados-software
new file mode 100644
index 0000000000..755f37b0c6
--- /dev/null
+++ b/changes/19447-ios-ipados-software
@@ -0,0 +1 @@
+- iOS and iPadOS device details refetch can now be triggered with the existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint.
diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go
index 4e8d547fe4..03d3621b19 100644
--- a/cmd/fleet/cron.go
+++ b/cmd/fleet/cron.go
@@ -1305,28 +1305,7 @@ func newIPhoneIPadRefetcher(
}
logger.Log("msg", "sending commands to refetch", "count", len(uuids), "lookup-duration", time.Since(start))
commandUUID := fleet.RefetchCommandUUIDPrefix + uuid.NewString()
- if err := commander.EnqueueCommand(ctx, uuids, fmt.Sprintf(`
-
-
-
- Command
-
- Queries
-
- DeviceName
- DeviceCapacity
- AvailableDeviceCapacity
- OSVersion
- WiFiMAC
- ProductName
-
- RequestType
- DeviceInformation
-
- CommandUUID
- %s
-
-`, commandUUID)); err != nil {
+ if err := commander.DeviceInformation(ctx, uuids, commandUUID); err != nil {
return ctxerr.Wrap(ctx, err, "send DeviceInformation commands to ios and ipados devices")
}
return nil
diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go
index 43ea5d7143..7c246e0743 100644
--- a/server/mdm/apple/commander.go
+++ b/server/mdm/apple/commander.go
@@ -273,6 +273,33 @@ func (svc *MDMAppleCommander) DeviceConfigured(ctx context.Context, hostUUID, cm
return svc.EnqueueCommand(ctx, []string{hostUUID}, raw)
}
+func (svc *MDMAppleCommander) DeviceInformation(ctx context.Context, hostUUIDs []string, cmdUUID string) error {
+ raw := fmt.Sprintf(`
+
+
+
+ Command
+
+ Queries
+
+ DeviceName
+ DeviceCapacity
+ AvailableDeviceCapacity
+ OSVersion
+ WiFiMAC
+ ProductName
+
+ RequestType
+ DeviceInformation
+
+ CommandUUID
+ %s
+
+`, cmdUUID)
+
+ return svc.EnqueueCommand(ctx, hostUUIDs, raw)
+}
+
// EnqueueCommand takes care of enqueuing the commands and sending push
// notifications to the devices.
//
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 5e0d149698..fa32591168 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -2752,6 +2752,7 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
host.PrimaryMac = wifiMac
host.HardwareModel = productName
host.DetailUpdatedAt = time.Now()
+ host.RefetchRequested = false
if err := svc.ds.UpdateHost(r.Context, host); err != nil {
return nil, ctxerr.Wrap(r.Context, err, "failed to update host")
}
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 4de8dd091a..18f6831d82 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -6,6 +6,7 @@ import (
"crypto/tls"
"encoding/csv"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -30,6 +31,7 @@ import (
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/go-kit/log/level"
"github.com/gocarina/gocsv"
+ "github.com/google/uuid"
)
// HostDetailResponse is the response struct that contains the full host information
@@ -1008,12 +1010,15 @@ func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
+ var host *fleet.Host
+ // iOS and iPadOS refetch are not authenticated with device token because these devices do not have Fleet Desktop
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
- if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
+ var err error
+ if err = svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return err
}
- host, err := svc.ds.HostLite(ctx, id)
+ host, err = svc.ds.HostLite(ctx, id)
if err != nil {
return ctxerr.Wrap(ctx, err, "find host for refetch")
}
@@ -1025,6 +1030,17 @@ func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
}
}
+ if host != nil && (host.Platform == "ios" || host.Platform == "ipados") {
+ err := svc.verifyMDMConfiguredAndConnected(ctx, host)
+ if err != nil {
+ return err
+ }
+ err = svc.mdmAppleCommander.DeviceInformation(ctx, []string{host.UUID}, fleet.RefetchCommandUUIDPrefix+uuid.NewString())
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "refetch host with MDM")
+ }
+ }
+
if err := svc.ds.UpdateHostRefetchRequested(ctx, id, true); err != nil {
return ctxerr.Wrap(ctx, err, "save host")
}
@@ -1032,6 +1048,24 @@ func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
return nil
}
+func (svc *Service) verifyMDMConfiguredAndConnected(ctx context.Context, host *fleet.Host) error {
+ if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
+ if errors.Is(err, fleet.ErrMDMNotConfigured) {
+ err = fleet.NewInvalidArgumentError("id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
+ }
+ return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
+ }
+ connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
+ }
+ if !connected {
+ return ctxerr.Wrap(ctx,
+ fleet.NewInvalidArgumentError("id", "Host does not have MDM turned on."))
+ }
+ return nil
+}
+
func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
if !opts.ExcludeSoftware {
if err := svc.ds.LoadHostSoftware(ctx, host, opts.IncludeCVEScores); err != nil {
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 3ada175919..eb6bf02ed1 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -771,6 +771,43 @@ func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testi
return fleetHost, mdmDevice
}
+func (s *integrationMDMTestSuite) createAppleMobileHostThenEnrollMDM(platform string) (*fleet.Host, *mdmtest.TestAppleMDMClient) {
+ ctx := context.Background()
+ t := s.T()
+
+ // create a host with minimal information and the serial, no uuid/osquery id
+ // (as when created via DEP sync).
+ dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+ serialNumber := mdmtest.RandSerialNumber()
+ fleetHost, err := s.ds.NewHost(ctx, &fleet.Host{
+ HardwareSerial: serialNumber,
+ Platform: platform,
+ LastEnrolledAt: dbZeroTime,
+ DetailUpdatedAt: dbZeroTime,
+ RefetchRequested: true,
+ })
+ require.NoError(t, err)
+ require.Equal(t, dbZeroTime, fleetHost.LastEnrolledAt)
+
+ // Perform the MDM enrollment.
+ mdmEnrollInfo := mdmtest.AppleEnrollInfo{
+ SCEPChallenge: s.scepChallenge,
+ SCEPURL: s.server.URL + apple_mdm.SCEPPath,
+ MDMURL: s.server.URL + apple_mdm.MDMPath,
+ }
+ model := "iPhone14,6"
+ if platform == "ipados" {
+ model = "iPad13,18"
+ }
+ mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model)
+ mdmDevice.SerialNumber = serialNumber
+ err = mdmDevice.Enroll()
+ require.NoError(t, err)
+
+ return fleetHost, mdmDevice
+
+}
+
func createWindowsHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) {
host := createOrbitEnrolledHost(t, "windows", "h1", ds)
mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(fleetServerURL, *host.OrbitNodeKey)
@@ -9311,37 +9348,56 @@ func (s *integrationMDMTestSuite) TestInvalidCommandUUID() {
func (s *integrationMDMTestSuite) TestEnrollAfterDEPSyncIOSIPadOS() {
t := s.T()
- ctx := context.Background()
- // create a host with minimal information and the serial, no uuid/osquery id
- // (as when created via DEP sync).
- dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
- serialNumber := mdmtest.RandSerialNumber()
- h, err := s.ds.NewHost(ctx, &fleet.Host{
- HardwareSerial: serialNumber,
- Platform: "ios",
- LastEnrolledAt: dbZeroTime,
- DetailUpdatedAt: dbZeroTime,
- RefetchRequested: true,
- })
- require.NoError(t, err)
- require.Equal(t, dbZeroTime, h.LastEnrolledAt)
-
- // Perform the MDM enrollment.
- mdmEnrollInfo := mdmtest.AppleEnrollInfo{
- SCEPChallenge: s.scepChallenge,
- SCEPURL: s.server.URL + apple_mdm.SCEPPath,
- MDMURL: s.server.URL + apple_mdm.MDMPath,
- }
- mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "iPhone14,6")
- mdmDevice.SerialNumber = serialNumber
- err = mdmDevice.Enroll()
- require.NoError(t, err)
+ h, _ := s.createAppleMobileHostThenEnrollMDM("ios")
// fetch the host, it will match the one created above
// (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response)
var hostResp getHostResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp)
require.Equal(t, h.ID, hostResp.Host.ID)
- require.NotEqual(t, dbZeroTime, hostResp.Host.LastEnrolledAt)
+ require.NotEqual(t, h.LastEnrolledAt, hostResp.Host.LastEnrolledAt)
+
+ h, _ = s.createAppleMobileHostThenEnrollMDM("ipados")
+
+ // fetch the host, it will match the one created above
+ // (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response)
+ hostResp = getHostResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp)
+ require.Equal(t, h.ID, hostResp.Host.ID)
+ require.NotEqual(t, h.LastEnrolledAt, hostResp.Host.LastEnrolledAt)
+
+}
+
+func (s *integrationMDMTestSuite) TestRefetchIOSIPadOS() {
+ t := s.T()
+
+ // Try to refetch host that is not MDM enrolled
+ serialNumber := mdmtest.RandSerialNumber()
+ fleetHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
+ HardwareSerial: serialNumber,
+ Platform: "ipados",
+ LastEnrolledAt: time.Now(),
+ DetailUpdatedAt: time.Now(),
+ RefetchRequested: true,
+ })
+ require.NoError(t, err)
+ r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", fleetHost.ID), nil, http.StatusUnprocessableEntity, "error",
+ "host is not enrolled in MDM")
+ assert.Contains(t, extractServerErrorText(r.Body), "Host does not have MDM turned on")
+
+ // Try to refetch an MDM enrolled host
+ host, mdmClient := s.createAppleMobileHostThenEnrollMDM("ios")
+ _ = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", host.ID), nil, http.StatusOK)
+
+ // Check the MDM command
+ cmd, err := mdmClient.Idle()
+ require.NoError(t, err)
+ require.NotNil(t, cmd)
+ assert.Equal(t, "DeviceInformation", cmd.Command.RequestType)
+
+ var hostResp getHostResponse
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
+ assert.Equal(t, host.ID, hostResp.Host.ID)
+ assert.True(t, host.RefetchRequested)
}