package apple_mdm import ( "context" "encoding/base64" "fmt" "net/http" "sort" "strings" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" mdmcrypto "github.com/fleetdm/fleet/v4/server/mdm/crypto" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" "github.com/groob/plist" ) // commandPayload is the common structure all MDM commands use type commandPayload struct { CommandUUID string Command any } // MDMAppleCommander contains methods to enqueue commands managed by Fleet and // send push notifications to hosts. // // It's intentionally decoupled from fleet.Service so it can be used internally // in crons and other services, leaving authentication/permission handling to // the caller. type MDMAppleCommander struct { storage fleet.MDMAppleStore pusher nanomdm_push.Pusher } // NewMDMAppleCommander creates a new commander instance. func NewMDMAppleCommander(mdmStorage fleet.MDMAppleStore, mdmPushService nanomdm_push.Pusher) *MDMAppleCommander { return &MDMAppleCommander{ storage: mdmStorage, pusher: mdmPushService, } } // InstallProfile sends the homonymous MDM command to the given hosts, it also // takes care of the base64 encoding of the provided profile bytes. func (svc *MDMAppleCommander) InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error { signedProfile, err := mdmcrypto.Sign(ctx, profile, svc.storage) if err != nil { return ctxerr.Wrap(ctx, err, "signing profile") } base64Profile := base64.StdEncoding.EncodeToString(signedProfile) raw := fmt.Sprintf(` CommandUUID %s Command RequestType InstallProfile Payload %s `, uuid, base64Profile) err = svc.EnqueueCommand(ctx, hostUUIDs, raw) return ctxerr.Wrap(ctx, err, "commander install profile") } // InstallProfile sends the homonymous MDM command to the given hosts. func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []string, profileIdentifier string, uuid string) error { raw := fmt.Sprintf(` CommandUUID %s Command RequestType RemoveProfile Identifier %s `, uuid, profileIdentifier) err := svc.EnqueueCommand(ctx, hostUUIDs, raw) return ctxerr.Wrap(ctx, err, "commander remove profile") } func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) (unlockPIN string, err error) { unlockPIN = GenerateRandomPin(6) raw := fmt.Sprintf(` CommandUUID %s Command RequestType DeviceLock PIN %s `, uuid, unlockPIN, ) cmd, err := mdm.DecodeCommand([]byte(raw)) if err != nil { return "", ctxerr.Wrap(ctx, err, "decoding command") } if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, unlockPIN); err != nil { return "", ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock") } if err := svc.SendNotifications(ctx, []string{host.UUID}); err != nil { return "", ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock") } return unlockPIN, nil } func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host, uuid string) error { pin := GenerateRandomPin(6) raw := fmt.Sprintf(` CommandUUID %s Command RequestType EraseDevice PIN %s ObliterationBehavior Default `, uuid, pin) cmd, err := mdm.DecodeCommand([]byte(raw)) if err != nil { return ctxerr.Wrap(ctx, err, "decoding command") } if err := svc.storage.EnqueueDeviceWipeCommand(ctx, host, cmd); err != nil { return ctxerr.Wrap(ctx, err, "enqueuing for DeviceWipe") } if err := svc.SendNotifications(ctx, []string{host.UUID}); err != nil { return ctxerr.Wrap(ctx, err, "sending notifications for DeviceWipe") } return nil } func (svc *MDMAppleCommander) InstallApplication(ctx context.Context, hostUUIDs []string, uuid string, adamID string) error { raw := fmt.Sprintf(` Command ManagementFlags 0 Options PurchaseMethod 1 RequestType InstallApplication iTunesStoreID %s CommandUUID %s `, adamID, uuid) return svc.EnqueueCommand(ctx, hostUUIDs, raw) } func (svc *MDMAppleCommander) InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error { raw := fmt.Sprintf(` Command ManifestURL %s RequestType InstallEnterpriseApplication CommandUUID %s `, manifestURL, uuid) return svc.EnqueueCommand(ctx, hostUUIDs, raw) } type installEnterpriseApplicationPayload struct { Manifest *appmanifest.Manifest RequestType string } func (svc *MDMAppleCommander) InstallEnterpriseApplicationWithEmbeddedManifest( ctx context.Context, hostUUIDs []string, uuid string, manifest *appmanifest.Manifest, ) error { cmd := commandPayload{ CommandUUID: uuid, Command: installEnterpriseApplicationPayload{ RequestType: "InstallEnterpriseApplication", Manifest: manifest, }, } raw, err := plist.Marshal(cmd) if err != nil { return fmt.Errorf("marshal command payload plist: %w", err) } return svc.EnqueueCommand(ctx, hostUUIDs, string(raw)) } func (svc *MDMAppleCommander) AccountConfiguration(ctx context.Context, hostUUIDs []string, uuid, fullName, userName string) error { raw := fmt.Sprintf(` Command PrimaryAccountFullName %s PrimaryAccountUserName %s LockPrimaryAccountInfo RequestType AccountConfiguration CommandUUID %s `, fullName, userName, uuid) return svc.EnqueueCommand(ctx, hostUUIDs, raw) } // DeclarativeManagement sends the homonym [command][1] to the device to enable DDM or start a new DDM session. // // [1]: https://developer.apple.com/documentation/devicemanagement/declarativemanagementcommand func (svc *MDMAppleCommander) DeclarativeManagement(ctx context.Context, hostUUIDs []string, uuid string) error { raw := fmt.Sprintf(` Command RequestType DeclarativeManagement CommandUUID %s `, uuid) return svc.EnqueueCommand(ctx, hostUUIDs, raw) } func (svc *MDMAppleCommander) DeviceConfigured(ctx context.Context, hostUUID, cmdUUID string) error { raw := fmt.Sprintf(` Command RequestType DeviceConfigured CommandUUID %s `, cmdUUID) 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) } func (svc *MDMAppleCommander) InstalledApplicationList(ctx context.Context, hostUUIDs []string, cmdUUID string) error { raw := fmt.Sprintf(` Command ManagedAppsOnly RequestType InstalledApplicationList Items Name ShortVersion Identifier CommandUUID %s `, cmdUUID) return svc.EnqueueCommand(ctx, hostUUIDs, raw) } // EnqueueCommand takes care of enqueuing the commands and sending push // notifications to the devices. // // Always sending the push notification when a command is enqueued was decided // internally, leaving making pushes optional as an optimization to be tackled // later. func (svc *MDMAppleCommander) EnqueueCommand(ctx context.Context, hostUUIDs []string, rawCommand string) error { cmd, err := mdm.DecodeCommand([]byte(rawCommand)) if err != nil { return ctxerr.Wrap(ctx, err, "decoding command") } if _, err := svc.storage.EnqueueCommand(ctx, hostUUIDs, cmd); err != nil { return ctxerr.Wrap(ctx, err, "enqueuing command") } if err := svc.SendNotifications(ctx, hostUUIDs); err != nil { return ctxerr.Wrap(ctx, err, "sending notifications") } return nil } func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs []string) error { apnsResponses, err := svc.pusher.Push(ctx, hostUUIDs) if err != nil { return ctxerr.Wrap(ctx, err, "commander push") } // Even if we didn't get an error, some of the APNs // responses might have failed, signal that to the caller. failed := map[string]error{} for uuid, response := range apnsResponses { if response.Err != nil { failed[uuid] = response.Err } } if len(failed) > 0 { return &APNSDeliveryError{errorsByUUID: failed} } return nil } // APNSDeliveryError records an error and the associated host UUIDs in which it // occurred. type APNSDeliveryError struct { errorsByUUID map[string]error } func (e *APNSDeliveryError) Error() string { var uuids []string for uuid := range e.errorsByUUID { uuids = append(uuids, uuid) } // sort UUIDs alphabetically for deterministic output sort.Strings(uuids) var errStrings []string for _, uuid := range uuids { errStrings = append(errStrings, fmt.Sprintf("UUID: %s, Error: %v", uuid, e.errorsByUUID[uuid])) } return fmt.Sprintf( "APNS delivery failed with the following errors:\n%s", strings.Join(errStrings, "\n"), ) } func (e *APNSDeliveryError) FailedUUIDs() []string { var uuids []string for uuid := range e.errorsByUUID { uuids = append(uuids, uuid) } // sort UUIDs alphabetically for deterministic output sort.Strings(uuids) return uuids } func (e *APNSDeliveryError) StatusCode() int { return http.StatusBadGateway }