Implement Profile and Blueprint removal APIs and mdmctl UI (#206)

This commit is contained in:
Jesse Peterson
2017-06-03 21:51:22 -07:00
committed by GitHub
parent 1d06957af9
commit 40ccbefc17
11 changed files with 465 additions and 0 deletions

View File

@@ -164,6 +164,32 @@ func (db *DB) BlueprintsByApplyAt(name string) ([]*Blueprint, error) {
return bps, err
}
func (db *DB) Delete(name string) error {
bp, err := db.BlueprintByName(name)
if err != nil {
return err
}
err = db.Update(func(tx *bolt.Tx) error {
// TODO: reformulate into a transaction?
b := tx.Bucket([]byte(BlueprintBucket))
i := tx.Bucket([]byte(blueprintIndexBucket))
err := i.Delete([]byte(bp.Name))
if err != nil {
return err
}
err = i.Delete([]byte(bp.UUID))
if err != nil {
return err
}
err = b.Delete([]byte(bp.UUID))
if err != nil {
return err
}
return nil
})
return err
}
type notFound struct {
ResourceType string
Message string

View File

@@ -29,6 +29,9 @@ func main() {
case "apply":
cmd := &applyCommand{}
run = cmd.Run
case "remove":
cmd := &removeCommand{}
run = cmd.Run
default:
usage()
os.Exit(1)
@@ -47,6 +50,7 @@ Available Commands:
get
apply
config
remove
version
Use micromdm <command> -h for additional usage of each command.

69
cmd/mdmctl/remove.go Normal file
View File

@@ -0,0 +1,69 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/micromdm/micromdm/core/remove"
)
type removeCommand struct {
config *ClientConfig
remove remove.Service
}
func (cmd *removeCommand) setup() error {
cfg, err := LoadClientConfig()
if err != nil {
return err
}
cmd.config = cfg
logger := log.NewLogfmtLogger(os.Stderr)
rmsvc, err := remove.NewClient(cfg.ServerURL, logger, cfg.APIToken, httptransport.SetClient(skipVerifyHTTPClient(cmd.config.SkipVerify)))
if err != nil {
return err
}
cmd.remove = rmsvc
return nil
}
func (cmd *removeCommand) Run(args []string) error {
if len(args) < 1 {
cmd.Usage()
os.Exit(1)
}
if err := cmd.setup(); err != nil {
return err
}
var run func([]string) error
switch strings.ToLower(args[0]) {
case "blueprints":
run = cmd.removeBlueprints
case "profiles":
run = cmd.removeProfiles
default:
cmd.Usage()
os.Exit(1)
}
return run(args[1:])
}
func (cmd *removeCommand) Usage() error {
const getUsage = `
Display one or many resources.
Valid resource types:
* blueprints
* profiles`
fmt.Println(getUsage)
return nil
}

View File

@@ -0,0 +1,29 @@
package main
import (
"context"
"flag"
"fmt"
"strings"
)
func (cmd *removeCommand) removeBlueprints(args []string) error {
flagset := flag.NewFlagSet("remove-blueprints", flag.ExitOnError)
var (
flBlueprintName = flagset.String("name", "", "name of blueprint, optionally comma separated")
)
flagset.Usage = usageFor(flagset, "mdmctl remove blueprints [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
ctx := context.Background()
err := cmd.remove.RemoveBlueprints(ctx, strings.Split(*flBlueprintName, ","))
if err != nil {
return err
}
fmt.Printf("removed blueprint(s): %s\n", *flBlueprintName)
return nil
}

View File

@@ -0,0 +1,29 @@
package main
import (
"context"
"flag"
"fmt"
"strings"
)
func (cmd *removeCommand) removeProfiles(args []string) error {
flagset := flag.NewFlagSet("remove-profiles", flag.ExitOnError)
var (
flIdentifier = flagset.String("id", "", "profile Identifier, optionally comma separated")
)
flagset.Usage = usageFor(flagset, "mdmctl remove profiles [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
ctx := context.Background()
err := cmd.remove.RemoveProfiles(ctx, strings.Split(*flIdentifier, ","))
if err != nil {
return err
}
fmt.Printf("removed profile(s): %s\n", *flIdentifier)
return nil
}

57
core/remove/client.go Normal file
View File

@@ -0,0 +1,57 @@
package remove
import (
"context"
"net/http"
"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, token string, opts ...httptransport.ClientOption) (Service, error) {
u, err := url.Parse(instance)
if err != nil {
return nil, err
}
var removeBlueprintsEndpoint endpoint.Endpoint
{
removeBlueprintsEndpoint = httptransport.NewClient(
"DELETE",
copyURL(u, "/v1/blueprints"),
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
DecodeBlueprintResponse,
opts...,
).Endpoint()
}
var removeProfilesEndpoint endpoint.Endpoint
{
removeProfilesEndpoint = httptransport.NewClient(
"DELETE",
copyURL(u, "/v1/profiles"),
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
DecodeProfileResponse,
opts...,
).Endpoint()
}
return Endpoints{
RemoveBlueprintsEndpoint: removeBlueprintsEndpoint,
RemoveProfilesEndpoint: removeProfilesEndpoint,
}, nil
}
func encodeRequestWithToken(token string, next httptransport.EncodeRequestFunc) httptransport.EncodeRequestFunc {
return func(ctx context.Context, r *http.Request, request interface{}) error {
r.SetBasicAuth("micromdm", token)
return next(ctx, r, request)
}
}
func copyURL(base *url.URL, path string) *url.URL {
next := *base
next.Path = path
return &next
}

78
core/remove/endpoint.go Normal file
View File

@@ -0,0 +1,78 @@
package remove
import (
"context"
"github.com/go-kit/kit/endpoint"
)
type Endpoints struct {
RemoveBlueprintsEndpoint endpoint.Endpoint
RemoveProfilesEndpoint endpoint.Endpoint
}
func MakeEndpoints(svc Service) Endpoints {
e := Endpoints{
RemoveBlueprintsEndpoint: MakeRemoveBlueprintsEndpoint(svc),
RemoveProfilesEndpoint: MakeRemoveProfilesEndpoint(svc),
}
return e
}
func (e Endpoints) RemoveBlueprints(ctx context.Context, names []string) error {
request := blueprintRequest{Names: names}
resp, err := e.RemoveBlueprintsEndpoint(ctx, request)
if err != nil {
return err
}
return resp.(blueprintResponse).Err
}
func (e Endpoints) RemoveProfiles(ctx context.Context, ids []string) error {
request := profileRequest{Identifiers: ids}
resp, err := e.RemoveProfilesEndpoint(ctx, request)
if err != nil {
return err
}
return resp.(profileResponse).Err
}
func MakeRemoveBlueprintsEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(blueprintRequest)
err = svc.RemoveBlueprints(ctx, req.Names)
return blueprintResponse{
Err: err,
}, nil
}
}
func MakeRemoveProfilesEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(profileRequest)
err = svc.RemoveProfiles(ctx, req.Identifiers)
return profileResponse{
Err: err,
}, nil
}
}
type blueprintRequest struct {
Names []string `json:"names"`
}
type blueprintResponse struct {
Err error `json:"err,omitempty"`
}
func (r blueprintResponse) error() error { return r.Err }
type profileRequest struct {
Identifiers []string `json:"ids"`
}
type profileResponse struct {
Err error `json:"err,omitempty"`
}
func (r profileResponse) error() error { return r.Err }

41
core/remove/service.go Normal file
View File

@@ -0,0 +1,41 @@
package remove
import (
"context"
"github.com/micromdm/micromdm/blueprint"
"github.com/micromdm/micromdm/profile"
)
type Service interface {
RemoveBlueprints(ctx context.Context, names []string) error
RemoveProfiles(ctx context.Context, ids []string) error
}
type RemoveService struct {
Blueprints *blueprint.DB
Profiles *profile.DB
}
func (svc *RemoveService) RemoveBlueprints(ctx context.Context, names []string) error {
// TODO: Wrap deletion(s) in transactions so as to not have
// incomplete removals?
for _, name := range names {
err := svc.Blueprints.Delete(name)
if err != nil {
return err
}
}
return nil
}
func (svc *RemoveService) RemoveProfiles(ctx context.Context, ids []string) error {
// TODO: Wrap deletion(s) in transactions so as to not have
// incomplete removals?
for _, id := range ids {
err := svc.Profiles.Delete(id)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,114 @@
package remove
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
)
type HTTPHandlers struct {
BlueprintHandler http.Handler
ProfileHandler http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoint Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
h := HTTPHandlers{
BlueprintHandler: httptransport.NewServer(
endpoint.RemoveBlueprintsEndpoint,
decodeBlueprintRequest,
encodeResponse,
opts...,
),
ProfileHandler: httptransport.NewServer(
endpoint.RemoveProfilesEndpoint,
decodeProfileRequest,
encodeResponse,
opts...,
),
}
return h
}
func decodeBlueprintRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var bpReq blueprintRequest
if err := json.NewDecoder(r.Body).Decode(&bpReq); err != nil {
return nil, err
}
return bpReq, nil
}
func decodeProfileRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req profileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}
type errorWrapper struct {
Error string `json:"error"`
}
type errorer interface {
error() error
}
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)
}
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) {
w.WriteHeader(http.StatusInternalServerError)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(errorWrapper{Error: err.Error()})
}
// 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 DecodeBlueprintResponse(_ context.Context, r *http.Response) (interface{}, error) {
if r.StatusCode != http.StatusOK {
return nil, errorDecoder(r)
}
var resp blueprintResponse
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}
func DecodeProfileResponse(_ context.Context, r *http.Response) (interface{}, error) {
if r.StatusCode != http.StatusOK {
return nil, errorDecoder(r)
}
var resp profileResponse
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}

View File

@@ -83,6 +83,18 @@ func (db *DB) ProfileById(id string) (*Profile, error) {
return &p, err
}
func (db *DB) Delete(id string) error {
err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(ProfileBucket))
v := b.Get([]byte(id))
if v == nil {
return &notFound{"Profile", fmt.Sprintf("id %s", id)}
}
return b.Delete([]byte(id))
})
return err
}
type notFound struct {
ResourceType string
Message string

View File

@@ -46,6 +46,7 @@ import (
"github.com/micromdm/micromdm/connect"
"github.com/micromdm/micromdm/core/apply"
"github.com/micromdm/micromdm/core/list"
"github.com/micromdm/micromdm/core/remove"
"github.com/micromdm/micromdm/crypto"
"github.com/micromdm/micromdm/depsync"
"github.com/micromdm/micromdm/deptoken"
@@ -271,6 +272,9 @@ func serve(args []string) error {
listAPIHandlers := list.MakeHTTPHandlers(ctx, listEndpoints, connectOpts...)
rmsvc := &remove.RemoveService{Blueprints: bpDB, Profiles: profDB}
removeAPIHandlers := remove.MakeHTTPHandlers(ctx, remove.MakeEndpoints(rmsvc), connectOpts...)
connectHandlers := connect.MakeHTTPHandlers(ctx, connectEndpoints, connectOpts...)
pushHandlers := nanopush.MakeHTTPHandlers(ctx, pushEndpoints, checkinOpts...)
@@ -296,8 +300,10 @@ func serve(args []string) error {
r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DEPTokensHandler)).Methods("PUT")
r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetBlueprintsHandler)).Methods("GET")
r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.BlueprintHandler)).Methods("PUT")
r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, removeAPIHandlers.BlueprintHandler)).Methods("DELETE")
r.Handle("/v1/profiles", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetProfilesHandler)).Methods("GET")
r.Handle("/v1/profiles", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.ProfileHandler)).Methods("PUT")
r.Handle("/v1/profiles", apiAuthMiddleware(*flAPIKey, removeAPIHandlers.ProfileHandler)).Methods("DELETE")
r.Handle("/v1/dep/devices", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPDeviceDetailsHandler)).Methods("GET")
r.Handle("/v1/dep/account", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPAccountInfoHandler)).Methods("GET")
r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPProfileHander)).Methods("GET")