From eb2b4e66078e95e916dc59fa9f1074a01adeb43d Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Sat, 13 Nov 2021 13:39:52 -0800 Subject: [PATCH] Declarative Management (#24) --- cmd/nanomdm/main.go | 9 ++++- docs/operations-guide.md | 6 +++ mdm/checkin.go | 12 ++++++ service/certauth/service.go | 7 ++++ service/dump/dump.go | 14 +++++++ service/microwebhook/service.go | 14 +++++++ service/multi/multi.go | 10 +++++ service/nanomdm/dm.go | 69 +++++++++++++++++++++++++++++++++ service/nanomdm/service.go | 28 +++++++++++++ service/request.go | 5 +++ service/service.go | 7 ++++ 11 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 service/nanomdm/dm.go diff --git a/cmd/nanomdm/main.go b/cmd/nanomdm/main.go index cbc4b35..3dd0504 100644 --- a/cmd/nanomdm/main.go +++ b/cmd/nanomdm/main.go @@ -57,6 +57,7 @@ func main() { flCheckin = flag.Bool("checkin", false, "enable separate HTTP endpoint for MDM check-ins") flMigration = flag.Bool("migration", false, "HTTP endpoint for enrollment migrations") flRetro = flag.Bool("retro", false, "Allow retroactive certificate-authorization association") + flDMURLPfx = flag.String("dm", "", "URL to send Declarative Management requests to") ) flag.Parse() @@ -89,7 +90,13 @@ func main() { } // create 'core' MDM service - nano := nanomdm.New(mdmStorage, nanomdm.WithLogger(logger.With("service", "nanomdm"))) + nanoOpts := []nanomdm.Option{nanomdm.WithLogger(logger.With("service", "nanomdm"))} + if *flDMURLPfx != "" { + logger.Debug("msg", "declarative management setup", "url", *flDMURLPfx) + dm := nanomdm.NewDeclarativeManagementHTTPCaller(*flDMURLPfx) + nanoOpts = append(nanoOpts, nanomdm.WithDeclarativeManagement(dm)) + } + nano := nanomdm.New(mdmStorage, nanoOpts...) mux := http.NewServeMux() diff --git a/docs/operations-guide.md b/docs/operations-guide.md index 71b7e90..324121b 100644 --- a/docs/operations-guide.md +++ b/docs/operations-guide.md @@ -77,6 +77,12 @@ Specifies the listen address (interface & port number) for the server to listen This switch disables MDM client capability. This effecitvely turns this running instance into "API-only" mode. It is not compatible with having an empty `-api` switch. +### -dm + +* URL to send Declarative Management requests to + +Specifies the "base" URL to send Declarative Management requests to. The full URL is constructed from this base URL appended with the type of Declarative Management ["Endpoint" request](https://developer.apple.com/documentation/devicemanagement/declarativemanagementrequest?language=objc) such as "status" or "declaration-items". Each HTTP request includes the NanoMDM enrollment ID as the HTTP header "X-Enrollment-ID". See [this blog post](https://micromdm.io/blog/wwdc21-declarative-management/) for more details. + ### -migration * HTTP endpoint for enrollment migrations diff --git a/mdm/checkin.go b/mdm/checkin.go index 975c77d..425756f 100644 --- a/mdm/checkin.go +++ b/mdm/checkin.go @@ -93,6 +93,16 @@ type GetBootstrapToken struct { Raw []byte `plist:"-"` // Original XML plist } +// DeclarativeManagement is a representation of a "DeclarativeManagement" check-in message type. +// See https://developer.apple.com/documentation/devicemanagement/declarativemanagementrequest +type DeclarativeManagement struct { + Enrollment + MessageType + Data []byte + Endpoint string + Raw []byte `plist:"-"` // Original XML plist +} + // newCheckinMessageForType returns a pointer to a check-in struct for MessageType t func newCheckinMessageForType(t string, raw []byte) interface{} { switch t { @@ -108,6 +118,8 @@ func newCheckinMessageForType(t string, raw []byte) interface{} { return &GetBootstrapToken{Raw: raw} case "UserAuthenticate": return &UserAuthenticate{Raw: raw} + case "DeclarativeManagement": + return &DeclarativeManagement{Raw: raw} default: return nil } diff --git a/service/certauth/service.go b/service/certauth/service.go index 5d32d00..fd0af1a 100644 --- a/service/certauth/service.go +++ b/service/certauth/service.go @@ -46,6 +46,13 @@ func (s *CertAuth) GetBootstrapToken(r *mdm.Request, m *mdm.GetBootstrapToken) ( return s.next.GetBootstrapToken(r, m) } +func (s *CertAuth) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeManagement) ([]byte, error) { + if err := s.validateOrAssociateForExistingEnrollment(r, &m.Enrollment); err != nil { + return nil, err + } + return s.next.DeclarativeManagement(r, m) +} + func (s *CertAuth) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { if err := s.validateOrAssociateForExistingEnrollment(r, &results.Enrollment); err != nil { return nil, err diff --git a/service/dump/dump.go b/service/dump/dump.go index c4de1b1..9708ba0 100644 --- a/service/dump/dump.go +++ b/service/dump/dump.go @@ -17,6 +17,7 @@ type Dumper struct { cmd bool bst bool usr bool + dm bool } // New creates a new dumper service middleware. @@ -27,6 +28,7 @@ func New(next service.CheckinAndCommandService, file *os.File) *Dumper { cmd: true, bst: true, usr: true, + dm: true, } } @@ -76,3 +78,15 @@ func (svc *Dumper) CommandAndReportResults(r *mdm.Request, results *mdm.CommandR } return cmd, err } + +func (svc *Dumper) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeManagement) ([]byte, error) { + svc.file.Write(m.Raw) + if len(m.Data) > 0 { + svc.file.Write(m.Data) + } + respBytes, err := svc.next.DeclarativeManagement(r, m) + if svc.dm && err != nil { + svc.file.Write(respBytes) + } + return respBytes, err +} diff --git a/service/microwebhook/service.go b/service/microwebhook/service.go index 6668f50..7ca33d3 100644 --- a/service/microwebhook/service.go +++ b/service/microwebhook/service.go @@ -129,3 +129,17 @@ func (w *MicroWebhook) CommandAndReportResults(r *mdm.Request, results *mdm.Comm } return nil, postWebhookEvent(r.Context, w.client, w.url, ev) } + +func (w *MicroWebhook) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeManagement) ([]byte, error) { + ev := &Event{ + Topic: "mdm.DeclarativeManagement", + CreatedAt: time.Now(), + CheckinEvent: &CheckinEvent{ + UDID: m.UDID, + EnrollmentID: m.EnrollmentID, + RawPayload: m.Raw, + Params: r.Params, + }, + } + return nil, postWebhookEvent(r.Context, w.client, w.url, ev) +} diff --git a/service/multi/multi.go b/service/multi/multi.go index 2f174cc..521f7fc 100644 --- a/service/multi/multi.go +++ b/service/multi/multi.go @@ -110,6 +110,16 @@ func (ms *MultiService) GetBootstrapToken(r *mdm.Request, m *mdm.GetBootstrapTok return bsToken, err } +func (ms *MultiService) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeManagement) ([]byte, error) { + retBytes, err := ms.svcs[0].DeclarativeManagement(r, m) + rc := ms.RequestWithContext(r) + ms.runOthers(func(svc service.CheckinAndCommandService) error { + _, err := svc.DeclarativeManagement(rc, m) + return err + }) + return retBytes, err +} + func (ms *MultiService) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { cmd, err := ms.svcs[0].CommandAndReportResults(r, results) rc := ms.RequestWithContext(r) diff --git a/service/nanomdm/dm.go b/service/nanomdm/dm.go new file mode 100644 index 0000000..a88232d --- /dev/null +++ b/service/nanomdm/dm.go @@ -0,0 +1,69 @@ +package nanomdm + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + + "github.com/micromdm/nanomdm/mdm" + "github.com/micromdm/nanomdm/service" +) + +const enrollmentIDHeader = "X-Enrollment-ID" + +type DeclarativeManagementHTTPCaller struct { + urlPrefix string + client *http.Client +} + +// NewDeclarativeManagementHTTPCaller creates a new DeclarativeManagementHTTPCaller +func NewDeclarativeManagementHTTPCaller(urlPrefix string) *DeclarativeManagementHTTPCaller { + return &DeclarativeManagementHTTPCaller{ + urlPrefix: urlPrefix, + client: http.DefaultClient, + } +} + +// DeclarativeManagement calls out to an HTTP URL to handle the actual Declarative Management protocol +func (c *DeclarativeManagementHTTPCaller) DeclarativeManagement(r *mdm.Request, message *mdm.DeclarativeManagement) ([]byte, error) { + if c.urlPrefix == "" { + return nil, errors.New("missing URL") + } + u, err := url.Parse(c.urlPrefix) + if err != nil { + return nil, err + } + u.Path = path.Join(u.Path, message.Endpoint) + method := http.MethodGet + if len(message.Data) > 0 { + method = http.MethodPut + } + req, err := http.NewRequestWithContext(r.Context, method, u.String(), bytes.NewBuffer(message.Data)) + if err != nil { + return nil, err + } + req.Header.Set(enrollmentIDHeader, r.ID) + if len(message.Data) > 0 { + req.Header.Set("Content-Type", "application/json") + } + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return bodyBytes, service.NewHTTPStatusError( + resp.StatusCode, + fmt.Errorf("unexpected HTTP status: %s", resp.Status), + ) + } + return bodyBytes, nil +} diff --git a/service/nanomdm/service.go b/service/nanomdm/service.go index a0d0462..b66418c 100644 --- a/service/nanomdm/service.go +++ b/service/nanomdm/service.go @@ -2,6 +2,7 @@ package nanomdm import ( + "errors" "fmt" "net/http" @@ -24,6 +25,9 @@ type Service struct { // https://developer.apple.com/documentation/devicemanagement/userauthenticate sendEmptyDigestChallenge bool storeRejectedUserAuth bool + + // Declarative Management + dm service.DeclarativeManagement } // normalize generates enrollment IDs that are used by other @@ -61,6 +65,12 @@ func WithLogger(logger log.Logger) Option { } } +func WithDeclarativeManagement(dm service.DeclarativeManagement) Option { + return func(s *Service) { + s.dm = dm + } +} + // New returns a new NanoMDM main service. func New(store storage.ServiceStore, opts ...Option) *Service { nanomdm := &Service{ @@ -204,6 +214,24 @@ func (s *Service) GetBootstrapToken(r *mdm.Request, message *mdm.GetBootstrapTok return s.store.RetrieveBootstrapToken(r, message) } +// DeclarativeManagement Check-in message implementation. Calls out to +// the service's DM handler (if configured). +func (s *Service) DeclarativeManagement(r *mdm.Request, message *mdm.DeclarativeManagement) ([]byte, error) { + if err := s.updateEnrollID(r, &message.Enrollment); err != nil { + return nil, err + } + s.logger.Info( + "msg", "DeclarativeManagement", + "id", r.ID, + "type", r.Type, + "endpoint", message.Endpoint, + ) + if s.dm == nil { + return nil, errors.New("no Declarative Management handler") + } + return s.dm.DeclarativeManagement(r, message) +} + // CommandAndReportResults command report and next-command request implementation. func (s *Service) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { if err := s.updateEnrollID(r, &results.Enrollment); err != nil { diff --git a/service/request.go b/service/request.go index 4726e73..332ba6f 100644 --- a/service/request.go +++ b/service/request.go @@ -69,6 +69,11 @@ func CheckinRequest(svc Checkin, r *mdm.Request, bodyBytes []byte) ([]byte, erro if err != nil { err = fmt.Errorf("marshal bootstrap token: %w", err) } + case *mdm.DeclarativeManagement: + respBytes, err = svc.DeclarativeManagement(r, m) + if err != nil { + err = fmt.Errorf("declarativemanagement service: %w", err) + } default: return nil, errors.New("unhandled check-in request type") } diff --git a/service/service.go b/service/service.go index bbb9550..8c57ef9 100644 --- a/service/service.go +++ b/service/service.go @@ -5,6 +5,12 @@ import ( "github.com/micromdm/nanomdm/mdm" ) +// DeclarativeManagement is the interface for handling the Apple +// Declarative Management protocol via MDM "v1." +type DeclarativeManagement interface { + DeclarativeManagement(*mdm.Request, *mdm.DeclarativeManagement) ([]byte, error) +} + // Checkin represents the various check-in requests. // See https://developer.apple.com/documentation/devicemanagement/check-in type Checkin interface { @@ -14,6 +20,7 @@ type Checkin interface { SetBootstrapToken(*mdm.Request, *mdm.SetBootstrapToken) error GetBootstrapToken(*mdm.Request, *mdm.GetBootstrapToken) (*mdm.BootstrapToken, error) UserAuthenticate(*mdm.Request, *mdm.UserAuthenticate) ([]byte, error) + DeclarativeManagement } // CommandAndReportResults represents the command report and next-command request.