mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
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:
parent
544d5b20c4
commit
90a1ac9faa
6 changed files with 148 additions and 50 deletions
1
changes/19447-ios-ipados-software
Normal file
1
changes/19447-ios-ipados-software
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue