Files
micromdm/platform/device/worker.go

358 lines
11 KiB
Go

package device
import (
"context"
"time"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/mdm"
"github.com/micromdm/micromdm/platform/dep/sync"
"github.com/micromdm/micromdm/platform/pubsub"
)
type DeviceWorkerStore interface {
Save(ctx context.Context, d *Device) error
DeviceByUDID(ctx context.Context, udid string) (*Device, error)
DeviceBySerial(ctx context.Context, serial string) (*Device, error)
}
type Worker struct {
db DeviceWorkerStore
ps pubsub.PublishSubscriber
logger log.Logger
}
func NewWorker(db DeviceWorkerStore, ps pubsub.PublishSubscriber, logger log.Logger) *Worker {
return &Worker{
db: db,
ps: ps,
logger: logger,
}
}
func (w *Worker) Run(ctx context.Context) error {
const subscription = "devices_worker"
authenticateEvents, err := w.ps.Subscribe(ctx, subscription, mdm.AuthenticateTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, mdm.AuthenticateTopic)
}
tokenUpdateEvents, err := w.ps.Subscribe(ctx, subscription, mdm.TokenUpdateTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, mdm.TokenUpdateTopic)
}
getBootstrapTokenEvents, err := w.ps.Subscribe(ctx, subscription, mdm.GetBootstrapTokenTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, mdm.GetBootstrapTokenTopic)
}
setBootstrapTokenEvents, err := w.ps.Subscribe(ctx, subscription, mdm.SetBootstrapTokenTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, mdm.SetBootstrapTokenTopic)
}
checkoutEvents, err := w.ps.Subscribe(ctx, subscription, mdm.CheckoutTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, mdm.CheckoutTopic)
}
depSyncEvents, err := w.ps.Subscribe(ctx, subscription, sync.SyncTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, sync.SyncTopic)
}
connectEvents, err := w.ps.Subscribe(ctx, subscription, mdm.ConnectTopic)
if err != nil {
return errors.Wrapf(err, "subscribing %s to %s", subscription, mdm.ConnectTopic)
}
for {
var err error
select {
case <-ctx.Done():
return ctx.Err()
case ev := <-authenticateEvents:
err = w.updateFromAuthenticate(ctx, ev.Message)
case ev := <-tokenUpdateEvents:
err = w.updateFromTokenUpdate(ctx, ev.Message)
case ev := <-getBootstrapTokenEvents:
err = w.updateFromGetBootstrapToken(ctx, ev.Message)
case ev := <-setBootstrapTokenEvents:
err = w.updateFromSetBootstrapToken(ctx, ev.Message)
case ev := <-checkoutEvents:
err = w.updateFromCheckout(ctx, ev.Message)
case ev := <-depSyncEvents:
err = w.updateFromDEPSync(ctx, ev.Message)
case ev := <-connectEvents:
err = w.updateFromAcknowledge(ctx, ev.Message)
}
if err != nil {
level.Info(w.logger).Log(
"msg", "update device from event",
"err", err,
)
continue
}
}
}
func (w *Worker) updateFromDEPSync(ctx context.Context, message []byte) error {
var ev sync.Event
if err := sync.UnmarshalEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal depsync event")
}
level.Debug(w.logger).Log(
"msg", "updating devices from DEP",
"device_count", len(ev.Devices),
)
for _, dd := range ev.Devices {
dev, err := getOrCreateDeviceBySerial(ctx, w.db, dd.SerialNumber)
if err != nil {
return errors.Wrap(err, "get device by serial")
}
notSeenBefore := dev.UUID == ""
logEvent := dd.OpType == "deleted" ||
(notSeenBefore && dd.OpType == "added") ||
(!notSeenBefore && dd.OpType == "modified")
if logEvent {
level.Debug(w.logger).Log(
"msg", "updating devices from dep sync",
"op_type", dd.OpType,
"serial", dd.SerialNumber,
"previously_known", !notSeenBefore,
)
}
if dev.UUID == "" {
dev.UUID = uuid.New().String()
}
dev.SerialNumber = dd.SerialNumber
dev.Model = dd.Model
dev.Description = dd.Description
dev.Color = dd.Color
dev.AssetTag = dd.AssetTag
dev.DEPProfileStatus = DEPProfileStatus(dd.ProfileStatus)
dev.DEPProfileUUID = dd.ProfileUUID
dev.DEPProfileAssignTime = dd.ProfileAssignTime
dev.DEPProfileAssignedDate = dd.DeviceAssignedDate
dev.DEPProfileAssignedBy = dd.DeviceAssignedBy
if err := w.db.Save(ctx, dev); err != nil {
return errors.Wrap(err, "save device %s from DEP sync")
}
}
return nil
}
func (w *Worker) updateFromAcknowledge(ctx context.Context, message []byte) error {
var ev mdm.AcknowledgeEvent
if err := mdm.UnmarshalAcknowledgeEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal acknowledge event")
}
if ev.Response.EnrollmentID != nil {
return nil
}
dev, err := w.db.DeviceByUDID(ctx, ev.Response.UDID)
if err != nil {
return errors.Wrapf(err, "retrieve device with udid %s", ev.Response.UDID)
}
dev.LastSeen = time.Now()
err = w.db.Save(ctx, dev)
return errors.Wrapf(err, "saving updated device for acknowledge event")
}
func (w *Worker) updateFromCheckout(ctx context.Context, message []byte) error {
var ev mdm.CheckinEvent
if err := mdm.UnmarshalCheckinEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal checkin event")
}
if ev.Command.EnrollmentID != "" {
return nil
}
dev, err := w.db.DeviceByUDID(ctx, ev.Command.UDID)
if err != nil {
return errors.Wrapf(err, "retrieve device with udid %s", ev.Command.UDID)
}
dev.Enrolled = false
dev.LastSeen = time.Now()
err = w.db.Save(ctx, dev)
return errors.Wrapf(err, "saving updated device for checkout event")
}
func (w *Worker) updateFromGetBootstrapToken(ctx context.Context, message []byte) error {
var ev mdm.CheckinEvent
if err := mdm.UnmarshalCheckinEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal checkin event")
}
dev, err := w.db.DeviceByUDID(ctx, ev.Command.UDID)
if err != nil {
return errors.Wrapf(err, "retrieve device with udid %s", ev.Command.UDID)
}
dev.AwaitingConfiguration = ev.Command.AwaitingConfiguration
dev.LastSeen = time.Now()
err = w.db.Save(ctx, dev)
return errors.Wrapf(err, "saving updated device for GetBootstrapToken event")
}
func (w *Worker) updateFromSetBootstrapToken(ctx context.Context, message []byte) error {
var ev mdm.CheckinEvent
if err := mdm.UnmarshalCheckinEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal checkin event")
}
dev, err := w.db.DeviceByUDID(ctx, ev.Command.UDID)
if err != nil {
return errors.Wrapf(err, "retrieve device with udid %s", ev.Command.UDID)
}
dev.BootstrapToken = ev.Command.BootstrapToken
dev.AwaitingConfiguration = ev.Command.AwaitingConfiguration
dev.LastSeen = time.Now()
err = w.db.Save(ctx, dev)
return errors.Wrapf(err, "saving updated device for SetBootstrapToken event")
}
func (w *Worker) updateFromTokenUpdate(ctx context.Context, message []byte) error {
var ev mdm.CheckinEvent
if err := mdm.UnmarshalCheckinEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal checkin event")
}
// do not process managed user, or user enrollment checkin events while updating device records.
if ev.Command.UserID != "" || ev.Command.EnrollmentID != "" {
return nil
}
dev, err := w.db.DeviceByUDID(ctx, ev.Command.UDID)
if err != nil {
return errors.Wrapf(err, "retrieve device with udid %s", ev.Command.UDID)
}
dev.Token = ev.Command.Token.String()
dev.PushMagic = ev.Command.PushMagic
dev.UnlockToken = ev.Command.UnlockToken.String()
dev.AwaitingConfiguration = ev.Command.AwaitingConfiguration
dev.LastSeen = time.Now()
// first TokenUpdate event will have the enrollment status set to false.
newlyEnrolled := !dev.Enrolled
dev.Enrolled = true
if err := w.db.Save(ctx, dev); err != nil {
return errors.Wrapf(err, "saving updated device for Token event udid=%s", ev.Command.UDID)
}
if newlyEnrolled {
// notify subscribers of a successful enrollment
// TODO: The enrollment topic needs a custom event.
err = w.ps.Publish(ctx, DeviceEnrolledTopic, message)
return errors.Wrap(err, "publishing new enrollment message")
}
return nil
}
func (w *Worker) updateFromAuthenticate(ctx context.Context, message []byte) error {
var ev mdm.CheckinEvent
if err := mdm.UnmarshalCheckinEvent(message, &ev); err != nil {
return errors.Wrap(err, "unmarshal checkin event")
}
if ev.Command.EnrollmentID != "" {
return nil
}
device, reenrolling, err := getOrCreateDevice(ctx, w.db, ev.Command.SerialNumber, ev.Command.UDID)
if err != nil {
return errors.Wrap(err, "get device for authenticate event")
}
if reenrolling {
level.Debug(w.logger).Log(
"msg", "re-enrolling device",
"serial", ev.Command.SerialNumber,
)
} else {
level.Debug(w.logger).Log(
"msg", "enrolling new device",
"serial", ev.Command.SerialNumber,
)
}
if device.UUID == "" {
device.UUID = uuid.New().String()
}
device.UDID = ev.Command.UDID
device.OSVersion = ev.Command.OSVersion
device.BuildVersion = ev.Command.BuildVersion
device.ProductName = ev.Command.ProductName
device.SerialNumber = ev.Command.SerialNumber
device.IMEI = ev.Command.IMEI
device.MEID = ev.Command.MEID
device.DeviceName = ev.Command.DeviceName
device.Model = ev.Command.Model
device.ModelName = ev.Command.ModelName
device.LastSeen = time.Now()
err = w.db.Save(ctx, device)
return errors.Wrapf(err, "saving updated device for authenticate event")
}
func getOrCreateDevice(ctx context.Context, db DeviceWorkerStore, serial, udid string) (dev *Device, reenrolling bool, err error) {
if udid != "" {
// first try to fetch a device by UDID.
// If the device was previously enrolled it will exist.
// In case the device is known, set the enrolled status to false before returning.
byUDID, err := db.DeviceByUDID(ctx, udid)
if err == nil {
byUDID.Enrolled = false
return byUDID, true, nil
}
if err != nil && !isNotFound(err) {
return nil, false, errors.Wrapf(err, "retrieve device with udid %s and serial %s", udid, serial)
}
}
// next try to find the device by serial. If found, it's a DEP device, which contains only the
// serials but not a udid.
dev, err = getOrCreateDeviceBySerial(ctx, db, serial)
return dev, false, err
}
func getOrCreateDeviceBySerial(ctx context.Context, db DeviceWorkerStore, serial string) (*Device, error) {
bySerial, err := db.DeviceBySerial(ctx, serial)
if err == nil && bySerial != nil {
return bySerial, nil
}
if err != nil && !isNotFound(err) {
return nil, errors.Wrapf(err, "retrieve device with serial number %s", serial)
}
dev := new(Device)
return dev, nil
}
func isNotFound(err error) bool {
err = errors.Cause(err)
type notFoundErr interface {
error
NotFound() bool
}
e, ok := err.(notFoundErr)
return ok && e.NotFound()
}