Add raw command and clear device queue endpoints (#864)

This commit is contained in:
Kory Prince
2023-04-23 16:47:42 -05:00
committed by GitHub
parent a5cf4e2742
commit 3d0746f3e3
13 changed files with 427 additions and 9 deletions

47
platform/command/clear.go Normal file
View File

@@ -0,0 +1,47 @@
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) ClearQueue(ctx context.Context, udid string) error {
if err := svc.queue.Clear(ctx, mdm.CheckinEvent{Command: mdm.CheckinCommand{UDID: udid}}); err != nil {
return errors.Wrap(err, "clearing command queue")
}
return nil
}
type clearRequest struct {
UDID string
}
type clearResponse struct {
Err error `json:"error,omitempty"`
}
func (r clearResponse) Failed() error { return r.Err }
func (r clearResponse) StatusCode() int { return http.StatusOK }
func decodeClearRequest(ctx context.Context, r *http.Request) (interface{}, error) {
return clearRequest{UDID: mux.Vars(r)["udid"]}, nil
}
// MakeClearQueueEndpoint creates an endpoint which clears device queues.
func MakeClearQueueEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(clearRequest)
if req.UDID == "" {
return clearResponse{Err: errEmptyRequest}, nil
}
if err := svc.ClearQueue(ctx, req.UDID); err != nil {
return clearResponse{Err: err}, nil
}
return clearResponse{}, nil
}
}

View File

@@ -61,3 +61,45 @@ func UnmarshalEvent(data []byte, e *Event) error {
e.Payload = &payload
return nil
}
type RawEvent struct {
CommandUUID string
Time time.Time
DeviceUDID string
Payload []byte
}
// NewRawEvent returns a RawEvent with the current time.
func NewRawEvent(cmd *RawCommand) *RawEvent {
event := RawEvent{
CommandUUID: cmd.CommandUUID,
Time: time.Now().UTC(),
DeviceUDID: cmd.UDID,
Payload: cmd.Raw,
}
return &event
}
// MarshalRawEvent serializes a RawEvent to a protocol buffer wire format.
func MarshalRawEvent(e *RawEvent) ([]byte, error) {
return proto.Marshal(&commandproto.Event{
Id: e.CommandUUID, // Id isn't used anywhere, so it's repurposed for CommandUUID
Time: e.Time.UnixNano(),
DeviceUdid: e.DeviceUDID,
PayloadBytes: e.Payload,
})
}
// UnmarshalRawEvent parses a protocol buffer representation of data into
// the RawEvent.
func UnmarshalRawEvent(data []byte, e *RawEvent) error {
var pb commandproto.Event
if err := proto.Unmarshal(data, &pb); err != nil {
return errors.Wrap(err, "unmarshal pb Event")
}
e.CommandUUID = pb.Id
e.Time = time.Unix(0, pb.Time).UTC()
e.DeviceUDID = pb.DeviceUdid
e.Payload = pb.PayloadBytes
return nil
}

View File

@@ -0,0 +1,49 @@
package command_test
import (
"reflect"
"testing"
"github.com/micromdm/micromdm/platform/command"
)
const testRawCmd = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>ManagedOnly</key>
<true/>
<key>RequestType</key>
<string>ProfileList</string>
</dict>
<key>CommandUUID</key>
<string>0001_ProfileList</string>
</dict>
</plist>`
func TestRawEvent(t *testing.T) {
cmd := &command.RawCommand{
UDID: "1234",
CommandUUID: "0001_ProfileList",
Raw: []byte(testRawCmd),
}
cmd.Command.RequestType = "ProfileList"
ev := command.NewRawEvent(cmd)
buf, err := command.MarshalRawEvent(ev)
if err != nil {
t.Fatalf("could not marshal event: %v", err)
}
ev2 := new(command.RawEvent)
if err = command.UnmarshalRawEvent(buf, ev2); err != nil {
t.Fatalf("could not unmarshal event: %v", err)
}
if !reflect.DeepEqual(ev, ev2) {
t.Error("expected events to be equal")
}
}

View File

@@ -1,9 +1,14 @@
package command
import (
"bytes"
"fmt"
"io"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/gorilla/mux"
"github.com/groob/plist"
"github.com/pkg/errors"
"golang.org/x/net/context"
@@ -14,6 +19,9 @@ import (
const (
// CommandTopic is a PubSub topic that events are published to.
CommandTopic = "mdm.Command"
// RawCommandTopic is a PubSub topic that events are published to.
RawCommandTopic = "mdm.RawCommand"
)
func (svc *CommandService) NewCommand(ctx context.Context, request *mdm.CommandRequest) (*mdm.CommandPayload, error) {
@@ -35,6 +43,21 @@ func (svc *CommandService) NewCommand(ctx context.Context, request *mdm.CommandR
return payload, nil
}
func (svc *CommandService) NewRawCommand(ctx context.Context, cmd *RawCommand) error {
if cmd == nil {
return errors.New("empty RawCommand")
}
event := NewRawEvent(cmd)
msg, err := MarshalRawEvent(event)
if err != nil {
return errors.Wrap(err, "marshalling raw mdm command event")
}
if err := svc.publisher.Publish(context.TODO(), RawCommandTopic, msg); err != nil {
return errors.Wrapf(err, "publish raw mdm command on topic: %s", RawCommandTopic)
}
return nil
}
type newCommandRequest struct {
mdm.CommandRequest
}
@@ -69,3 +92,65 @@ func MakeNewCommandEndpoint(svc Service) endpoint.Endpoint {
return newCommandResponse{Payload: payload}, nil
}
}
type RawCommand struct {
UDID string `json:"udid" plist:"-"`
CommandUUID string `json:"command_uuid"`
Command struct {
RequestType string `json:"request_type"`
} `json:"command"`
Raw []byte `plist:"-" json:"payload"`
}
type newRawCommandRequest struct {
RawCommand
}
type newRawCommandResponse struct {
Payload *RawCommand `json:"payload,omitempty"`
Err error `json:"error,omitempty"`
}
func (r newRawCommandResponse) Failed() error { return r.Err }
func (r newRawCommandResponse) StatusCode() int { return http.StatusCreated }
func decodeNewRawCommandRequest(ctx context.Context, r *http.Request) (interface{}, error) {
udid, ok := mux.Vars(r)["udid"]
if !ok {
return nil, errors.New("empty udid")
}
payload, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("read payload body: %w", err)
}
// verify body is valid plist and parse CommandUUID and RequestType
var req newRawCommandRequest
if err := plist.NewXMLDecoder(bytes.NewBuffer(payload)).Decode(&req); err != nil {
return nil, fmt.Errorf("parse payload as plist: %w", err)
}
req.UDID = udid
req.Raw = payload
return req, nil
}
var errMalformedRequest = errors.New("request is malformed")
// MakeNewRawCommandEndpoint creates an endpoint which creates new raw MDM Commands.
func MakeNewRawCommandEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(newRawCommandRequest)
if req.UDID == "" {
return newRawCommandResponse{Err: errEmptyRequest}, nil
}
if req.CommandUUID == "" || req.Command.RequestType == "" {
return newRawCommandResponse{Err: errMalformedRequest}, nil
}
if err := svc.NewRawCommand(ctx, &req.RawCommand); err != nil {
return newRawCommandResponse{Err: err}, nil
}
return newRawCommandResponse{Payload: &req.RawCommand}, nil
}
}

View File

@@ -8,22 +8,41 @@ import (
)
type Endpoints struct {
NewCommandEndpoint endpoint.Endpoint
NewCommandEndpoint endpoint.Endpoint
NewRawCommandEndpoint endpoint.Endpoint
ClearQueueEndpoint endpoint.Endpoint
}
func MakeServerEndpoints(s Service, outer endpoint.Middleware, others ...endpoint.Middleware) Endpoints {
return Endpoints{
NewCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewCommandEndpoint(s)),
NewCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewCommandEndpoint(s)),
NewRawCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewRawCommandEndpoint(s)),
ClearQueueEndpoint: endpoint.Chain(outer, others...)(MakeClearQueueEndpoint(s)),
}
}
func RegisterHTTPHandlers(r *mux.Router, e Endpoints, options ...httptransport.ServerOption) {
// POST /v1/commands Add new MDM Command to device queue.
r.Methods("POST").Path("/v1/commands").Handler(httptransport.NewServer(
e.NewCommandEndpoint,
decodeNewCommandRequest,
httputil.EncodeJSONResponse,
options...,
))
// POST /v1/commands/udid Add new MDM Command with raw plist to device queue.
r.Methods("POST").Path("/v1/commands/{udid}").Handler(httptransport.NewServer(
e.NewRawCommandEndpoint,
decodeNewRawCommandRequest,
httputil.EncodeJSONResponse,
options...,
))
// DELETE /v1/commands/udid Clear device queue.
r.Methods("DELETE").Path("/v1/commands/{udid}").Handler(httptransport.NewServer(
e.ClearQueueEndpoint,
decodeClearRequest,
httputil.EncodeJSONResponse,
options...,
))
}

View File

@@ -2,6 +2,7 @@
package command
import (
mdmsvc "github.com/micromdm/micromdm/mdm"
"github.com/micromdm/micromdm/mdm/mdm"
"github.com/micromdm/micromdm/platform/pubsub"
"golang.org/x/net/context"
@@ -9,15 +10,24 @@ import (
type Service interface {
NewCommand(context.Context, *mdm.CommandRequest) (*mdm.CommandPayload, error)
NewRawCommand(context.Context, *RawCommand) error
ClearQueue(ctx context.Context, udid string) error
}
// Queue is an MDM Command Queue.
type Queue interface {
Clear(context.Context, mdmsvc.CheckinEvent) error
}
type CommandService struct {
publisher pubsub.Publisher
queue Queue
}
func New(pub pubsub.Publisher) (*CommandService, error) {
func New(pub pubsub.Publisher, queue Queue) (*CommandService, error) {
svc := CommandService{
publisher: pub,
queue: queue,
}
return &svc, nil
}