iOS and iPadOS device details refetch (#20678)

Part 1 of #19447
- iOS and iPadOS device details refetch can now be triggered with the
existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint

# Checklist for submitter

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky 2024-07-24 19:46:24 +02:00 committed by GitHub
parent 544d5b20c4
commit 90a1ac9faa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 148 additions and 50 deletions

View file

@ -0,0 +1 @@
- iOS and iPadOS device details refetch can now be triggered with the existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint.

View file

@ -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(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>Queries</key>
<array>
<string>DeviceName</string>
<string>DeviceCapacity</string>
<string>AvailableDeviceCapacity</string>
<string>OSVersion</string>
<string>WiFiMAC</string>
<string>ProductName</string>
</array>
<key>RequestType</key>
<string>DeviceInformation</string>
</dict>
<key>CommandUUID</key>
<string>%s</string>
</dict>
</plist>`, 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

View file

@ -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(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>Queries</key>
<array>
<string>DeviceName</string>
<string>DeviceCapacity</string>
<string>AvailableDeviceCapacity</string>
<string>OSVersion</string>
<string>WiFiMAC</string>
<string>ProductName</string>
</array>
<key>RequestType</key>
<string>DeviceInformation</string>
</dict>
<key>CommandUUID</key>
<string>%s</string>
</dict>
</plist>`, cmdUUID)
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
}
// EnqueueCommand takes care of enqueuing the commands and sending push
// notifications to the devices.
//

View file

@ -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")
}

View file

@ -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 {

View file

@ -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)
}