mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-09 02:55:59 +08:00
Implement Profile and Blueprint removal APIs and mdmctl UI (#206)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
69
cmd/mdmctl/remove.go
Normal 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
|
||||
}
|
||||
29
cmd/mdmctl/remove_blueprints.go
Normal file
29
cmd/mdmctl/remove_blueprints.go
Normal 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
|
||||
}
|
||||
29
cmd/mdmctl/remove_profiles.go
Normal file
29
cmd/mdmctl/remove_profiles.go
Normal 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
57
core/remove/client.go
Normal 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
78
core/remove/endpoint.go
Normal 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
41
core/remove/service.go
Normal 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
|
||||
}
|
||||
114
core/remove/transport_http.go
Normal file
114
core/remove/transport_http.go
Normal 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
|
||||
}
|
||||
@@ -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 ¬Found{"Profile", fmt.Sprintf("id %s", id)}
|
||||
}
|
||||
return b.Delete([]byte(id))
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
type notFound struct {
|
||||
ResourceType string
|
||||
Message string
|
||||
|
||||
6
serve.go
6
serve.go
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user