diff --git a/cmd/mdmctl/list.go b/cmd/mdmctl/list.go new file mode 100644 index 00000000..5d70fe6b --- /dev/null +++ b/cmd/mdmctl/list.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "text/tabwriter" + + "github.com/go-kit/kit/log" + "github.com/micromdm/micromdm/core/list" +) + +type tableOutput struct{ w *tabwriter.Writer } + +func (out *tableOutput) BasicHeader() { + fmt.Fprintf(out.w, "UDID\tSerialNumber\tEnrollmentStatus\tLastSeen\n") +} + +func (out *tableOutput) BasicFooter() { + out.w.Flush() +} + +func (out *tableOutput) BasicLine(line ...string) { + for _, l := range line { + fmt.Fprintf(out.w, l+"\t") + } + fmt.Fprintf(out.w, "\n") +} + +func listDevices(args []string) error { + if len(args) == 0 || args[0] != "devices" { + return errors.New("must specify resource name as an argument (ex: devices)") + } + w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0) + out := &tableOutput{w} + out.BasicHeader() + defer out.BasicFooter() + logger := log.NewLogfmtLogger(os.Stderr) + // TODO needs some config for authentication and server url client side. + instance := os.Getenv("MICROMDM_SERVER_URL") + if instance == "" { + return errors.New("MICROMDM_SERVER_URL not set") + } + svc, err := list.NewClient(instance, logger) + if err != nil { + return err + } + + ctx := context.Background() + devices, err := svc.ListDevices(ctx, list.ListDevicesOption{}) + if err != nil { + return err + } + for _, d := range devices { + fmt.Fprintf(out.w, "%s\t%s\t%v\t%s\n", d.UDID, d.SerialNumber, d.EnrollmentStatus, d.LastSeen) + } + return nil +} diff --git a/cmd/mdmctl/mdmdctl.go b/cmd/mdmctl/mdmdctl.go new file mode 100644 index 00000000..37ca4a30 --- /dev/null +++ b/cmd/mdmctl/mdmdctl.go @@ -0,0 +1,63 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/micromdm/micromdm/version" +) + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(1) + } + var run func([]string) error + switch strings.ToLower(os.Args[1]) { + case "version", "-version": + version.Print() + return + case "list": + run = listDevices + default: + usage() + os.Exit(1) + } + + if err := run(os.Args[2:]); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func usage() error { + helpText := `USAGE: mdmctl + +Available Commands: + list + version + +Use micromdm -h for additional usage of each command. +Example: micromdm serve -h +` + fmt.Println(helpText) + return nil +} + +func usageFor(fs *flag.FlagSet, short string) func() { + return func() { + fmt.Fprintf(os.Stderr, "USAGE\n") + fmt.Fprintf(os.Stderr, " %s\n", short) + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "FLAGS\n") + w := tabwriter.NewWriter(os.Stderr, 0, 2, 2, ' ', 0) + fs.VisitAll(func(f *flag.Flag) { + fmt.Fprintf(w, "\t-%s %s\t%s\n", f.Name, f.DefValue, f.Usage) + }) + w.Flush() + fmt.Fprintf(os.Stderr, "\n") + } +} diff --git a/core/list/client.go b/core/list/client.go new file mode 100644 index 00000000..d6b1bf72 --- /dev/null +++ b/core/list/client.go @@ -0,0 +1,36 @@ +package list + +import ( + "net/url" + + "github.com/go-kit/kit/endpoint" + "github.com/go-kit/kit/log" + httptransport "github.com/go-kit/kit/transport/http" +) + +func NewClient(instance string, logger log.Logger) (Service, error) { + u, err := url.Parse(instance) + if err != nil { + return nil, err + } + + var listDevicesEndpoint endpoint.Endpoint + { + listDevicesEndpoint = httptransport.NewClient( + "GET", + copyURL(u, "/v1/devices"), + EncodeHTTPGenericRequest, + DecodeDevicesResponse, + ).Endpoint() + } + + return Endpoints{ + ListDevicesEndpoint: listDevicesEndpoint, + }, nil +} + +func copyURL(base *url.URL, path string) *url.URL { + next := *base + next.Path = path + return &next +} diff --git a/core/list/endpoint.go b/core/list/endpoint.go new file mode 100644 index 00000000..3177c2bd --- /dev/null +++ b/core/list/endpoint.go @@ -0,0 +1,45 @@ +package list + +import ( + "context" + "time" + + "github.com/go-kit/kit/endpoint" +) + +type Endpoints struct { + ListDevicesEndpoint endpoint.Endpoint +} + +func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) { + request := devicesRequest{opts} + response, err := e.ListDevicesEndpoint(ctx, request.Opts) + if err != nil { + return nil, err + } + return response.(devicesResponse).Devices, response.(devicesResponse).Err +} + +func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(devicesRequest) + dto, err := svc.ListDevices(ctx, req.Opts) + return devicesResponse{ + Devices: dto, + Err: err, + }, nil + } +} + +type DeviceDTO struct { + SerialNumber string `json:"serial_number"` + UDID string `json:"udid"` + EnrollmentStatus bool `json:"enrollment_status"` + LastSeen time.Time `json:"last_seen"` +} + +type devicesRequest struct{ Opts ListDevicesOption } +type devicesResponse struct { + Devices []DeviceDTO `json:"devices"` + Err error `json:"err,omitempty"` +} diff --git a/core/list/service.go b/core/list/service.go new file mode 100644 index 00000000..47228761 --- /dev/null +++ b/core/list/service.go @@ -0,0 +1,37 @@ +package list + +import ( + "context" + + "github.com/micromdm/micromdm/device" +) + +type ListDevicesOption struct { + Page int + PerPage int + + FilterSerial []string + FilterUDID []string +} + +type Service interface { + ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) +} + +type ListService struct { + Devices *device.DB +} + +func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) { + devices, err := svc.Devices.List() + dto := []DeviceDTO{} + for _, d := range devices { + dto = append(dto, DeviceDTO{ + SerialNumber: d.SerialNumber, + UDID: d.UDID, + EnrollmentStatus: d.Enrolled, + LastSeen: d.LastCheckin, + }) + } + return dto, err +} diff --git a/core/list/transport_http.go b/core/list/transport_http.go new file mode 100644 index 00000000..046df124 --- /dev/null +++ b/core/list/transport_http.go @@ -0,0 +1,81 @@ +package list + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + + httptransport "github.com/go-kit/kit/transport/http" +) + +func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) http.Handler { + var h http.Handler + h = httptransport.NewServer( + endpoints.ListDevicesEndpoint, + decodeListDevicesRequest, + encodeResponse, + opts..., + ) + return h +} + +func decodeListDevicesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + req := devicesRequest{ + Opts: ListDevicesOption{}, + } + return req, nil +} + +func errorDecoder(r *http.Response) error { + var w errorWrapper + if err := json.NewDecoder(r.Body).Decode(&w); err != nil { + return err + } + return errors.New(w.Error) +} + +type errorWrapper struct { + Error string `json:"error"` +} + +type errorer interface { + error() error +} + +func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { + if e, ok := response.(errorer); ok && e.error() != nil { + EncodeError(ctx, e.error(), w) + return nil + } + + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(response) +} + +func EncodeError(ctx context.Context, err error, w http.ResponseWriter) { + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +// EncodeHTTPGenericRequest is a transport/http.EncodeRequestFunc that +// JSON-encodes any request to the request body. Primarily useful in a client. +func EncodeHTTPGenericRequest(_ context.Context, r *http.Request, request interface{}) error { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(request); err != nil { + return err + } + r.Body = ioutil.NopCloser(&buf) + return nil +} + +func DecodeDevicesResponse(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp devicesResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/device/db.go b/device/db.go index 13a46752..83b5a6c3 100644 --- a/device/db.go +++ b/device/db.go @@ -47,6 +47,24 @@ func NewDB(db *bolt.DB, sub pubsub.Subscriber) (*DB, error) { return datastore, nil } +func (db *DB) List() ([]Device, error) { + // TODO add filter/limit with ForEach + var devices []Device + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(DeviceBucket)) + c := b.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + var dev Device + if err := UnmarshalDevice(v, &dev); err != nil { + return err + } + devices = append(devices, dev) + } + return nil + }) + return devices, err +} + func (db *DB) Save(dev *Device) error { tx, err := db.DB.Begin(true) if err != nil { diff --git a/serve.go b/serve.go index 95e57a24..95654007 100644 --- a/serve.go +++ b/serve.go @@ -43,6 +43,7 @@ import ( "github.com/micromdm/micromdm/checkin" "github.com/micromdm/micromdm/command" "github.com/micromdm/micromdm/connect" + "github.com/micromdm/micromdm/core/list" "github.com/micromdm/micromdm/depsync" "github.com/micromdm/micromdm/device" "github.com/micromdm/micromdm/enroll" @@ -136,7 +137,7 @@ func serve(args []string) error { stdlog.Fatal(err) } - _, err := device.NewDB(sm.db, sm.pubclient) + devDB, err := device.NewDB(sm.db, sm.pubclient) if err != nil { stdlog.Fatal(err) } @@ -180,6 +181,21 @@ func serve(args []string) error { ConnectEndpoint: connectEndpoint, } + var listsvc list.Service + { + listsvc = &list.ListService{Devices: devDB} + } + var listDevicesEndpoint endpoint.Endpoint + { + listDevicesEndpoint = list.MakeListDevicesEndpoint(listsvc) + + } + listEndpoints := list.Endpoints{ + ListDevicesEndpoint: listDevicesEndpoint, + } + + listAPIHandlers := list.MakeHTTPHandlers(ctx, listEndpoints, connectOpts...) + connectHandlers := connect.MakeHTTPHandlers(ctx, connectEndpoints, connectOpts...) pushHandlers := nanopush.MakeHTTPHandlers(ctx, pushEndpoints, checkinOpts...) @@ -192,6 +208,7 @@ func serve(args []string) error { r.Handle("/scep", scepHandler) r.Handle("/push/{udid}", pushHandlers.PushHandler) r.Handle("/v1/commands", commandHandlers.NewCommandHandler).Methods("POST") + r.Handle("/v1/devices", listAPIHandlers).Methods("GET") r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, homePage) })