DEP auto-assignment (v2) (#405)

Support DEP auto-assignment in MicroMDM.

Resolves #227.
Resolves #178.
This commit is contained in:
Jesse Peterson
2018-05-11 13:52:06 -07:00
committed by GitHub
parent b53f9b5c5e
commit 10d6d99f9b
22 changed files with 691 additions and 108 deletions

View File

@@ -2,6 +2,7 @@
### Auto-assigner
* Added DEP auto-assigner logic. #405
* Added support for querying devices by serial. #363
* Added support for triggering a DEP sync via API. #404
* Added support for mdmcert.download directly to `mdmctl` #401

View File

@@ -64,6 +64,8 @@ func (cmd *applyCommand) Run(args []string) error {
run = cmd.applyBlock
case "users":
run = cmd.applyUser
case "dep-autoassigner":
run = cmd.applyDEPAutoAssigner
default:
cmd.Usage()
os.Exit(1)
@@ -82,6 +84,7 @@ Valid resource types:
* users
* dep-tokens
* dep-profiles
* dep-autoassigner
* app
* block

View File

@@ -0,0 +1,37 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/micromdm/micromdm/dep/depsync"
"github.com/pkg/errors"
)
func (cmd *applyCommand) applyDEPAutoAssigner(args []string) error {
flagset := flag.NewFlagSet("dep-autoassigner", flag.ExitOnError)
var (
flFilter = flagset.String("filter", "*", "filter string (only '*' supported right now)")
flProfileUUID = flagset.String("uuid", "", "DEP profile UUID to set")
)
flagset.Usage = usageFor(flagset, "mdmctl apply dep-autoassigner [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
if *flFilter == "" || *flProfileUUID == "" {
return errors.New("bad input: must provide both -filter and -uuid")
}
assigner := depsync.AutoAssigner{*flFilter, *flProfileUUID}
err := cmd.depsyncsvc.ApplyAutoAssigner(context.TODO(), &assigner)
if err != nil {
return err
}
fmt.Printf("saved auto-assign filter '%s' to DEP profile UUID '%s'\n", assigner.Filter, assigner.ProfileUUID)
fmt.Println("newly added DEP devices will be auto-assigned to the above profile UUID")
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"os"
"github.com/micromdm/dep"
"github.com/micromdm/micromdm/dep/depsync"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/pkg/crypto"
@@ -40,6 +41,7 @@ func (cmd *applyCommand) applyDEPProfile(args []string) error {
flTemplate = flagset.Bool("template", false, "print a JSON example of a DEP profile")
flAnchorFile = flagset.String("anchor", "", "filename of PEM cert(s) to add to anchor certs in template")
flUseServer = flagset.Bool("use-server-cert", false, "use the server cert(s) to add to anchor certs in template")
flFilter = flagset.String("filter", "", "set the auto-assign filter to for the defined profile")
)
flagset.Usage = usageFor(flagset, "mdmctl apply dep-profiles [flags]")
if err := flagset.Parse(args); err != nil {
@@ -97,6 +99,16 @@ func (cmd *applyCommand) applyDEPProfile(args []string) error {
// TODO: it would be nice to encode back a profile that save the
// UUID for future reference.
fmt.Printf("Defined DEP Profile with UUID %s\n", resp.ProfileUUID)
if *flFilter != "" {
assigner := depsync.AutoAssigner{*flFilter, resp.ProfileUUID}
err := cmd.depsyncsvc.ApplyAutoAssigner(context.TODO(), &assigner)
if err != nil {
return errors.Wrap(err, "set auto-assigner")
}
fmt.Printf("Saved auto-assign filter '%s' for this DEP profile\n", *flFilter)
}
return nil
}

View File

@@ -72,6 +72,8 @@ func (cmd *getCommand) Run(args []string) error {
run = cmd.getUsers
case "apps":
run = cmd.getApps
case "dep-autoassigners":
run = cmd.getDEPAutoAssigners
default:
cmd.Usage()
os.Exit(1)
@@ -92,6 +94,7 @@ Valid resource types:
* dep-devices
* dep-account
* dep-profiles
* dep-autoassigners
* users
* profiles
* apps

View File

@@ -0,0 +1,31 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"text/tabwriter"
)
func (cmd *getCommand) getDEPAutoAssigners(args []string) error {
flagset := flag.NewFlagSet("dep-autoassigner", flag.ExitOnError)
flagset.Usage = usageFor(flagset, "mdmctl get dep-autoassigner [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
assigners, err := cmd.depsyncsvc.GetAutoAssigners(context.TODO())
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintf(w, "Filter\tDEP Profile UUID\n")
for _, a := range assigners {
fmt.Fprintf(w, "%s\t%s\n", a.Filter, a.ProfileUUID)
}
w.Flush()
return nil
}

View File

@@ -46,6 +46,8 @@ func (cmd *removeCommand) Run(args []string) error {
run = cmd.removeProfiles
case "block":
run = cmd.removeBlock
case "dep-autoassigner":
run = cmd.removeDEPAutoAssigner
default:
cmd.Usage()
os.Exit(1)
@@ -63,6 +65,7 @@ Valid resource types:
* blueprints
* profiles
* block
* dep-autoassigner
`
fmt.Println(getUsage)

View File

@@ -0,0 +1,32 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/pkg/errors"
)
func (cmd *removeCommand) removeDEPAutoAssigner(args []string) error {
flagset := flag.NewFlagSet("dep-autoassigner", flag.ExitOnError)
var (
flFilter = flagset.String("filter", "*", "filter string (only '*' supported right now)")
)
flagset.Usage = usageFor(flagset, "mdmctl remove dep-autoassigner [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
if *flFilter == "" {
return errors.New("bad input: must provide -filter")
}
err := cmd.depsyncsvc.RemoveAutoAssigner(context.TODO(), *flFilter)
if err != nil {
return err
}
fmt.Printf("removed DEP profile associated with filter '%s'\n", *flFilter)
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/micromdm/micromdm/dep/depsync"
"github.com/micromdm/micromdm/platform/appstore"
"github.com/micromdm/micromdm/platform/blueprint"
"github.com/micromdm/micromdm/platform/config"
@@ -23,6 +24,7 @@ type remoteServices struct {
configsvc config.Service
appsvc appstore.Service
depsvc dep.Service
depsyncsvc depsync.Service
}
func setupClient(logger log.Logger) (*remoteServices, error) {
@@ -87,6 +89,13 @@ func setupClient(logger log.Logger) (*remoteServices, error) {
return nil, err
}
depsyncsvc, err := depsync.NewHTTPClient(
cfg.ServerURL, cfg.APIToken, logger,
httptransport.SetClient(skipVerifyHTTPClient(cfg.SkipVerify)))
if err != nil {
return nil, err
}
return &remoteServices{
profilesvc: profilesvc,
blueprintsvc: blueprintsvc,
@@ -96,5 +105,6 @@ func setupClient(logger log.Logger) (*remoteServices, error) {
configsvc: configsvc,
appsvc: appsvc,
depsvc: depsvc,
depsyncsvc: depsyncsvc,
}, nil
}

View File

@@ -301,8 +301,7 @@ func serve(args []string) error {
scepHandler := scep.ServiceHandler(ctx, sm.scepService, httpLogger)
enrollHandlers := enroll.MakeHTTPHandlers(ctx, enroll.MakeServerEndpoints(sm.enrollService, sm.scepDepot), httptransport.ServerErrorLogger(httpLogger))
syncNowEndpoint := depsync.MakeSyncNowEndpoint(depsync.NewRPC(syncer))
depsyncHandlers := depsync.MakeHTTPHandlers(ctx, depsync.Endpoints{SyncNowEndpoint: syncNowEndpoint}, connectOpts...)
depsyncEndpoints := depsync.MakeServerEndpoints(depsync.NewService(syncer))
r := mux.NewRouter()
r.Handle("/version", version.Handler())
@@ -325,6 +324,7 @@ func serve(args []string) error {
deviceHandler := device.MakeHTTPHandler(deviceEndpoints, logger)
depHandlers := depapi.MakeHTTPHandler(depEndpoints, logger)
apnsHandlers := apns.MakeHTTPHandler(apnsEndpoints, logger)
depsyncHandlers := depsync.MakeHTTPHandler(depsyncEndpoints, logger)
// API commands. Only handled if the user provides an api key.
if *flAPIKey != "" {
@@ -341,7 +341,8 @@ func serve(args []string) error {
r.Handle("/v1/dep/devices", apiAuthMiddleware(*flAPIKey, depHandlers))
r.Handle("/v1/dep/account", apiAuthMiddleware(*flAPIKey, depHandlers))
r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, depHandlers))
r.Handle("/v1/dep/syncnow", apiAuthMiddleware(*flAPIKey, depsyncHandlers.SyncNowHandler)).Methods("POST")
r.Handle("/v1/dep/syncnow", apiAuthMiddleware(*flAPIKey, depsyncHandlers))
r.Handle("/v1/dep/autoassigners", apiAuthMiddleware(*flAPIKey, depsyncHandlers))
r.Handle("/v1/commands", apiAuthMiddleware(*flAPIKey, commandHandlers.NewCommandHandler)).Methods("POST")
r.Handle("/push/{udid}", apiAuthMiddleware(*flAPIKey, apnsHandlers))
} else {

View File

@@ -0,0 +1,56 @@
package depsync
import (
"context"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/micromdm/micromdm/pkg/httputil"
)
func (s DEPSyncService) ApplyAutoAssigner(ctx context.Context, aa *AutoAssigner) error {
conf := s.syncer.GetConfig()
return conf.saveAutoAssigner(aa)
}
type applyAutoAssignerRequest struct {
*AutoAssigner
}
type applyAutoAssignerResponse struct {
Err error `json:"err,omitempty"`
}
func (r applyAutoAssignerResponse) Failed() error { return r.Err }
func MakeApplyAutoAssignerEndpoint(s Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(applyAutoAssignerRequest)
err := s.ApplyAutoAssigner(ctx, req.AutoAssigner)
return &applyAutoAssignerResponse{
Err: err,
}, nil
}
}
func decodeApplyAutoAssignerResponse(ctx context.Context, r *http.Response) (interface{}, error) {
var req applyAutoAssignerResponse
err := httputil.DecodeJSONResponse(r, &req)
return req, err
}
func decodeApplyAutoAssignerRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req applyAutoAssignerRequest
err := httputil.DecodeJSONRequest(r, &req)
return req, err
}
func (e Endpoints) ApplyAutoAssigner(ctx context.Context, aa *AutoAssigner) error {
request := applyAutoAssignerRequest{AutoAssigner: aa}
resp, err := e.ApplyAutoAssignerEndpoint(ctx, request)
if err != nil {
return err
}
response := resp.(applyAutoAssignerResponse)
return response.Err
}

69
dep/depsync/client.go Normal file
View File

@@ -0,0 +1,69 @@
package depsync
import (
"net/url"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/micromdm/micromdm/pkg/httputil"
)
func NewHTTPClient(instance, token string, logger log.Logger, opts ...httptransport.ClientOption) (Service, error) {
u, err := url.Parse(instance)
if err != nil {
return nil, err
}
var syncNowEndpoint endpoint.Endpoint
{
syncNowEndpoint = httptransport.NewClient(
"PUT",
httputil.CopyURL(u, "/v1/dep/autoassign"),
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
decodeEmptyResponse,
opts...,
).Endpoint()
}
var applyAutoAssignerEndpoint endpoint.Endpoint
{
applyAutoAssignerEndpoint = httptransport.NewClient(
"POST",
httputil.CopyURL(u, "/v1/dep/autoassigners"),
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
decodeApplyAutoAssignerResponse,
opts...,
).Endpoint()
}
var getAutoAssignersEndpoint endpoint.Endpoint
{
getAutoAssignersEndpoint = httptransport.NewClient(
"GET",
httputil.CopyURL(u, "/v1/dep/autoassigners"),
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
decodeGetAutoAssignersResponse,
opts...,
).Endpoint()
}
var removeAutoAssignerEndpoint endpoint.Endpoint
{
removeAutoAssignerEndpoint = httptransport.NewClient(
"DELETE",
httputil.CopyURL(u, "/v1/dep/autoassigners"),
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
decodeRemoveAutoAssignerResponse,
opts...,
).Endpoint()
}
return Endpoints{
SyncNowEndpoint: syncNowEndpoint,
ApplyAutoAssignerEndpoint: applyAutoAssignerEndpoint,
GetAutoAssignersEndpoint: getAutoAssignersEndpoint,
RemoveAutoAssignerEndpoint: removeAutoAssignerEndpoint,
}, nil
}

View File

@@ -27,6 +27,49 @@ func (cfg *config) Save() error {
return errors.Wrap(err, "saving dep sync cursor")
}
func (cfg *config) saveAutoAssigner(assigner *AutoAssigner) error {
if assigner.Filter != "*" {
return errors.New("only '*' filter auto-assigners supported")
}
err := cfg.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(AutoAssignBucket))
if err != nil {
return err
}
return b.Put([]byte(assigner.Filter), []byte(assigner.ProfileUUID))
})
return errors.Wrap(err, "saving auto-assigner")
}
func (cfg *config) loadAutoAssigners() ([]*AutoAssigner, error) {
assigners := []*AutoAssigner{}
err := cfg.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(AutoAssignBucket))
if b == nil { // bucket doesn't exist yet
return nil
}
return b.ForEach(func(k, v []byte) error {
assigners = append(assigners, &AutoAssigner{
Filter: string(k),
ProfileUUID: string(v),
})
return nil
})
})
return assigners, errors.Wrap(err, "loading auto-assigners")
}
func (cfg *config) deleteAutoAssigner(filter string) error {
return cfg.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(AutoAssignBucket))
if b == nil { // bucket doesn't exist yet
return nil
}
return b.Delete([]byte(filter))
})
}
func LoadConfig(db *bolt.DB) (*config, error) {
conf := config{DB: db}
err := db.Update(func(tx *bolt.Tx) error {

View File

@@ -18,8 +18,9 @@ import (
)
const (
SyncTopic = "mdm.DepSync"
ConfigBucket = "mdm.DEPConfig"
SyncTopic = "mdm.DepSync"
ConfigBucket = "mdm.DEPConfig"
AutoAssignBucket = "mdm.DEPAutoAssign"
syncDuration = 30 * time.Minute
cursorValidDuration = 7 * 24 * time.Hour
@@ -27,6 +28,12 @@ const (
type Syncer interface {
SyncNow()
GetConfig() *config // TODO: #302
}
type AutoAssigner struct {
Filter string `json:"filter"`
ProfileUUID string `json:"profile_uuid"`
}
type watcher struct {
@@ -157,6 +164,10 @@ func (w *watcher) SyncNow() {
w.syncNow <- true
}
func (w *watcher) GetConfig() *config {
return w.conf
}
// TODO this needs to be a proper error in the micromdm/dep package.
func isCursorExhausted(err error) bool {
return strings.Contains(err.Error(), "EXHAUSTED_CURSOR")
@@ -166,6 +177,109 @@ func isCursorExpired(err error) bool {
return strings.Contains(err.Error(), "EXPIRED_CURSOR")
}
// Process DEP messages and pull out filter-matching serial numbers
// associated to profile UUIDs for auto-assignment.
func (w *watcher) filteredAutoAssignments(devices []dep.Device) (map[string][]string, error) {
// load auto-assigners every run to make sure we get the latest set of
// auto-assigner profile UUIDs/filters. Note this makes every *watcher
// (i.e. every DEP sync instance) share the current DB set of auto-
// assigners. perhaps to refactor to be more separated.
assigners, err := w.conf.loadAutoAssigners()
if err != nil {
return nil, err
}
assigned := make(map[string][]string)
// skip looping over serials if we have no autoassigners
if len(assigners) < 1 {
return assigned, nil
}
for _, d := range devices {
// only process DEP "added" OpType messages
if d.OpType != "added" {
continue
}
// filter our devices by our assigner filters and get list of
// which devices are to be assigned to which profiles
for _, assigner := range assigners {
if assigner.Filter == "*" { // only supported filter type right now
if serials, ok := assigned[assigner.ProfileUUID]; ok {
assigned[assigner.ProfileUUID] = append(serials, d.SerialNumber)
} else {
assigned[assigner.ProfileUUID] = []string{d.SerialNumber}
}
}
}
}
return assigned, nil
}
func (w *watcher) processAutoAssign(devices []dep.Device) error {
assignments, err := w.filteredAutoAssignments(devices)
if err != nil {
return err
}
for profileUUID, serials := range assignments {
resp, err := w.client.AssignProfile(profileUUID, serials)
if err != nil {
level.Info(w.logger).Log(
"err", err,
"msg", "auto-assign error assigning serials to profile",
"profile", profileUUID,
)
continue
}
// count our results for logging
resultCounts := map[string]int{
"SUCCESS": 0,
"NOT_ACCESSIBLE": 0,
"FAILED": 0,
}
for _, result := range resp.Devices {
if ct, ok := resultCounts[result]; ok {
// NOTE: we're logging _only_ the above pre-defined result types
resultCounts[result] = ct + 1
}
}
// TODO: alternate strategy is to log all failed devices
// TODO: handle/requeue failed devices?
level.Info(w.logger).Log(
"msg", "DEP auto-assigned",
"profile", profileUUID,
"success", resultCounts["SUCCESS"],
"not_accessible", resultCounts["NOT_ACCESSIBLE"],
"failed", resultCounts["FAILED"],
)
}
return nil
}
func (w *watcher) publishAndProcessDevices(devices []dep.Device) error {
e := NewEvent(devices)
data, err := MarshalEvent(e)
if err != nil {
return err
}
err = w.publisher.Publish(context.TODO(), SyncTopic, data)
if err != nil {
return err
}
// TODO: instead of directly kicking off the auto-assigner process
// consider placing a subscriber on the DEP pubsub topic. The same
// information gets marshalled but it allows us the future
// flexibility to separate out that component if we desired.
go func() {
err := w.processAutoAssign(devices)
if err != nil {
level.Info(w.logger).Log("err", err, "msg", "auto-assign error")
}
}()
return nil
}
func (w *watcher) Run() error {
ticker := time.NewTicker(syncDuration).C
FETCH:
@@ -176,17 +290,18 @@ FETCH:
} else if err != nil {
return err
}
level.Info(w.logger).Log("msg", "DEP fetch", "more", resp.MoreToFollow, "cursor", resp.Cursor, "fetched", resp.FetchedUntil)
level.Info(w.logger).Log(
"msg", "DEP fetch",
"more", resp.MoreToFollow,
"cursor", resp.Cursor,
"fetched", resp.FetchedUntil,
"devices", len(resp.Devices),
)
w.conf.Cursor = cursor{Value: resp.Cursor, CreatedAt: time.Now()}
if err := w.conf.Save(); err != nil {
return errors.Wrap(err, "saving cursor from fetch")
}
e := NewEvent(resp.Devices)
data, err := MarshalEvent(e)
if err != nil {
return err
}
if err := w.publisher.Publish(context.TODO(), SyncTopic, data); err != nil {
if err := w.publishAndProcessDevices(resp.Devices); err != nil {
return err
}
if !resp.MoreToFollow {
@@ -203,22 +318,19 @@ SYNC:
} else if err != nil {
return err
}
if len(resp.Devices) != 0 {
level.Info(w.logger).Log("msg", "DEP sync", "more", resp.MoreToFollow, "cursor", resp.Cursor, "fetched", resp.FetchedUntil)
}
level.Info(w.logger).Log(
"msg", "DEP sync",
"more", resp.MoreToFollow,
"cursor", resp.Cursor,
"fetched", resp.FetchedUntil,
"devices", len(resp.Devices),
)
w.conf.Cursor = cursor{Value: resp.Cursor, CreatedAt: time.Now()}
if err := w.conf.Save(); err != nil {
return errors.Wrap(err, "saving cursor from sync")
}
if len(resp.Devices) > 0 {
e := NewEvent(resp.Devices)
data, err := MarshalEvent(e)
if err != nil {
return err
}
if err := w.publisher.Publish(context.TODO(), SyncTopic, data); err != nil {
return err
}
if err := w.publishAndProcessDevices(resp.Devices); err != nil {
return err
}
if !resp.MoreToFollow {
select {

View File

@@ -1,20 +0,0 @@
package depsync
import (
"context"
"github.com/go-kit/kit/endpoint"
)
type Endpoints struct {
SyncNowEndpoint endpoint.Endpoint
}
func MakeSyncNowEndpoint(s Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
s.SyncNow(ctx)
return syncNowResponse{}, nil
}
}
type syncNowResponse struct{}

View File

@@ -0,0 +1,47 @@
package depsync
import (
"context"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/micromdm/micromdm/pkg/httputil"
)
func (s DEPSyncService) GetAutoAssigners(ctx context.Context) ([]*AutoAssigner, error) {
conf := s.syncer.GetConfig()
return conf.loadAutoAssigners()
}
type getAutoAssignersResponse struct {
AutoAssigners []*AutoAssigner `json:"autoassigners"`
Err error `json:"err,omitempty"`
}
func (r getAutoAssignersResponse) Failed() error { return r.Err }
func MakeGetAutoAssignersEndpoint(s Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
assigners, err := s.GetAutoAssigners(ctx)
return &getAutoAssignersResponse{
AutoAssigners: assigners,
Err: err,
}, nil
}
}
func decodeGetAutoAssignersResponse(ctx context.Context, r *http.Response) (interface{}, error) {
var req getAutoAssignersResponse
err := httputil.DecodeJSONResponse(r, &req)
return req, err
}
func (e Endpoints) GetAutoAssigners(ctx context.Context) ([]*AutoAssigner, error) {
resp, err := e.GetAutoAssignersEndpoint(ctx, nil)
if err != nil {
return nil, err
}
response := resp.(getAutoAssignersResponse)
return response.AutoAssigners, response.Err
}

View File

@@ -0,0 +1,56 @@
package depsync
import (
"context"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/micromdm/micromdm/pkg/httputil"
)
func (s DEPSyncService) RemoveAutoAssigner(ctx context.Context, filter string) error {
conf := s.syncer.GetConfig()
return conf.deleteAutoAssigner(filter)
}
type removeAutoAssignerRequest struct {
Filter string `json:"filter"`
}
type removeAutoAssignerResponse struct {
Err error `json:"err,omitempty"`
}
func (r removeAutoAssignerResponse) Failed() error { return r.Err }
func MakeRemoveAutoAssignerEndpoint(s Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(removeAutoAssignerRequest)
err := s.RemoveAutoAssigner(ctx, req.Filter)
return &removeAutoAssignerResponse{
Err: err,
}, nil
}
}
func decodeRemoveAutoAssignerResponse(ctx context.Context, r *http.Response) (interface{}, error) {
var req removeAutoAssignerResponse
err := httputil.DecodeJSONResponse(r, &req)
return req, err
}
func decodeRemoveAutoAssignerRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req removeAutoAssignerRequest
err := httputil.DecodeJSONRequest(r, &req)
return req, err
}
func (e Endpoints) RemoveAutoAssigner(ctx context.Context, filter string) error {
request := removeAutoAssignerRequest{Filter: filter}
resp, err := e.RemoveAutoAssignerEndpoint(ctx, request)
if err != nil {
return err
}
response := resp.(removeAutoAssignerResponse)
return response.Err
}

69
dep/depsync/server.go Normal file
View File

@@ -0,0 +1,69 @@
package depsync
import (
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
"github.com/micromdm/micromdm/pkg/httputil"
)
func NewService(syncer Syncer) *DEPSyncService {
return &DEPSyncService{syncer: syncer}
}
type Endpoints struct {
SyncNowEndpoint endpoint.Endpoint
ApplyAutoAssignerEndpoint endpoint.Endpoint
GetAutoAssignersEndpoint endpoint.Endpoint
RemoveAutoAssignerEndpoint endpoint.Endpoint
}
func MakeServerEndpoints(s Service) Endpoints {
return Endpoints{
SyncNowEndpoint: MakeSyncNowEndpoint(s),
ApplyAutoAssignerEndpoint: MakeApplyAutoAssignerEndpoint(s),
GetAutoAssignersEndpoint: MakeGetAutoAssignersEndpoint(s),
RemoveAutoAssignerEndpoint: MakeRemoveAutoAssignerEndpoint(s),
}
}
func MakeHTTPHandler(e Endpoints, logger log.Logger) *mux.Router {
r, options := httputil.NewRouter(logger)
// POST /v1/dep/syncnow request a DEP sync operation to happen now
// POST /v1/dep/autoassigners set a DEP auto-assigner
// GET /v1/dep/autoassigners get list of DEP auto-assigners
// DELETE /v1/dep/autoassigners remove a DEP auto-assigner
r.Methods("POST").Path("/v1/dep/syncnow").Handler(httptransport.NewServer(
e.SyncNowEndpoint,
decodeEmptyRequest,
encodeEmptyResponse,
options...,
))
r.Methods("POST").Path("/v1/dep/autoassigners").Handler(httptransport.NewServer(
e.ApplyAutoAssignerEndpoint,
decodeApplyAutoAssignerRequest,
httputil.EncodeJSONResponse,
options...,
))
r.Methods("GET").Path("/v1/dep/autoassigners").Handler(httptransport.NewServer(
e.GetAutoAssignersEndpoint,
decodeEmptyRequest,
httputil.EncodeJSONResponse,
options...,
))
r.Methods("DELETE").Path("/v1/dep/autoassigners").Handler(httptransport.NewServer(
e.RemoveAutoAssignerEndpoint,
decodeRemoveAutoAssignerRequest,
httputil.EncodeJSONResponse,
options...,
))
return r
}

View File

@@ -5,18 +5,12 @@ import (
)
type Service interface {
SyncNow(ctx context.Context)
SyncNow(context.Context) error
ApplyAutoAssigner(context.Context, *AutoAssigner) error
GetAutoAssigners(context.Context) ([]*AutoAssigner, error)
RemoveAutoAssigner(context.Context, string) error
}
type syncNowService struct {
type DEPSyncService struct {
syncer Syncer
}
func (s *syncNowService) SyncNow(_ context.Context) {
s.syncer.SyncNow()
return
}
func NewRPC(syncer Syncer) *syncNowService {
return &syncNowService{syncer: syncer}
}

40
dep/depsync/syncnow.go Normal file
View File

@@ -0,0 +1,40 @@
package depsync
import (
"context"
"net/http"
"github.com/go-kit/kit/endpoint"
)
func (s *DEPSyncService) SyncNow(_ context.Context) error {
s.syncer.SyncNow()
return nil
}
type syncNowResponse struct{}
type syncNowRequest struct{}
func MakeSyncNowEndpoint(s Service) endpoint.Endpoint {
return func(ctx context.Context, _ interface{}) (interface{}, error) {
s.SyncNow(ctx)
return syncNowResponse{}, nil
}
}
func decodeEmptyRequest(ctx context.Context, r *http.Request) (interface{}, error) {
return nil, nil
}
func encodeEmptyResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
return nil
}
func decodeEmptyResponse(ctx context.Context, r *http.Response) (interface{}, error) {
return nil, nil
}
func (e Endpoints) SyncNow(ctx context.Context) error {
_, err := e.SyncNowEndpoint(ctx, nil)
return err
}

View File

@@ -1,31 +0,0 @@
package depsync
import (
"context"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
)
type HTTPHandlers struct {
SyncNowHandler http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
return HTTPHandlers{
SyncNowHandler: httptransport.NewServer(
endpoints.SyncNowEndpoint,
decodeEmptyRequest,
encodeEmptyResponse,
opts...,
),
}
}
func decodeEmptyRequest(ctx context.Context, r *http.Request) (interface{}, error) {
return nil, nil
}
func encodeEmptyResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
return nil
}

View File

@@ -293,31 +293,46 @@ func (db *DB) pollCheckin(pubsubSvc pubsub.PublishSubscriber) error {
}
fmt.Printf("got %d devices from DEP\n", len(ev.Devices))
for _, d := range ev.Devices {
newDevice := new(device.Device)
bySerial, err := db.DeviceBySerial(d.SerialNumber)
if err == nil && bySerial != nil { // must be a DEP device
fmt.Printf("existing device checked in from DEP: %s\n", d.SerialNumber)
newDevice = bySerial
}
if err != nil && !isNotFound(err) {
fmt.Println(err) // some other issue is going on
updDevice, updDeviceErr := db.DeviceBySerial(d.SerialNumber)
if updDeviceErr != nil && !isNotFound(updDeviceErr) {
fmt.Printf("error getting device %s: %s\n", d.SerialNumber, err)
continue
}
if newDevice.UUID == "" { // previously unknown
newDevice.UUID = uuid.NewV4().String()
if updDeviceErr != nil && isNotFound(updDeviceErr) {
updDevice = new(device.Device)
if d.OpType == "modified" {
fmt.Printf("warning: no existing device for DEP device update: %s\n", d.SerialNumber)
}
}
newDevice.SerialNumber = d.SerialNumber
newDevice.Model = d.Model
newDevice.Description = d.Description
newDevice.Color = d.Color
newDevice.AssetTag = d.AssetTag
newDevice.DEPProfileStatus = device.DEPProfileStatus(d.ProfileStatus)
newDevice.DEPProfileUUID = d.ProfileUUID
newDevice.DEPProfileAssignTime = d.ProfileAssignTime
newDevice.DEPProfileAssignedDate = d.DeviceAssignedDate
newDevice.DEPProfileAssignedBy = d.DeviceAssignedBy
// TODO: deal with sync fields OpType, OpDate
if err := db.Save(newDevice); err != nil {
if updDeviceErr == nil && d.OpType == "added" {
// consider issuing this warning if op_type == "" as well.
// in that case it's likely the device came from a DEP
// fetch (vs. a sync) which could be a re-fetch of devices
fmt.Printf("warning: re-adding existing DEP device: %s\n", d.SerialNumber)
}
if d.OpType == "deleted" {
fmt.Printf("warning: DEP device unassigned: %s\n", d.SerialNumber)
}
if updDevice.UUID == "" {
// generate UUID for any device that doesn't have one
updDevice.UUID = uuid.NewV4().String()
}
updDevice.SerialNumber = d.SerialNumber
updDevice.Model = d.Model
updDevice.Description = d.Description
updDevice.Color = d.Color
updDevice.AssetTag = d.AssetTag
updDevice.DEPProfileStatus = device.DEPProfileStatus(d.ProfileStatus)
updDevice.DEPProfileUUID = d.ProfileUUID
updDevice.DEPProfileAssignTime = d.ProfileAssignTime
updDevice.DEPProfileAssignedDate = d.DeviceAssignedDate
updDevice.DEPProfileAssignedBy = d.DeviceAssignedBy
// TODO: support profile_push_time, os, device_family, op_date
if err := db.Save(updDevice); err != nil {
fmt.Println(err)
continue
}