Skip to content

Commit

Permalink
Declarative Management (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessepeterson authored Nov 13, 2021
1 parent 4990535 commit eb2b4e6
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 1 deletion.
9 changes: 8 additions & 1 deletion cmd/nanomdm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions docs/operations-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions mdm/checkin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions service/certauth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions service/dump/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Dumper struct {
cmd bool
bst bool
usr bool
dm bool
}

// New creates a new dumper service middleware.
Expand All @@ -27,6 +28,7 @@ func New(next service.CheckinAndCommandService, file *os.File) *Dumper {
cmd: true,
bst: true,
usr: true,
dm: true,
}
}

Expand Down Expand Up @@ -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
}
14 changes: 14 additions & 0 deletions service/microwebhook/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
10 changes: 10 additions & 0 deletions service/multi/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
69 changes: 69 additions & 0 deletions service/nanomdm/dm.go
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 28 additions & 0 deletions service/nanomdm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package nanomdm

import (
"errors"
"fmt"
"net/http"

Expand All @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions service/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
7 changes: 7 additions & 0 deletions service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down

0 comments on commit eb2b4e6

Please sign in to comment.