mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 16:08:47 +00:00
355 lines
12 KiB
Go
355 lines
12 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest"
|
|
kitlog "github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Name of the Apple MDM job as registered in the worker. Note that although it
|
|
// is a single job, it can process a number of different-but-related tasks,
|
|
// identified by the Task field in the job's payload.
|
|
const appleMDMJobName = "apple_mdm"
|
|
|
|
type AppleMDMTask string
|
|
|
|
// List of supported tasks.
|
|
const (
|
|
AppleMDMPostDEPEnrollmentTask AppleMDMTask = "post_dep_enrollment"
|
|
AppleMDMPostManualEnrollmentTask AppleMDMTask = "post_manual_enrollment"
|
|
AppleMDMPostDEPReleaseDeviceTask AppleMDMTask = "post_dep_release_device"
|
|
)
|
|
|
|
// AppleMDM is the job processor for the apple_mdm job.
|
|
type AppleMDM struct {
|
|
Datastore fleet.Datastore
|
|
Log kitlog.Logger
|
|
Commander *apple_mdm.MDMAppleCommander
|
|
}
|
|
|
|
// Name returns the name of the job.
|
|
func (a *AppleMDM) Name() string {
|
|
return appleMDMJobName
|
|
}
|
|
|
|
// appleMDMArgs is the payload for the Apple MDM job.
|
|
type appleMDMArgs struct {
|
|
Task AppleMDMTask `json:"task"`
|
|
HostUUID string `json:"host_uuid"`
|
|
TeamID *uint `json:"team_id,omitempty"`
|
|
EnrollReference string `json:"enroll_reference,omitempty"`
|
|
EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
|
|
}
|
|
|
|
// Run executes the apple_mdm job.
|
|
func (a *AppleMDM) Run(ctx context.Context, argsJSON json.RawMessage) error {
|
|
// if Commander is nil, then mdm is not enabled, so just return without
|
|
// error so we clean up any pending jobs.
|
|
if a.Commander == nil {
|
|
return nil
|
|
}
|
|
|
|
var args appleMDMArgs
|
|
if err := json.Unmarshal(argsJSON, &args); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "unmarshal args")
|
|
}
|
|
|
|
switch args.Task {
|
|
case AppleMDMPostDEPEnrollmentTask:
|
|
err := a.runPostDEPEnrollment(ctx, args)
|
|
return ctxerr.Wrap(ctx, err, "running post Apple DEP enrollment task")
|
|
|
|
case AppleMDMPostManualEnrollmentTask:
|
|
err := a.runPostManualEnrollment(ctx, args)
|
|
return ctxerr.Wrap(ctx, err, "running post Apple manual enrollment task")
|
|
|
|
case AppleMDMPostDEPReleaseDeviceTask:
|
|
err := a.runPostDEPReleaseDevice(ctx, args)
|
|
return ctxerr.Wrap(ctx, err, "running post Apple DEP release device task")
|
|
|
|
default:
|
|
return ctxerr.Errorf(ctx, "unknown task: %v", args.Task)
|
|
}
|
|
}
|
|
|
|
func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArgs) error {
|
|
if _, err := a.installFleetd(ctx, args.HostUUID); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error {
|
|
var awaitCmdUUIDs []string
|
|
|
|
fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
|
|
}
|
|
awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID)
|
|
|
|
bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
|
|
}
|
|
if bootstrapCmdUUID != "" {
|
|
awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID)
|
|
}
|
|
|
|
if ref := args.EnrollReference; ref != "" {
|
|
a.Log.Log("info", "got an enroll_reference", "host_uuid", args.HostUUID, "ref", ref)
|
|
appCfg, err := a.Datastore.AppConfig(ctx)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "getting app config")
|
|
}
|
|
|
|
acct, err := a.Datastore.GetMDMIdPAccountByUUID(ctx, ref)
|
|
if err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "getting idp account details for enroll reference %s", ref)
|
|
}
|
|
|
|
ssoEnabled := appCfg.MDM.MacOSSetup.EnableEndUserAuthentication
|
|
if args.TeamID != nil {
|
|
team, err := a.Datastore.Team(ctx, *args.TeamID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "fetch team to send AccountConfiguration")
|
|
}
|
|
ssoEnabled = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
|
|
}
|
|
|
|
if ssoEnabled {
|
|
a.Log.Log("info", "setting username and fullname", "host_uuid", args.HostUUID)
|
|
cmdUUID := uuid.New().String()
|
|
if err := a.Commander.AccountConfiguration(
|
|
ctx,
|
|
[]string{args.HostUUID},
|
|
cmdUUID,
|
|
acct.Fullname,
|
|
acct.Username,
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "sending AccountConfiguration command")
|
|
}
|
|
awaitCmdUUIDs = append(awaitCmdUUIDs, cmdUUID)
|
|
}
|
|
}
|
|
|
|
var manualRelease bool
|
|
if args.TeamID == nil {
|
|
ac, err := a.Datastore.AppConfig(ctx)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get AppConfig to read enable_release_device_manually")
|
|
}
|
|
manualRelease = ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
|
} else {
|
|
tm, err := a.Datastore.Team(ctx, *args.TeamID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get Team to read enable_release_device_manually")
|
|
}
|
|
manualRelease = tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
|
}
|
|
|
|
if !manualRelease {
|
|
// send all command uuids for the commands sent here during post-DEP
|
|
// enrollment and enqueue a job to look for the status of those commands to
|
|
// be final and same for MDM profiles of that host; it means the DEP
|
|
// enrollment process is done and the device can be released.
|
|
if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask,
|
|
args.HostUUID, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error {
|
|
// Edge cases:
|
|
// - if the device goes offline for a long time, should we go ahead and
|
|
// release after a while?
|
|
// - if some commands/profiles failed (a final state), should we go ahead
|
|
// and release?
|
|
// - if the device keeps moving team, or profiles keep being added/removed
|
|
// from its team, it's possible that its profiles will never settle and
|
|
// always have pending statuses. Same as going offline, should we release
|
|
// after a while?
|
|
//
|
|
// We opted "yes" to all those, and we want to release after a few minutes,
|
|
// not hours, so we'll allow only a couple retries.
|
|
|
|
level.Debug(a.Log).Log(
|
|
"task", "runPostDEPReleaseDevice",
|
|
"msg", fmt.Sprintf("awaiting commands %v and profiles to settle for host %s", args.EnrollmentCommands, args.HostUUID),
|
|
)
|
|
|
|
if retryNum, _ := ctx.Value(retryNumberCtxKey).(int); retryNum > 2 {
|
|
// give up and release the device
|
|
a.Log.Log("info", "releasing device after too many attempts", "host_uuid", args.HostUUID, "retries", retryNum)
|
|
if err := a.Commander.DeviceConfigured(ctx, args.HostUUID, uuid.NewString()); err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "failed to enqueue DeviceConfigured command after %d retries", retryNum)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, cmdUUID := range args.EnrollmentCommands {
|
|
if cmdUUID == "" {
|
|
continue
|
|
}
|
|
|
|
res, err := a.Datastore.GetMDMAppleCommandResults(ctx, cmdUUID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "failed to get MDM command results")
|
|
}
|
|
|
|
var completed bool
|
|
for _, r := range res {
|
|
// succeeded or failed, it is done (final state)
|
|
if r.Status == fleet.MDMAppleStatusAcknowledged || r.Status == fleet.MDMAppleStatusError ||
|
|
r.Status == fleet.MDMAppleStatusCommandFormatError {
|
|
completed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !completed {
|
|
// DEP enrollment commands are not done being delivered to that device,
|
|
// cannot release it now.
|
|
return fmt.Errorf("device not ready for release, still awaiting result for command %s, will retry", cmdUUID)
|
|
}
|
|
level.Debug(a.Log).Log(
|
|
"task", "runPostDEPReleaseDevice",
|
|
"msg", fmt.Sprintf("command %s has completed", cmdUUID),
|
|
)
|
|
}
|
|
|
|
// all DEP-enrollment commands are done, check the host's profiles
|
|
profs, err := a.Datastore.GetHostMDMAppleProfiles(ctx, args.HostUUID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "failed to get host MDM profiles")
|
|
}
|
|
for _, prof := range profs {
|
|
// if it has any pending profiles, then its profiles are not done being
|
|
// delivered (installed or removed).
|
|
if prof.Status == nil || *prof.Status == fleet.MDMDeliveryPending {
|
|
return fmt.Errorf("device not ready for release, profile %s is still pending, will retry", prof.Identifier)
|
|
}
|
|
level.Debug(a.Log).Log(
|
|
"task", "runPostDEPReleaseDevice",
|
|
"msg", fmt.Sprintf("profile %s has been deployed", prof.Identifier),
|
|
)
|
|
}
|
|
|
|
// release the device
|
|
a.Log.Log("info", "releasing device, all DEP enrollment commands and profiles have completed", "host_uuid", args.HostUUID)
|
|
if err := a.Commander.DeviceConfigured(ctx, args.HostUUID, uuid.NewString()); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "failed to enqueue DeviceConfigured command")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) (string, error) {
|
|
cmdUUID := uuid.New().String()
|
|
if err := a.Commander.InstallEnterpriseApplication(ctx, []string{hostUUID}, cmdUUID, apple_mdm.FleetdPublicManifestURL); err != nil {
|
|
return "", err
|
|
}
|
|
a.Log.Log("info", "sent command to install fleetd", "host_uuid", hostUUID)
|
|
return cmdUUID, nil
|
|
}
|
|
|
|
func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string, teamID *uint) (string, error) {
|
|
// GetMDMAppleBootstrapPackageMeta expects team id 0 for no team
|
|
var tmID uint
|
|
if teamID != nil {
|
|
tmID = *teamID
|
|
}
|
|
meta, err := a.Datastore.GetMDMAppleBootstrapPackageMeta(ctx, tmID)
|
|
if err != nil {
|
|
var nfe fleet.NotFoundError
|
|
if errors.As(err, &nfe) {
|
|
a.Log.Log("info", "unable to find a bootstrap package for DEP enrolled device, skipping installation", "host_uuid", hostUUID)
|
|
return "", nil
|
|
}
|
|
|
|
return "", err
|
|
}
|
|
|
|
appCfg, err := a.Datastore.AppConfig(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
url, err := meta.URL(appCfg.ServerSettings.ServerURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
manifest := appmanifest.NewFromSha(meta.Sha256, url)
|
|
cmdUUID := uuid.New().String()
|
|
err = a.Commander.InstallEnterpriseApplicationWithEmbeddedManifest(ctx, []string{hostUUID}, cmdUUID, manifest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = a.Datastore.RecordHostBootstrapPackage(ctx, cmdUUID, hostUUID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
a.Log.Log("info", "sent command to install bootstrap package", "host_uuid", hostUUID)
|
|
return cmdUUID, nil
|
|
}
|
|
|
|
// QueueAppleMDMJob queues a apple_mdm job for one of the supported tasks, to
|
|
// be processed asynchronously via the worker.
|
|
func QueueAppleMDMJob(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
logger kitlog.Logger,
|
|
task AppleMDMTask,
|
|
hostUUID string,
|
|
teamID *uint,
|
|
enrollReference string,
|
|
enrollmentCommandUUIDs ...string,
|
|
) error {
|
|
attrs := []interface{}{
|
|
"enabled", "true",
|
|
appleMDMJobName, task,
|
|
"host_uuid", hostUUID,
|
|
"with_enroll_reference", enrollReference != "",
|
|
}
|
|
if teamID != nil {
|
|
attrs = append(attrs, "team_id", *teamID)
|
|
}
|
|
if len(enrollmentCommandUUIDs) > 0 {
|
|
attrs = append(attrs, "enrollment_commands", enrollmentCommandUUIDs)
|
|
}
|
|
level.Info(logger).Log(attrs...)
|
|
|
|
args := &appleMDMArgs{
|
|
Task: task,
|
|
HostUUID: hostUUID,
|
|
TeamID: teamID,
|
|
EnrollReference: enrollReference,
|
|
EnrollmentCommands: enrollmentCommandUUIDs,
|
|
}
|
|
|
|
// the release device task is always added with a delay
|
|
var delay time.Duration
|
|
if task == AppleMDMPostDEPReleaseDeviceTask {
|
|
delay = 30 * time.Second
|
|
}
|
|
job, err := QueueJobWithDelay(ctx, ds, appleMDMJobName, args, delay)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "queueing job")
|
|
}
|
|
level.Debug(logger).Log("job_id", job.ID)
|
|
return nil
|
|
}
|