diff --git a/cmd/mdmctl/apply.go b/cmd/mdmctl/apply.go index 589beeeb..2715fadb 100644 --- a/cmd/mdmctl/apply.go +++ b/cmd/mdmctl/apply.go @@ -6,6 +6,8 @@ import ( "flag" "fmt" "io/ioutil" + "net/http" + "net/url" "os" "path/filepath" "strings" @@ -60,6 +62,8 @@ func (cmd *applyCommand) Run(args []string) error { run = cmd.applyProfile case "app": run = cmd.applyApp + case "block": + run = cmd.applyBlock case "users": run = cmd.applyUser default: @@ -81,6 +85,7 @@ Valid resource types: * dep-tokens * dep-profiles * app + * block Examples: # Apply a Blueprint. @@ -189,6 +194,40 @@ func (cmd *applyCommand) applyDEPTokens(args []string) error { return nil } +func (cmd *applyCommand) applyBlock(args []string) error { + flagset := flag.NewFlagSet("block", flag.ExitOnError) + var ( + flUDID = flagset.String("udid", "", "UDID of a device to block.") + ) + flagset.Usage = usageFor(flagset, "mdmctl apply block [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + if *flUDID == "" { + flagset.Usage() + return errors.New("bad input: must provide a device UDID to block.") + } + if err := cmd.applysvc.BlockDevice(context.Background(), *flUDID); err != nil { + return err + } + + // trigger a push + u, err := url.Parse(cmd.config.ServerURL) + if err != nil { + fmt.Println(err) + return nil + } + u.Path = "/push/" + url.QueryEscape(*flUDID) + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + fmt.Println(err) + return nil + } + req.SetBasicAuth("micromdm", cmd.config.APIToken) + http.DefaultClient.Do(req) + return nil +} + func (cmd *applyCommand) applyProfile(args []string) error { flagset := flag.NewFlagSet("profiles", flag.ExitOnError) var ( diff --git a/cmd/mdmctl/remove.go b/cmd/mdmctl/remove.go index a7a38eab..6b7a39c2 100644 --- a/cmd/mdmctl/remove.go +++ b/cmd/mdmctl/remove.go @@ -48,6 +48,8 @@ func (cmd *removeCommand) Run(args []string) error { run = cmd.removeBlueprints case "profiles": run = cmd.removeProfiles + case "block": + run = cmd.removeBlock default: cmd.Usage() os.Exit(1) @@ -63,7 +65,9 @@ Display one or many resources. Valid resource types: * blueprints - * profiles` + * profiles + * block +` fmt.Println(getUsage) return nil diff --git a/cmd/mdmctl/remove_block.go b/cmd/mdmctl/remove_block.go new file mode 100644 index 00000000..511afce3 --- /dev/null +++ b/cmd/mdmctl/remove_block.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/pkg/errors" +) + +func (cmd *removeCommand) removeBlock(args []string) error { + flagset := flag.NewFlagSet("unblock", flag.ExitOnError) + var ( + flUDID = flagset.String("udid", "", "UDID of device to unblock") + ) + flagset.Usage = usageFor(flagset, "mdmctl remove block [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + + if *flUDID == "" { + flagset.Usage() + return errors.New("bad input: must provide a device UDID to unblock.") + } + + ctx := context.Background() + if err := cmd.remove.UnblockDevice(ctx, *flUDID); err != nil { + return err + } + + fmt.Println("success") + + return nil +} diff --git a/cmd/micromdm/serve.go b/cmd/micromdm/serve.go index 4e961a00..705400ee 100644 --- a/cmd/micromdm/serve.go +++ b/cmd/micromdm/serve.go @@ -57,6 +57,7 @@ import ( "github.com/micromdm/micromdm/platform/pubsub" "github.com/micromdm/micromdm/platform/pubsub/inmem" "github.com/micromdm/micromdm/platform/queue" + block "github.com/micromdm/micromdm/platform/remove" "github.com/micromdm/micromdm/platform/user" "github.com/micromdm/micromdm/workflow/webhook" ) @@ -142,8 +143,10 @@ func serve(args []string) error { // no less secure and prevents a useless dialog from showing. SCEPChallenge: "micromdm", } + sm.setupPubSub() sm.setupBolt() + sm.setupRemoveService() sm.setupConfigStore() sm.loadPushCerts() sm.setupSCEP(logger) @@ -157,6 +160,11 @@ func serve(args []string) error { stdlog.Fatal(sm.err) } + removeService, err := block.NewService(sm.removeDB) + if err != nil { + stdlog.Fatal(err) + } + devDB, err := device.NewDB(sm.db, sm.pubclient) if err != nil { stdlog.Fatal(err) @@ -298,12 +306,13 @@ func serve(args []string) error { var applysvc apply.Service { l := &apply.ApplyService{ - DEPClient: dc, - Blueprints: bpDB, - Tokens: tokenDB, - Profiles: sm.profileDB, - Apps: appDB, - Users: userDB, + DEPClient: dc, + Blueprints: bpDB, + Tokens: tokenDB, + Profiles: sm.profileDB, + Apps: appDB, + Users: userDB, + RemoveService: removeService, } applysvc = l if err := l.WatchTokenUpdates(sm.pubclient); err != nil { @@ -343,13 +352,14 @@ func serve(args []string) error { DefineDEPProfileEndpoint: defineDEPProfileEndpoint, AppUploadEndpoint: appUploadEndpoint, ApplyUserEndpoint: applyUserEndpoint, + BlockDeviceEndpoint: apply.MakeBlockDeviceEndpoint(applysvc), } applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...) listAPIHandlers := list.MakeHTTPHandlers(ctx, listEndpoints, connectOpts...) - rmsvc := &remove.RemoveService{Blueprints: bpDB, Profiles: sm.profileDB} + rmsvc := &remove.RemoveService{Blueprints: bpDB, Profiles: sm.profileDB, RemoveService: removeService} removeAPIHandlers := remove.MakeHTTPHandlers(ctx, remove.MakeEndpoints(rmsvc), connectOpts...) connectHandlers := connect.MakeHTTPHandlers(ctx, connectEndpoints, connectOpts...) @@ -373,6 +383,8 @@ func serve(args []string) error { r.Handle("/push/{udid}", apiAuthMiddleware(*flAPIKey, pushHandlers.PushHandler)) r.Handle("/v1/commands", apiAuthMiddleware(*flAPIKey, commandHandlers.NewCommandHandler)).Methods("POST") r.Handle("/v1/devices", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListDevicesHandler)).Methods("GET") + r.Handle("/v1/devices/{udid}/block", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.BlockDeviceHandler)).Methods("POST") + r.Handle("/v1/devices/{udid}/unblock", apiAuthMiddleware(*flAPIKey, removeAPIHandlers.UnblockDeviceHandler)).Methods("POST") r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPTokensHandler)).Methods("GET") r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DEPTokensHandler)).Methods("PUT") r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetBlueprintsHandler)).Methods("GET") @@ -486,6 +498,7 @@ type server struct { scepDepot *boltdepot.Depot profileDB *profile.DB configDB *config.DB + removeDB *block.DB CommandWebhookURL string // TODO: refactor enroll service and remove the need to reference @@ -496,7 +509,7 @@ type server struct { PushService *push.Service // bufford push pushService apns.Service checkinService checkin.Service - connectService connect.ConnectService + connectService connect.Service enrollService enroll.Service scepService scep.Service commandService command.Service @@ -550,6 +563,18 @@ func (c *server) startWebhooks() { } } +func (c *server) setupRemoveService() { + if c.err != nil { + return + } + removeDB, err := block.NewDB(c.db) + if err != nil { + c.err = err + return + } + c.removeDB = removeDB +} + func (c *server) setupCommandQueue(logger log.Logger) { if c.err != nil { return @@ -560,18 +585,16 @@ func (c *server) setupCommandQueue(logger log.Logger) { return } - var connectService connect.ConnectService + var connectService connect.Service { svc, err := connect.New(q, c.pubclient) if err != nil { c.err = err return } - svc = connect.NewLoggingService( - svc, - log.With(level.Info(logger), "component", "connect"), - ) connectService = svc + connectService = connect.LoggingMiddleware(log.With(level.Info(logger), "component", "connect"))(svc) + connectService = block.RemoveMiddleware(c.removeDB)(connectService) } c.connectService = connectService } diff --git a/mdm/connect/connect.go b/mdm/connect/connect.go index a81a4588..ad6b9fea 100644 --- a/mdm/connect/connect.go +++ b/mdm/connect/connect.go @@ -12,16 +12,16 @@ import ( const ConnectTopic = "mdm.Connect" -// The ConnectService accepts responses sent to an MDM server by an enrolled +// The Service accepts responses sent to an MDM server by an enrolled // device. -type ConnectService interface { +type Service interface { // Acknowledge acknowledges a response sent by a device and returns // the next payload if one is available. Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error) } -type connectSvc struct { +type ConnectService struct { queue Queue pub pubsub.Publisher } @@ -30,14 +30,14 @@ type Queue interface { Next(context.Context, mdm.Response) (*queue.Command, error) } -func New(queue Queue, pub pubsub.Publisher) (ConnectService, error) { - return &connectSvc{ +func New(queue Queue, pub pubsub.Publisher) (*ConnectService, error) { + return &ConnectService{ queue: queue, pub: pub, }, nil } -func (svc *connectSvc) Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error) { +func (svc *ConnectService) Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error) { event := NewEvent(req) msg, err := MarshalEvent(event) if err != nil { diff --git a/mdm/connect/endpoint.go b/mdm/connect/endpoint.go index 554b16e4..19869f41 100644 --- a/mdm/connect/endpoint.go +++ b/mdm/connect/endpoint.go @@ -23,7 +23,7 @@ type Endpoints struct { ConnectEndpoint endpoint.Endpoint } -func MakeConnectEndpoint(svc ConnectService) endpoint.Endpoint { +func MakeConnectEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(MDMConnectRequest) payload, err := svc.Acknowledge(ctx, req) diff --git a/mdm/connect/logging.go b/mdm/connect/middleware.go similarity index 74% rename from mdm/connect/logging.go rename to mdm/connect/middleware.go index f1657024..45870d56 100644 --- a/mdm/connect/logging.go +++ b/mdm/connect/middleware.go @@ -7,16 +7,20 @@ import ( "github.com/go-kit/kit/log" ) -type loggingMiddleware struct { - logger log.Logger - next ConnectService +type Middleware func(Service) Service + +func LoggingMiddleware(logger log.Logger) Middleware { + return func(next Service) Service { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } } -func NewLoggingService(svc ConnectService, logger log.Logger) loggingMiddleware { - return loggingMiddleware{ - next: svc, - logger: logger, - } +type loggingMiddleware struct { + next Service + logger log.Logger } func (mw loggingMiddleware) Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error) { diff --git a/mdm/connect/transport_http.go b/mdm/connect/transport_http.go index 2e86ee67..fc59bf5e 100644 --- a/mdm/connect/transport_http.go +++ b/mdm/connect/transport_http.go @@ -2,8 +2,8 @@ package connect import ( "context" - "fmt" "io/ioutil" + "log" "net/http" httptransport "github.com/go-kit/kit/transport/http" @@ -68,6 +68,17 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa // The EncodeError should be passed to the Go-Kit httptransport as the // ServerErrorEncoder to encode error responses. func EncodeError(ctx context.Context, err error, w http.ResponseWriter) { - fmt.Printf("connect error: %s\n", err) + type checkoutErr interface { + error + Checkout() bool + } + if e, ok := err.(checkoutErr); ok { + if e.Checkout() { + log.Printf("connect: forced checkout error: %s\n", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + } + log.Printf("connect error: %s\n", err) w.WriteHeader(http.StatusInternalServerError) } diff --git a/platform/api/server/apply/client.go b/platform/api/server/apply/client.go index bc7c335d..7c34b698 100644 --- a/platform/api/server/apply/client.go +++ b/platform/api/server/apply/client.go @@ -80,6 +80,17 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra ).Endpoint() } + var blockDeviceEndpoint endpoint.Endpoint + { + blockDeviceEndpoint = httptransport.NewClient( + "POST", + copyURL(u, ""), // empty path, modified by the encodeRequest func + encodeRequestWithToken(token, encodeBlockDeviceRequest), + DecodeBlockDeviceResponse, + opts..., + ).Endpoint() + } + return Endpoints{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, ApplyDEPTokensEndpoint: applyDEPTokensEndpoint, @@ -87,6 +98,7 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra DefineDEPProfileEndpoint: defineDEPProfileEndpoint, AppUploadEndpoint: uploadAppEndpoint, ApplyUserEndpoint: applyUserEndpoint, + BlockDeviceEndpoint: blockDeviceEndpoint, }, nil } diff --git a/platform/api/server/apply/endpoint.go b/platform/api/server/apply/endpoint.go index 68218d04..ee00ff6d 100644 --- a/platform/api/server/apply/endpoint.go +++ b/platform/api/server/apply/endpoint.go @@ -19,6 +19,7 @@ type Endpoints struct { DefineDEPProfileEndpoint endpoint.Endpoint AppUploadEndpoint endpoint.Endpoint ApplyUserEndpoint endpoint.Endpoint + BlockDeviceEndpoint endpoint.Endpoint } func (e Endpoints) ApplyUser(ctx context.Context, u user.User) (*user.User, error) { @@ -33,6 +34,17 @@ func (e Endpoints) ApplyUser(ctx context.Context, u user.User) (*user.User, erro return &usr, resp.(applyUserResponse).Err } +func (e Endpoints) BlockDevice(ctx context.Context, udid string) error { + request := blockDeviceRequest{ + UDID: udid, + } + resp, err := e.BlockDeviceEndpoint(ctx, request) + if err != nil { + return err + } + return resp.(blockDeviceResponse).Err +} + func (e Endpoints) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error { request := appUploadRequest{ ManifestName: manifestName, @@ -146,6 +158,16 @@ func MakeUploadAppEndpiont(svc Service) endpoint.Endpoint { } } +func MakeBlockDeviceEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(blockDeviceRequest) + err = svc.BlockDevice(ctx, req.UDID) + return &blockDeviceResponse{ + Err: err, + }, nil + } +} + type appUploadRequest struct { ManifestName string ManifestFile io.Reader @@ -208,3 +230,13 @@ type applyUserResponse struct { } func (r applyUserResponse) error() error { return r.Err } + +type blockDeviceRequest struct { + UDID string +} + +type blockDeviceResponse struct { + Err error `json:"err,omitempty"` +} + +func (r blockDeviceResponse) error() error { return r.Err } diff --git a/platform/api/server/apply/service.go b/platform/api/server/apply/service.go index 9e3e9b0f..95772392 100644 --- a/platform/api/server/apply/service.go +++ b/platform/api/server/apply/service.go @@ -21,6 +21,7 @@ import ( "github.com/micromdm/micromdm/platform/deptoken" "github.com/micromdm/micromdm/platform/profile" "github.com/micromdm/micromdm/platform/pubsub" + "github.com/micromdm/micromdm/platform/remove" "github.com/micromdm/micromdm/platform/user" ) @@ -31,6 +32,7 @@ type Service interface { UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error ApplyUser(ctx context.Context, u user.User) (*user.User, error) DEPService + BlockDevice(ctx context.Context, udid string) error } type ApplyService struct { @@ -42,6 +44,7 @@ type ApplyService struct { Tokens *deptoken.DB Apps appstore.AppStore Users *user.DB + *remove.RemoveService } func (svc *ApplyService) ApplyUser(ctx context.Context, u user.User) (*user.User, error) { diff --git a/platform/api/server/apply/transport_http.go b/platform/api/server/apply/transport_http.go index f6adae2e..a29cdfee 100644 --- a/platform/api/server/apply/transport_http.go +++ b/platform/api/server/apply/transport_http.go @@ -8,8 +8,10 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "net/url" httptransport "github.com/go-kit/kit/transport/http" + "github.com/gorilla/mux" "github.com/pkg/errors" ) @@ -20,6 +22,7 @@ type HTTPHandlers struct { DefineDEPProfileHandler http.Handler AppUploadHandler http.Handler ApplyUserhandler http.Handler + BlockDeviceHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -60,10 +63,28 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran encodeResponse, opts..., ), + BlockDeviceHandler: httptransport.NewServer( + endpoints.BlockDeviceEndpoint, + decodeBlockDeviceRequest, + encodeResponse, + opts..., + ), } return h } +func decodeBlockDeviceRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var errBadRoute = errors.New("bad route") + var req blockDeviceRequest + vars := mux.Vars(r) + udid, ok := vars["udid"] + if !ok { + return 0, errBadRoute + } + req.UDID = udid + return req, nil +} + func decodeDEPTokensRequest(ctx context.Context, r *http.Request) (interface{}, error) { var req depTokensRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -208,6 +229,13 @@ func EncodeHTTPGenericRequest(_ context.Context, r *http.Request, request interf return nil } +func encodeBlockDeviceRequest(_ context.Context, r *http.Request, request interface{}) error { + req := request.(blockDeviceRequest) + udid := url.QueryEscape(req.UDID) + r.Method, r.URL.Path = "POST", "/v1/devices/"+udid+"/block" + return nil +} + func DecodeBlueprintResponse(_ context.Context, r *http.Response) (interface{}, error) { if r.StatusCode != http.StatusOK { return nil, errorDecoder(r) @@ -261,3 +289,12 @@ func DecodeApplyUserResponse(_ context.Context, r *http.Response) (interface{}, err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } + +func DecodeBlockDeviceResponse(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp blockDeviceResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/platform/api/server/remove/client.go b/platform/api/server/remove/client.go index 9c5c1cd9..1fb0967e 100644 --- a/platform/api/server/remove/client.go +++ b/platform/api/server/remove/client.go @@ -37,9 +37,21 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra ).Endpoint() } + var unblockDeviceEndpoint endpoint.Endpoint + { + unblockDeviceEndpoint = httptransport.NewClient( + "POST", + copyURL(u, ""), //modified by encodeRequestFunc + encodeRequestWithToken(token, encodeUnblockDeviceRequest), + DecodeUnblockDeviceResponse, + opts..., + ).Endpoint() + } + return Endpoints{ RemoveBlueprintsEndpoint: removeBlueprintsEndpoint, RemoveProfilesEndpoint: removeProfilesEndpoint, + UnblockDeviceEndpoint: unblockDeviceEndpoint, }, nil } diff --git a/platform/api/server/remove/endpoint.go b/platform/api/server/remove/endpoint.go index e1e976fe..0fc47e7b 100644 --- a/platform/api/server/remove/endpoint.go +++ b/platform/api/server/remove/endpoint.go @@ -9,16 +9,27 @@ import ( type Endpoints struct { RemoveBlueprintsEndpoint endpoint.Endpoint RemoveProfilesEndpoint endpoint.Endpoint + UnblockDeviceEndpoint endpoint.Endpoint } func MakeEndpoints(svc Service) Endpoints { e := Endpoints{ RemoveBlueprintsEndpoint: MakeRemoveBlueprintsEndpoint(svc), RemoveProfilesEndpoint: MakeRemoveProfilesEndpoint(svc), + UnblockDeviceEndpoint: MakeUnblockDeviceEndpoint(svc), } return e } +func (e Endpoints) UnblockDevice(ctx context.Context, udid string) error { + request := unblockDeviceRequest{UDID: udid} + resp, err := e.UnblockDeviceEndpoint(ctx, request) + if err != nil { + return err + } + return resp.(unblockDeviceResponse).Err +} + func (e Endpoints) RemoveBlueprints(ctx context.Context, names []string) error { request := blueprintRequest{Names: names} resp, err := e.RemoveBlueprintsEndpoint(ctx, request) @@ -57,6 +68,26 @@ func MakeRemoveProfilesEndpoint(svc Service) endpoint.Endpoint { } } +func MakeUnblockDeviceEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(unblockDeviceRequest) + err = svc.UnblockDevice(ctx, req.UDID) + return unblockDeviceResponse{ + Err: err, + }, nil + } +} + +type unblockDeviceRequest struct { + UDID string +} + +type unblockDeviceResponse struct { + Err error `json:"err,omitempty"` +} + +func (r unblockDeviceResponse) error() error { return r.Err } + type blueprintRequest struct { Names []string `json:"names"` } diff --git a/platform/api/server/remove/service.go b/platform/api/server/remove/service.go index 10294da1..f3a0e814 100644 --- a/platform/api/server/remove/service.go +++ b/platform/api/server/remove/service.go @@ -5,16 +5,19 @@ import ( "github.com/micromdm/micromdm/platform/blueprint" "github.com/micromdm/micromdm/platform/profile" + "github.com/micromdm/micromdm/platform/remove" ) type Service interface { RemoveBlueprints(ctx context.Context, names []string) error RemoveProfiles(ctx context.Context, ids []string) error + UnblockDevice(ctx context.Context, udid string) error } type RemoveService struct { Blueprints *blueprint.DB Profiles *profile.DB + *remove.RemoveService } func (svc *RemoveService) RemoveBlueprints(ctx context.Context, names []string) error { diff --git a/platform/api/server/remove/transport_http.go b/platform/api/server/remove/transport_http.go index d9949c3c..981e5e0c 100644 --- a/platform/api/server/remove/transport_http.go +++ b/platform/api/server/remove/transport_http.go @@ -7,13 +7,16 @@ import ( "errors" "io/ioutil" "net/http" + "net/url" httptransport "github.com/go-kit/kit/transport/http" + "github.com/gorilla/mux" ) type HTTPHandlers struct { - BlueprintHandler http.Handler - ProfileHandler http.Handler + BlueprintHandler http.Handler + ProfileHandler http.Handler + UnblockDeviceHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoint Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -30,6 +33,12 @@ func MakeHTTPHandlers(ctx context.Context, endpoint Endpoints, opts ...httptrans encodeResponse, opts..., ), + UnblockDeviceHandler: httptransport.NewServer( + endpoint.UnblockDeviceEndpoint, + decodeUnblockDeviceRequest, + encodeResponse, + opts..., + ), } return h } @@ -50,6 +59,18 @@ func decodeProfileRequest(ctx context.Context, r *http.Request) (interface{}, er return req, nil } +func decodeUnblockDeviceRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var errBadRoute = errors.New("bad route") + var req unblockDeviceRequest + vars := mux.Vars(r) + udid, ok := vars["udid"] + if !ok { + return 0, errBadRoute + } + req.UDID = udid + return req, nil +} + type errorWrapper struct { Error string `json:"error"` } @@ -95,6 +116,13 @@ func EncodeHTTPGenericRequest(_ context.Context, r *http.Request, request interf return nil } +func encodeUnblockDeviceRequest(_ context.Context, r *http.Request, request interface{}) error { + req := request.(unblockDeviceRequest) + udid := url.QueryEscape(req.UDID) + r.Method, r.URL.Path = "POST", "/v1/devices/"+udid+"/unblock" + return nil +} + func DecodeBlueprintResponse(_ context.Context, r *http.Response) (interface{}, error) { if r.StatusCode != http.StatusOK { return nil, errorDecoder(r) @@ -112,3 +140,12 @@ func DecodeProfileResponse(_ context.Context, r *http.Response) (interface{}, er err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } + +func DecodeUnblockDeviceResponse(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp unblockDeviceResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/platform/remove/db.go b/platform/remove/db.go new file mode 100644 index 00000000..3f22128b --- /dev/null +++ b/platform/remove/db.go @@ -0,0 +1,90 @@ +package remove + +import ( + "fmt" + + "github.com/boltdb/bolt" + "github.com/pkg/errors" +) + +const RemoveBucket = "mdm.RemoveDevice" + +type DB struct { + *bolt.DB +} + +func NewDB(db *bolt.DB) (*DB, error) { + err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(RemoveBucket)) + return err + }) + if err != nil { + return nil, errors.Wrapf(err, "creating %s bucket", RemoveBucket) + } + datastore := &DB{ + DB: db, + } + return datastore, nil +} + +func (db *DB) DeviceByUDID(udid string) (*Device, error) { + var dev Device + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(RemoveBucket)) + v := b.Get([]byte(udid)) + if v == nil { + return ¬Found{"Device", fmt.Sprintf("udid %s", udid)} + } + return UnmarshalDevice(v, &dev) + }) + return &dev, errors.Wrap(err, "remove: get device by udid") +} + +func (db *DB) Save(dev *Device) error { + tx, err := db.DB.Begin(true) + if err != nil { + return errors.Wrap(err, "begin transaction") + } + bkt := tx.Bucket([]byte(RemoveBucket)) + if bkt == nil { + return fmt.Errorf("bucket %q not found!", RemoveBucket) + } + pb, err := MarshalDevice(dev) + if err != nil { + return errors.Wrap(err, "marshalling Device") + } + key := []byte(dev.UDID) + if err := bkt.Put(key, pb); err != nil { + return errors.Wrap(err, "put device to boltdb") + } + return tx.Commit() +} + +func (db *DB) Delete(udid string) error { + err := db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(RemoveBucket)) + v := b.Get([]byte(udid)) + if v == nil { + return ¬Found{"Device", fmt.Sprintf("udid %s", udid)} + } + return b.Delete([]byte(udid)) + }) + return errors.Wrapf(err, "delete device with udid %s", udid) +} + +type notFound struct { + ResourceType string + Message string +} + +func (e *notFound) Error() string { + return fmt.Sprintf("not found: %s %s", e.ResourceType, e.Message) +} + +func isNotFound(err error) bool { + cause := errors.Cause(err) + if _, ok := cause.(*notFound); ok { + return true + } + return false +} diff --git a/platform/remove/internal/removeproto/remove.go b/platform/remove/internal/removeproto/remove.go new file mode 100644 index 00000000..cf41eb96 --- /dev/null +++ b/platform/remove/internal/removeproto/remove.go @@ -0,0 +1,3 @@ +package removeproto + +//go:generate protoc --go_out=. remove.proto diff --git a/platform/remove/internal/removeproto/remove.pb.go b/platform/remove/internal/removeproto/remove.pb.go new file mode 100644 index 00000000..832b2ce5 --- /dev/null +++ b/platform/remove/internal/removeproto/remove.pb.go @@ -0,0 +1,59 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: remove.proto + +/* +Package removeproto is a generated protocol buffer package. + +It is generated from these files: + remove.proto + +It has these top-level messages: + Device +*/ +package removeproto + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Device struct { + Udid string `protobuf:"bytes,1,opt,name=udid" json:"udid,omitempty"` +} + +func (m *Device) Reset() { *m = Device{} } +func (m *Device) String() string { return proto.CompactTextString(m) } +func (*Device) ProtoMessage() {} +func (*Device) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Device) GetUdid() string { + if m != nil { + return m.Udid + } + return "" +} + +func init() { + proto.RegisterType((*Device)(nil), "removeproto.Device") +} + +func init() { proto.RegisterFile("remove.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 75 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0x4a, 0xcd, 0xcd, + 0x2f, 0x4b, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x86, 0xf0, 0xc0, 0x1c, 0x25, 0x19, + 0x2e, 0x36, 0x97, 0xd4, 0xb2, 0xcc, 0xe4, 0x54, 0x21, 0x21, 0x2e, 0x96, 0xd2, 0x94, 0xcc, 0x14, + 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x30, 0x3b, 0x89, 0x0d, 0xac, 0xc8, 0x18, 0x10, 0x00, + 0x00, 0xff, 0xff, 0xf6, 0xb9, 0x87, 0xa4, 0x41, 0x00, 0x00, 0x00, +} diff --git a/platform/remove/internal/removeproto/remove.proto b/platform/remove/internal/removeproto/remove.proto new file mode 100644 index 00000000..d12a1f0c --- /dev/null +++ b/platform/remove/internal/removeproto/remove.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package removeproto; + +message Device { + string udid = 1; +} diff --git a/platform/remove/remove.go b/platform/remove/remove.go new file mode 100644 index 00000000..ba1a3ab6 --- /dev/null +++ b/platform/remove/remove.go @@ -0,0 +1,90 @@ +package remove + +import ( + "context" + + "github.com/gogo/protobuf/proto" + "github.com/pkg/errors" + + "github.com/micromdm/micromdm/mdm/connect" + "github.com/micromdm/micromdm/platform/remove/internal/removeproto" +) + +type Service interface { + BlockDevice(ctx context.Context, udid string) error + UnblockDevice(ctx context.Context, udid string) error +} + +type RemoveService struct { + db *DB +} + +func NewService(db *DB) (*RemoveService, error) { + return &RemoveService{db: db}, nil +} + +func (svc *RemoveService) BlockDevice(ctx context.Context, udid string) error { + return svc.db.Save(&Device{UDID: udid}) +} + +func (svc *RemoveService) UnblockDevice(ctx context.Context, udid string) error { + return svc.db.Delete(udid) +} + +type Device struct { + UDID string `json:"udid"` +} + +func MarshalDevice(dev *Device) ([]byte, error) { + protodev := removeproto.Device{ + Udid: dev.UDID, + } + return proto.Marshal(&protodev) +} + +func UnmarshalDevice(data []byte, dev *Device) error { + var pb removeproto.Device + if err := proto.Unmarshal(data, &pb); err != nil { + return errors.Wrap(err, "remove: unmarshal proto to device") + } + dev.UDID = pb.GetUdid() + return nil +} + +func RemoveMiddleware(db *DB) connect.Middleware { + return func(next connect.Service) connect.Service { + return &removeMiddleware{ + db: db, + next: next, + } + } +} + +type removeMiddleware struct { + db *DB + next connect.Service +} + +func (mw removeMiddleware) Acknowledge(ctx context.Context, req connect.MDMConnectRequest) ([]byte, error) { + udid := req.MDMResponse.UDID + _, err := mw.db.DeviceByUDID(udid) + if err != nil { + if !isNotFound(err) { + return nil, errors.Wrapf(err, "remove: get device by udid %s", udid) + } + } + if err == nil { + return nil, checkoutErr{} + } + return mw.next.Acknowledge(ctx, req) +} + +type checkoutErr struct{} + +func (checkoutErr) Error() string { + return "checkout forced by device block" +} + +func (checkoutErr) Checkout() bool { + return true +}