From c4a1cd97ef206abc37299e9a6e952d4dfaa8f224 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Wed, 12 Jul 2023 00:07:42 +0100 Subject: [PATCH] Add endpoint for inspecting the MDM command queue (#895) --- CHANGELOG.md | 6 ++-- docs/user-guide/api-and-webhooks.md | 24 +++++++++++++ mdm/service.go | 7 ++++ platform/command/server.go | 10 ++++++ platform/command/service.go | 2 ++ platform/command/view.go | 54 +++++++++++++++++++++++++++++ platform/queue/inmem/inmem.go | 24 +++++++++++++ platform/queue/queue.go | 27 +++++++++++++++ tools/api/inspect_queue | 5 +++ 9 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 platform/command/view.go create mode 100755 tools/api/inspect_queue diff --git a/CHANGELOG.md b/CHANGELOG.md index 06182075..3d149b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ - Add `-log-time` flag to include timestamps in log messages (#890) - Add `-device-signature-skew` flag to allow configuring clock skew when verifying device signatures (#887) -- Tidy code for Go 1.20, and update Go version for Docker and CI +- Tidy code for Go 1.20 and update Go version for Docker and CI (#902) +- Add support for inspecting the MDM command queue (#895) + - See the [docs](https://github.com/micromdm/micromdm/blob/main/docs/user-guide/api-and-webhooks.md#inspecting-the-command-queue) for how to use - Project dependency updates (#888, #889, #900) -Thanks to our contributors: @jamesez, @korylprince +Thanks to our contributors: @grahamgilbert, @jamesez, @korylprince ## [v1.11.0](https://github.com/micromdm/micromdm/compare/v1.10.1...v1.11.0) diff --git a/docs/user-guide/api-and-webhooks.md b/docs/user-guide/api-and-webhooks.md index 0ee993a1..bc439f92 100644 --- a/docs/user-guide/api-and-webhooks.md +++ b/docs/user-guide/api-and-webhooks.md @@ -294,3 +294,27 @@ Authorization: Basic bWljcm9tZG06c3VwZXJzZWNyZXQ= A helper script is also available at `./tools/api/clear_queue`: `$ ./clear_queue 55693EB3-DF03-5FD1-9263-F7CDB8AD7FFD` + +# Inspecting the Command Queue + +[PR #895](https://github.com/micromdm/micromdm/pull/895) added support for inspecting the command queue, which can be useful when diagnosing issues with commands. + +Assuming the example command from [Schedule Raw Commands with the API](#schedule-raw-commands-with-the-api) is in the command queue, inspecting the queue looks like: + +``` +GET /v1/commands/55693EB3-DF03-5FD1-9263-F7CDB8AD7FFD HTTP/1.1 +Authorization: Basic bWljcm9tZG06c3VwZXJzZWNyZXQ= + +{ + "commands": [ + { + "uuid": "0001_ProfileList", + "payload": "" + } + ] +} +``` + +A helper script is also available at `./tools/api/inspect_queue`: + +`$ ./inspect_queue 55693EB3-DF03-5FD1-9263-F7CDB8AD7FFD` diff --git a/mdm/service.go b/mdm/service.go index f7687afd..48d726a1 100644 --- a/mdm/service.go +++ b/mdm/service.go @@ -36,10 +36,17 @@ type BootstrapTokenRetriever interface { GetBootstrapToken(ctx context.Context, udid string) ([]byte, error) } +// Command is an MDM Command +type Command struct { + UUID string `json:"uuid"` + Payload []byte `json:"payload"` +} + // Queue is an MDM Command Queue. type Queue interface { Next(context.Context, Response) ([]byte, error) Clear(context.Context, CheckinEvent) error + ViewQueue(context.Context, CheckinEvent) ([]*Command, error) } type MDMService struct { diff --git a/platform/command/server.go b/platform/command/server.go index 74c6cb68..02e705ad 100644 --- a/platform/command/server.go +++ b/platform/command/server.go @@ -11,6 +11,7 @@ type Endpoints struct { NewCommandEndpoint endpoint.Endpoint NewRawCommandEndpoint endpoint.Endpoint ClearQueueEndpoint endpoint.Endpoint + ViewQueueEndpoint endpoint.Endpoint } func MakeServerEndpoints(s Service, outer endpoint.Middleware, others ...endpoint.Middleware) Endpoints { @@ -18,10 +19,19 @@ func MakeServerEndpoints(s Service, outer endpoint.Middleware, others ...endpoin NewCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewCommandEndpoint(s)), NewRawCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewRawCommandEndpoint(s)), ClearQueueEndpoint: endpoint.Chain(outer, others...)(MakeClearQueueEndpoint(s)), + ViewQueueEndpoint: endpoint.Chain(outer, others...)(MakeViewQueueEndpoint(s)), } } func RegisterHTTPHandlers(r *mux.Router, e Endpoints, options ...httptransport.ServerOption) { + // GET /v1/commands/udid View device queue. + r.Methods("GET").Path("/v1/commands/{udid}").Handler(httptransport.NewServer( + e.ViewQueueEndpoint, + decodeViewQueueRequest, + httputil.EncodeJSONResponse, + options..., + )) + // POST /v1/commands Add new MDM Command to device queue. r.Methods("POST").Path("/v1/commands").Handler(httptransport.NewServer( e.NewCommandEndpoint, diff --git a/platform/command/service.go b/platform/command/service.go index 8e2620a7..95e18c95 100644 --- a/platform/command/service.go +++ b/platform/command/service.go @@ -12,11 +12,13 @@ type Service interface { NewCommand(context.Context, *mdm.CommandRequest) (*mdm.CommandPayload, error) NewRawCommand(context.Context, *RawCommand) error ClearQueue(ctx context.Context, udid string) error + ViewQueue(ctx context.Context, udid string) ([]*mdmsvc.Command, error) } // Queue is an MDM Command Queue. type Queue interface { Clear(context.Context, mdmsvc.CheckinEvent) error + ViewQueue(context.Context, mdmsvc.CheckinEvent) ([]*mdmsvc.Command, error) } type CommandService struct { diff --git a/platform/command/view.go b/platform/command/view.go new file mode 100644 index 00000000..57c48839 --- /dev/null +++ b/platform/command/view.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "net/http" + + "github.com/go-kit/kit/endpoint" + "github.com/gorilla/mux" + "github.com/micromdm/micromdm/mdm" + "github.com/pkg/errors" +) + +func (svc *CommandService) ViewQueue(ctx context.Context, udid string) ([]*mdm.Command, error) { + commands, err := svc.queue.ViewQueue( + ctx, + mdm.CheckinEvent{Command: mdm.CheckinCommand{UDID: udid}}, + ) + if err != nil { + return nil, errors.Wrap(err, "clearing command queue") + } + return commands, nil +} + +type viewQueueRequest struct { + UDID string +} + +type viewQueueResponse struct { + Err error `json:"error,omitempty"` + Commands []*mdm.Command `json:"commands,omitempty"` +} + +func (r viewQueueResponse) Failed() error { return r.Err } +func (r viewQueueResponse) StatusCode() int { return http.StatusOK } + +func decodeViewQueueRequest(ctx context.Context, r *http.Request) (interface{}, error) { + return viewQueueRequest{UDID: mux.Vars(r)["udid"]}, nil +} + +// MakeViewQueueEndpoint creates an endpoint which views device queues. +func MakeViewQueueEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(viewQueueRequest) + if req.UDID == "" { + return viewQueueResponse{Err: errEmptyRequest}, nil + } + commands, err := svc.ViewQueue(ctx, req.UDID) + if err != nil { + return viewQueueResponse{Err: err}, nil + } + + return viewQueueResponse{Commands: commands}, nil + } +} diff --git a/platform/queue/inmem/inmem.go b/platform/queue/inmem/inmem.go index ee761161..31c57c86 100644 --- a/platform/queue/inmem/inmem.go +++ b/platform/queue/inmem/inmem.go @@ -121,6 +121,30 @@ func (q *QueueInMem) Clear(_ context.Context, event mdm.CheckinEvent) error { return nil } +// View returns the command queue for the device in event +func (q *QueueInMem) ViewQueue(_ context.Context, event mdm.CheckinEvent) ([]*mdm.Command, error) { + udid := event.Command.UDID + if event.Command.UserID != "" { + udid = event.Command.UserID + } + if event.Command.EnrollmentID != "" { + udid = event.Command.EnrollmentID + } + + l := q.getList(udid) + + cmds := make([]*mdm.Command, 0, l.Len()) + for item := l.Front(); item != nil; item = item.Next() { + cmd := item.Value.(*queuedCommand) + cmds = append(cmds, &mdm.Command{ + UUID: cmd.uuid, + Payload: cmd.payload, + }) + } + + return cmds, nil +} + func (q *QueueInMem) startPolling(pubsub pubsub.PublishSubscriber) error { events, err := pubsub.Subscribe(context.TODO(), "command-queue", command.CommandTopic) if err != nil { diff --git a/platform/queue/queue.go b/platform/queue/queue.go index 45913d4a..8f61f86a 100644 --- a/platform/queue/queue.go +++ b/platform/queue/queue.go @@ -54,6 +54,33 @@ func (db *Store) Next(ctx context.Context, resp mdm.Response) ([]byte, error) { return cmd.Payload, nil } +func (db *Store) ViewQueue(ctx context.Context, event mdm.CheckinEvent) ([]*mdm.Command, error) { + udid := event.Command.UDID + if event.Command.UserID != "" { + udid = event.Command.UserID + } + if event.Command.EnrollmentID != "" { + udid = event.Command.EnrollmentID + } + + dc, err := db.DeviceCommand(udid) + if isNotFound(err) { + return nil, nil + } else if err != nil { + return nil, errors.Wrapf(err, "get device commands, udid: %s", udid) + } + + cmds := make([]*mdm.Command, len(dc.Commands)) + for idx, cmd := range dc.Commands { + cmds[idx] = &mdm.Command{ + UUID: cmd.UUID, + Payload: cmd.Payload, + } + } + + return cmds, nil +} + func (db *Store) Clear(ctx context.Context, event mdm.CheckinEvent) error { udid := event.Command.UDID if event.Command.UserID != "" { diff --git a/tools/api/inspect_queue b/tools/api/inspect_queue new file mode 100755 index 00000000..44a84950 --- /dev/null +++ b/tools/api/inspect_queue @@ -0,0 +1,5 @@ +#!/bin/bash +source $MICROMDM_ENV_PATH +endpoint="v1/commands/$1" + +curl $CURL_OPTS -K <(cat <<< "-u micromdm:$API_TOKEN") -X GET "$SERVER_URL/$endpoint"