From 431a09998cc040a58742da64b5e4496f50f3fb58 Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Sun, 10 Dec 2017 01:21:59 -0500 Subject: [PATCH] refactor app upload APIs (#353) Closes #296 --- cmd/mdmctl/apply_app.go | 2 +- cmd/mdmctl/get_app.go | 4 +- cmd/mdmctl/setup.go | 10 ++ cmd/micromdm/serve.go | 23 ++- platform/api/server/apply/client.go | 12 -- platform/api/server/apply/endpoint.go | 40 ------ platform/api/server/apply/service.go | 21 --- platform/api/server/apply/transport_http.go | 75 ---------- platform/api/server/list/client.go | 12 -- platform/api/server/list/endpoint.go | 37 ----- platform/api/server/list/service.go | 32 ----- platform/api/server/list/transport_http.go | 24 ---- platform/appstore/{ => builtin}/appstore.go | 12 +- platform/appstore/client.go | 45 ++++++ platform/appstore/get_apps.go | 86 +++++++++++ platform/appstore/server.go | 45 ++++++ platform/appstore/service.go | 27 ++++ platform/appstore/upload_app.go | 133 ++++++++++++++++++ .../upload_app_test.go} | 2 +- 19 files changed, 364 insertions(+), 278 deletions(-) rename platform/appstore/{ => builtin}/appstore.go (86%) create mode 100644 platform/appstore/client.go create mode 100644 platform/appstore/get_apps.go create mode 100644 platform/appstore/server.go create mode 100644 platform/appstore/service.go create mode 100644 platform/appstore/upload_app.go rename platform/{api/server/apply/transport_http_test.go => appstore/upload_app_test.go} (98%) diff --git a/cmd/mdmctl/apply_app.go b/cmd/mdmctl/apply_app.go index b96d31a4..94e1e63e 100644 --- a/cmd/mdmctl/apply_app.go +++ b/cmd/mdmctl/apply_app.go @@ -106,7 +106,7 @@ Please rebuild the package and re-run the command. if _, err := f.Seek(0, 0); err != nil { return errors.Wrap(err, "reset pkg file reader") } - err := cmd.applysvc.UploadApp(context.TODO(), nameMannifest(f.Name()), &buf, filepath.Base(f.Name()), f) + err := cmd.appsvc.UploadApp(context.TODO(), nameMannifest(f.Name()), &buf, filepath.Base(f.Name()), f) if err != nil { return err } diff --git a/cmd/mdmctl/get_app.go b/cmd/mdmctl/get_app.go index 19579915..df5e0931 100644 --- a/cmd/mdmctl/get_app.go +++ b/cmd/mdmctl/get_app.go @@ -8,7 +8,7 @@ import ( "os" "text/tabwriter" - "github.com/micromdm/micromdm/platform/api/server/list" + "github.com/micromdm/micromdm/platform/appstore" ) type appsTableOutput struct{ w *tabwriter.Writer } @@ -32,7 +32,7 @@ func (cmd *getCommand) getApps(args []string) error { return err } ctx := context.Background() - apps, err := cmd.list.ListApplications(ctx, list.ListAppsOption{ + apps, err := cmd.appsvc.ListApplications(ctx, appstore.ListAppsOption{ FilterName: []string{*flNameFilter}, }) if err != nil { diff --git a/cmd/mdmctl/setup.go b/cmd/mdmctl/setup.go index 69b9dc51..026df5dc 100644 --- a/cmd/mdmctl/setup.go +++ b/cmd/mdmctl/setup.go @@ -6,6 +6,7 @@ import ( "github.com/micromdm/micromdm/platform/api/server/apply" "github.com/micromdm/micromdm/platform/api/server/list" + "github.com/micromdm/micromdm/platform/appstore" "github.com/micromdm/micromdm/platform/blueprint" "github.com/micromdm/micromdm/platform/config" "github.com/micromdm/micromdm/platform/profile" @@ -19,6 +20,7 @@ type remoteServices struct { blocksvc remove.Service usersvc user.Service configsvc config.Service + appsvc appstore.Service applysvc apply.Service list list.Service } @@ -64,6 +66,13 @@ func setupClient(logger log.Logger) (*remoteServices, error) { return nil, err } + appsvc, err := appstore.NewHTTPClient( + cfg.ServerURL, cfg.APIToken, logger, + httptransport.SetClient(skipVerifyHTTPClient(cfg.SkipVerify))) + if err != nil { + return nil, err + } + applysvc, err := apply.NewClient( cfg.ServerURL, logger, cfg.APIToken, httptransport.SetClient(skipVerifyHTTPClient(cfg.SkipVerify))) @@ -84,6 +93,7 @@ func setupClient(logger log.Logger) (*remoteServices, error) { blocksvc: blocksvc, usersvc: usersvc, configsvc: configsvc, + appsvc: appsvc, applysvc: applysvc, list: listsvc, }, nil diff --git a/cmd/micromdm/serve.go b/cmd/micromdm/serve.go index 5920a750..7f128fe1 100644 --- a/cmd/micromdm/serve.go +++ b/cmd/micromdm/serve.go @@ -47,6 +47,7 @@ import ( "github.com/micromdm/micromdm/platform/api/server/list" "github.com/micromdm/micromdm/platform/apns" "github.com/micromdm/micromdm/platform/appstore" + appsbuiltin "github.com/micromdm/micromdm/platform/appstore/builtin" "github.com/micromdm/micromdm/platform/blueprint" blueprintbuiltin "github.com/micromdm/micromdm/platform/blueprint/builtin" "github.com/micromdm/micromdm/platform/command" @@ -256,7 +257,7 @@ func serve(args []string) error { if err != nil { stdlog.Fatalf("creating DEP client: %s\n", err) } - appDB := &appstore.Repo{Path: *flRepoPath} + appDB := &appsbuiltin.Repo{Path: *flRepoPath} var profilesvc profile.Service { @@ -287,12 +288,18 @@ func serve(args []string) error { configEndpoints := config.MakeServerEndpoints(configsvc) + var appsvc appstore.Service + { + appsvc = appstore.New(appDB) + } + + appEndpoints := appstore.MakeServerEndpoints(appsvc) + var listsvc list.Service { l := &list.ListService{ DEPClient: dc, Devices: devDB, - Apps: appDB, } listsvc = l @@ -310,14 +317,12 @@ func serve(args []string) error { GetDEPAccountInfoEndpoint: list.MakeGetDEPAccountInfoEndpoint(listsvc), GetDEPProfileEndpoint: list.MakeGetDEPProfileEndpoint(listsvc), GetDEPDeviceEndpoint: list.MakeGetDEPDeviceDetailsEndpoint(listsvc), - ListAppsEndpont: list.MakeListAppsEndpoint(listsvc), } var applysvc apply.Service { l := &apply.ApplyService{ DEPClient: dc, - Apps: appDB, } applysvc = l if err := l.WatchTokenUpdates(sm.pubclient); err != nil { @@ -330,14 +335,8 @@ func serve(args []string) error { defineDEPProfileEndpoint = apply.MakeDefineDEPProfile(applysvc) } - var appUploadEndpoint endpoint.Endpoint - { - appUploadEndpoint = apply.MakeUploadAppEndpiont(applysvc) - } - applyEndpoints := apply.Endpoints{ DefineDEPProfileEndpoint: defineDEPProfileEndpoint, - AppUploadEndpoint: appUploadEndpoint, } applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...) @@ -365,12 +364,14 @@ func serve(args []string) error { blockhandler := block.MakeHTTPHandler(blockEndpoints, logger) userHandler := user.MakeHTTPHandler(userEndpoints, logger) configHandler := config.MakeHTTPHandler(configEndpoints, logger) + appsHandler := appstore.MakeHTTPHandler(appEndpoints, logger) // API commands. Only handled if the user provides an api key. if *flAPIKey != "" { r.Handle("/v1/profiles", apiAuthMiddleware(*flAPIKey, profilesHandler)) r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, blueprintsHandler)) r.Handle("/v1/users", apiAuthMiddleware(*flAPIKey, userHandler)) + r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, appsHandler)) r.Handle("/v1/devices/{udid}/block", apiAuthMiddleware(*flAPIKey, blockhandler)) r.Handle("/v1/devices/{udid}/unblock", apiAuthMiddleware(*flAPIKey, blockhandler)) r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, configHandler)) @@ -383,8 +384,6 @@ func serve(args []string) error { r.Handle("/v1/dep/account", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPAccountInfoHandler)).Methods("GET") r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPProfileHandler)).Methods("GET") r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DefineDEPProfileHandler)).Methods("POST") - r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.AppUploadHandler)).Methods("POST") - r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListAppsHandler)).Methods("GET") } if *flRepoPath != "" { diff --git a/platform/api/server/apply/client.go b/platform/api/server/apply/client.go index 49e325e3..f0765518 100644 --- a/platform/api/server/apply/client.go +++ b/platform/api/server/apply/client.go @@ -27,20 +27,8 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra ).Endpoint() } - var uploadAppEndpoint endpoint.Endpoint - { - uploadAppEndpoint = httptransport.NewClient( - "POST", - copyURL(u, "/v1/apps"), - encodeRequestWithToken(token, EncodeUploadAppRequest), - DecodeUploadAppResponse, - opts..., - ).Endpoint() - } - return Endpoints{ DefineDEPProfileEndpoint: defineDEPProfileEndpoint, - AppUploadEndpoint: uploadAppEndpoint, }, nil } diff --git a/platform/api/server/apply/endpoint.go b/platform/api/server/apply/endpoint.go index 945a8504..7f3c2df0 100644 --- a/platform/api/server/apply/endpoint.go +++ b/platform/api/server/apply/endpoint.go @@ -2,7 +2,6 @@ package apply import ( "context" - "io" "github.com/go-kit/kit/endpoint" "github.com/micromdm/dep" @@ -10,21 +9,6 @@ import ( type Endpoints struct { DefineDEPProfileEndpoint endpoint.Endpoint - AppUploadEndpoint endpoint.Endpoint -} - -func (e Endpoints) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error { - request := appUploadRequest{ - ManifestName: manifestName, - ManifestFile: manifest, - PKGFilename: pkgName, - PKGFile: pkg, - } - resp, err := e.AppUploadEndpoint(ctx, request) - if err != nil { - return err - } - return resp.(appUploadResponse).Err } func (e Endpoints) DefineDEPProfile(ctx context.Context, p *dep.Profile) (*dep.ProfileResponse, error) { @@ -48,30 +32,6 @@ func MakeDefineDEPProfile(svc Service) endpoint.Endpoint { } } -func MakeUploadAppEndpiont(svc Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(appUploadRequest) - err = svc.UploadApp(ctx, req.ManifestName, req.ManifestFile, req.PKGFilename, req.PKGFile) - return &appUploadResponse{ - Err: err, - }, nil - } -} - -type appUploadRequest struct { - ManifestName string - ManifestFile io.Reader - - PKGFilename string - PKGFile io.Reader -} - -type appUploadResponse struct { - Err error `json:"err,omitempty"` -} - -func (r appUploadResponse) error() error { return r.Err } - type depProfileRequest struct{ *dep.Profile } type depProfileResponse struct { *dep.ProfileResponse diff --git a/platform/api/server/apply/service.go b/platform/api/server/apply/service.go index 1c7beb6f..d1a797c9 100644 --- a/platform/api/server/apply/service.go +++ b/platform/api/server/apply/service.go @@ -3,43 +3,22 @@ package apply import ( "context" "encoding/json" - "io" "log" "sync" "github.com/micromdm/dep" - "github.com/micromdm/micromdm/platform/appstore" "github.com/micromdm/micromdm/platform/config" "github.com/micromdm/micromdm/platform/pubsub" ) type Service interface { - UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error DEPService } type ApplyService struct { mtx sync.RWMutex DEPClient dep.Client - - Apps appstore.AppStore -} - -func (svc *ApplyService) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error { - if manifestName != "" { - if err := svc.Apps.SaveFile(manifestName, manifest); err != nil { - return err - } - } - - if pkgName != "" { - if err := svc.Apps.SaveFile(pkgName, pkg); err != nil { - return err - } - } - - return nil } func (svc *ApplyService) WatchTokenUpdates(pubsub pubsub.Subscriber) error { diff --git a/platform/api/server/apply/transport_http.go b/platform/api/server/apply/transport_http.go index 57ad2639..e2eb1114 100644 --- a/platform/api/server/apply/transport_http.go +++ b/platform/api/server/apply/transport_http.go @@ -4,9 +4,7 @@ import ( "bytes" "context" "encoding/json" - "io" "io/ioutil" - "mime/multipart" "net/http" httptransport "github.com/go-kit/kit/transport/http" @@ -26,12 +24,6 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran encodeResponse, opts..., ), - AppUploadHandler: httptransport.NewServer( - endpoints.AppUploadEndpoint, - decodeAppUploadRequest, - encodeResponse, - opts..., - ), } return h } @@ -44,64 +36,6 @@ func decodeDEPProfileRequest(ctx context.Context, r *http.Request) (interface{}, return req, nil } -func decodeAppUploadRequest(ctx context.Context, r *http.Request) (interface{}, error) { - defer r.Body.Close() - appManifestFilename := r.FormValue("app_manifest_filename") - manifestFile, _, err := r.FormFile("app_manifest_filedata") - if err != nil && err != http.ErrMissingFile { - return nil, errors.Wrap(err, "manifest file") - } - pkgFilename := r.FormValue("pkg_name") - pkgFile, _, err := r.FormFile("pkg_filedata") - if err != nil && err != http.ErrMissingFile { - return nil, err - } - - return appUploadRequest{ - ManifestName: appManifestFilename, - ManifestFile: manifestFile, - PKGFilename: pkgFilename, - PKGFile: pkgFile, - }, nil -} - -func EncodeUploadAppRequest(_ context.Context, r *http.Request, request interface{}) error { - req := request.(appUploadRequest) - body := new(bytes.Buffer) - writer := multipart.NewWriter(body) - - if req.ManifestName != "" { - partManifest, err := writer.CreateFormFile("app_manifest_filedata", req.ManifestName) - if err != nil { - return err - } - _, err = io.Copy(partManifest, req.ManifestFile) - if err != nil { - return errors.Wrap(err, "copying appmanifest file to multipart writer") - } - writer.WriteField("app_manifest_filename", req.ManifestName) - } - - if req.PKGFilename != "" { - partPkg, err := writer.CreateFormFile("pkg_filedata", req.PKGFilename) - if err != nil { - return err - } - _, err = io.Copy(partPkg, req.PKGFile) - if err != nil { - return errors.Wrap(err, "copying pkg file to multipart writer") - } - writer.WriteField("pkg_name", req.PKGFilename) - } - if err := writer.Close(); err != nil { - return errors.Wrap(err, "closing multipart writer") - } - - r.Header.Set("Content-Type", writer.FormDataContentType()) - r.Body = ioutil.NopCloser(body) - return nil -} - type errorWrapper struct { Error string `json:"error"` } @@ -155,12 +89,3 @@ func DecodeDEPProfileResponse(_ context.Context, r *http.Response) (interface{}, err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } - -func DecodeUploadAppResponse(_ context.Context, r *http.Response) (interface{}, error) { - if r.StatusCode != http.StatusOK { - return nil, errorDecoder(r) - } - var resp appUploadResponse - err := json.NewDecoder(r.Body).Decode(&resp) - return resp, err -} diff --git a/platform/api/server/list/client.go b/platform/api/server/list/client.go index b4f479bf..7c73b2b6 100644 --- a/platform/api/server/list/client.go +++ b/platform/api/server/list/client.go @@ -60,23 +60,11 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra ).Endpoint() } - var listAppsEndpoint endpoint.Endpoint - { - listAppsEndpoint = httptransport.NewClient( - "GET", - copyURL(u, "/v1/apps"), - encodeRequestWithToken(token, EncodeHTTPGenericRequest), - DecodeListAppsResponse, - opts..., - ).Endpoint() - } - return Endpoints{ ListDevicesEndpoint: listDevicesEndpoint, GetDEPAccountInfoEndpoint: getDEPAccountInfoEndpoint, GetDEPDeviceEndpoint: getDEPDeviceDetailsEndpoint, GetDEPProfileEndpoint: getDEPProfilesEndpoint, - ListAppsEndpont: listAppsEndpoint, }, nil } diff --git a/platform/api/server/list/endpoint.go b/platform/api/server/list/endpoint.go index f61ee3dc..25b6be16 100644 --- a/platform/api/server/list/endpoint.go +++ b/platform/api/server/list/endpoint.go @@ -13,7 +13,6 @@ type Endpoints struct { GetDEPAccountInfoEndpoint endpoint.Endpoint GetDEPDeviceEndpoint endpoint.Endpoint GetDEPProfileEndpoint endpoint.Endpoint - ListAppsEndpont endpoint.Endpoint } func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) { @@ -25,15 +24,6 @@ func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]D return response.(devicesResponse).Devices, response.(devicesResponse).Err } -func (e Endpoints) ListApplications(ctx context.Context, opts ListAppsOption) ([]AppDTO, error) { - request := appListRequest{opts} - response, err := e.ListAppsEndpont(ctx, request.Opts) - if err != nil { - return nil, err - } - return response.(appListResponse).Apps, response.(appListResponse).Err -} - func (e Endpoints) GetDEPAccountInfo(ctx context.Context) (*dep.Account, error) { request := depAccountInforequest{} response, err := e.GetDEPAccountInfoEndpoint(ctx, request) @@ -63,17 +53,6 @@ func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint { } } -func MakeListAppsEndpoint(svc Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(appListRequest) - apps, err := svc.ListApplications(ctx, req.Opts) - return appListResponse{ - Apps: apps, - Err: err, - }, nil - } -} - func (e Endpoints) GetDEPProfile(ctx context.Context, uuid string) (*dep.Profile, error) { request := depProfileRequest{UUID: uuid} response, err := e.GetDEPProfileEndpoint(ctx, request) @@ -147,19 +126,3 @@ type depProfileResponse struct { } func (r depProfileResponse) error() error { return r.Err } - -type appListRequest struct { - Opts ListAppsOption -} - -type AppDTO struct { - Name string `json:"name"` - Payload []byte `json:"payload,omitempty"` -} - -type appListResponse struct { - Apps []AppDTO `json:"apps,omitempty"` - Err error `json:"err,omitempty"` -} - -func (r appListResponse) error() error { return r.Err } diff --git a/platform/api/server/list/service.go b/platform/api/server/list/service.go index 975d0668..5b90d9ea 100644 --- a/platform/api/server/list/service.go +++ b/platform/api/server/list/service.go @@ -6,11 +6,8 @@ import ( "log" "sync" - "github.com/groob/plist" "github.com/micromdm/dep" - "github.com/pkg/errors" - "github.com/micromdm/micromdm/platform/appstore" "github.com/micromdm/micromdm/platform/config" "github.com/micromdm/micromdm/platform/device" "github.com/micromdm/micromdm/platform/pubsub" @@ -24,13 +21,8 @@ type ListDevicesOption struct { FilterUDID []string } -type ListAppsOption struct { - FilterName []string `json:"filter_name"` -} - type Service interface { ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) - ListApplications(ctx context.Context, opt ListAppsOption) ([]AppDTO, error) DEPService } @@ -39,30 +31,6 @@ type ListService struct { DEPClient dep.Client Devices *device.DB - Apps appstore.AppStore -} - -func (svc *ListService) ListApplications(ctx context.Context, opts ListAppsOption) ([]AppDTO, error) { - var filter string - if len(opts.FilterName) == 1 { - filter = opts.FilterName[0] - } - apps, err := svc.Apps.Apps(filter) - if err != nil { - return nil, err - } - var appList []AppDTO - for name, app := range apps { - payload, err := plist.MarshalIndent(&app, " ") - if err != nil { - return nil, errors.Wrap(err, "create dto payload") - } - appList = append(appList, AppDTO{ - Name: name, - Payload: payload, - }) - } - return appList, nil } func (svc *ListService) WatchTokenUpdates(pubsub pubsub.Subscriber) error { diff --git a/platform/api/server/list/transport_http.go b/platform/api/server/list/transport_http.go index 309057bd..ef88dccd 100644 --- a/platform/api/server/list/transport_http.go +++ b/platform/api/server/list/transport_http.go @@ -16,7 +16,6 @@ type HTTPHandlers struct { GetDEPAccountInfoHandler http.Handler GetDEPProfileHandler http.Handler GetDEPDeviceDetailsHandler http.Handler - ListAppsHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -45,12 +44,6 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran encodeResponse, opts..., ), - ListAppsHandler: httptransport.NewServer( - endpoints.ListAppsEndpont, - decodeListAppsRequest, - encodeResponse, - opts..., - ), } return h } @@ -82,14 +75,6 @@ func decodeDEPProfileRequest(ctx context.Context, r *http.Request) (interface{}, return request, nil } -func decodeListAppsRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var request appListRequest - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - return nil, err - } - return request, nil -} - func errorDecoder(r *http.Response) error { var w errorWrapper if err := json.NewDecoder(r.Body).Decode(&w); err != nil { @@ -170,12 +155,3 @@ func DecodeDEPProfileResponse(_ context.Context, r *http.Response) (interface{}, err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } - -func DecodeListAppsResponse(_ context.Context, r *http.Response) (interface{}, error) { - if r.StatusCode != http.StatusOK { - return nil, errorDecoder(r) - } - var resp appListResponse - err := json.NewDecoder(r.Body).Decode(&resp) - return resp, err -} diff --git a/platform/appstore/appstore.go b/platform/appstore/builtin/appstore.go similarity index 86% rename from platform/appstore/appstore.go rename to platform/appstore/builtin/appstore.go index 2c416373..a7de159e 100644 --- a/platform/appstore/appstore.go +++ b/platform/appstore/builtin/appstore.go @@ -1,6 +1,6 @@ -// package appstore provides an abstraction for uploading files and manifests -// to a repository. -package appstore +// package builtin provides an abstraction for uploading files and manifests +// to a file repository. +package builtin import ( "io" @@ -15,12 +15,6 @@ import ( "github.com/micromdm/micromdm/mdm/appmanifest" ) -type AppStore interface { - SaveFile(name string, f io.Reader) error - Manifest(name string) (*appmanifest.Manifest, error) - Apps(name string) (map[string]appmanifest.Manifest, error) -} - type Repo struct { Path string } diff --git a/platform/appstore/client.go b/platform/appstore/client.go new file mode 100644 index 00000000..d426af47 --- /dev/null +++ b/platform/appstore/client.go @@ -0,0 +1,45 @@ +package appstore + +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 appUploadEndpoint endpoint.Endpoint + { + appUploadEndpoint = httptransport.NewClient( + "POST", + httputil.CopyURL(u, "/v1/apps"), + httputil.EncodeRequestWithToken(token, encodeUploadAppRequest), + decodeUploadAppResponse, + opts..., + ).Endpoint() + } + + var listAppsEndpoint endpoint.Endpoint + { + listAppsEndpoint = httptransport.NewClient( + "GET", + httputil.CopyURL(u, "/v1/apps"), + httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest), + decodeListAppsResponse, + opts..., + ).Endpoint() + } + + return Endpoints{ + AppUploadEndpoint: appUploadEndpoint, + ListAppsEndpoint: listAppsEndpoint, + }, nil +} diff --git a/platform/appstore/get_apps.go b/platform/appstore/get_apps.go new file mode 100644 index 00000000..b42b9419 --- /dev/null +++ b/platform/appstore/get_apps.go @@ -0,0 +1,86 @@ +package appstore + +import ( + "context" + "net/http" + + "github.com/go-kit/kit/endpoint" + "github.com/groob/plist" + "github.com/micromdm/micromdm/pkg/httputil" + "github.com/pkg/errors" +) + +type ListAppsOption struct { + FilterName []string `json:"filter_name"` +} + +type AppDTO struct { + Name string `json:"name"` + Payload []byte `json:"payload,omitempty"` +} + +func (svc *AppService) ListApplications(ctx context.Context, opts ListAppsOption) ([]AppDTO, error) { + var filter string + if len(opts.FilterName) == 1 { + filter = opts.FilterName[0] + } + apps, err := svc.store.Apps(filter) + if err != nil { + return nil, err + } + var appList []AppDTO + for name, app := range apps { + payload, err := plist.MarshalIndent(&app, " ") + if err != nil { + return nil, errors.Wrap(err, "create dto payload") + } + appList = append(appList, AppDTO{ + Name: name, + Payload: payload, + }) + } + return appList, nil +} + +type appListRequest struct { + Opts ListAppsOption +} + +type appListResponse struct { + Apps []AppDTO `json:"apps,omitempty"` + Err error `json:"err,omitempty"` +} + +func (r appListResponse) Failed() error { return r.Err } + +func decodeListAppsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req appListRequest + err := httputil.DecodeJSONRequest(r, &req) + return req, err +} + +func decodeListAppsResponse(_ context.Context, r *http.Response) (interface{}, error) { + var resp appListResponse + err := httputil.DecodeJSONResponse(r, &resp) + return resp, err +} + +func MakeListAppsEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(appListRequest) + apps, err := svc.ListApplications(ctx, req.Opts) + return appListResponse{ + Apps: apps, + Err: err, + }, nil + } +} + +func (e Endpoints) ListApplications(ctx context.Context, opts ListAppsOption) ([]AppDTO, error) { + request := appListRequest{opts} + response, err := e.ListAppsEndpoint(ctx, request.Opts) + if err != nil { + return nil, err + } + return response.(appListResponse).Apps, response.(appListResponse).Err +} diff --git a/platform/appstore/server.go b/platform/appstore/server.go new file mode 100644 index 00000000..b488accf --- /dev/null +++ b/platform/appstore/server.go @@ -0,0 +1,45 @@ +package appstore + +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" +) + +type Endpoints struct { + AppUploadEndpoint endpoint.Endpoint + ListAppsEndpoint endpoint.Endpoint +} + +func MakeServerEndpoints(s Service) Endpoints { + return Endpoints{ + AppUploadEndpoint: MakeUploadAppEndpiont(s), + ListAppsEndpoint: MakeListAppsEndpoint(s), + } +} + +func MakeHTTPHandler(e Endpoints, logger log.Logger) *mux.Router { + r, options := httputil.NewRouter(logger) + + // POST /v1/apps upload an app to the server + // GET /v1/apps list apps managed by the server + + r.Methods("POST").Path("/v1/apps").Handler(httptransport.NewServer( + e.AppUploadEndpoint, + decodeAppUploadRequest, + httputil.EncodeJSONResponse, + options..., + )) + + r.Methods("GET").Path("/v1/apps").Handler(httptransport.NewServer( + e.ListAppsEndpoint, + decodeListAppsRequest, + httputil.EncodeJSONResponse, + options..., + )) + + return r +} diff --git a/platform/appstore/service.go b/platform/appstore/service.go new file mode 100644 index 00000000..8f6dae7a --- /dev/null +++ b/platform/appstore/service.go @@ -0,0 +1,27 @@ +package appstore + +import ( + "context" + "io" + + "github.com/micromdm/micromdm/mdm/appmanifest" +) + +type Service interface { + UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error + ListApplications(ctx context.Context, opt ListAppsOption) ([]AppDTO, error) +} + +type AppService struct { + store Store +} + +type Store interface { + SaveFile(name string, f io.Reader) error + Manifest(name string) (*appmanifest.Manifest, error) + Apps(name string) (map[string]appmanifest.Manifest, error) +} + +func New(store Store) *AppService { + return &AppService{store: store} +} diff --git a/platform/appstore/upload_app.go b/platform/appstore/upload_app.go new file mode 100644 index 00000000..5c8df24c --- /dev/null +++ b/platform/appstore/upload_app.go @@ -0,0 +1,133 @@ +package appstore + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + + "github.com/go-kit/kit/endpoint" + "github.com/pkg/errors" + + "github.com/micromdm/micromdm/pkg/httputil" +) + +func (svc *AppService) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error { + if manifestName != "" { + if err := svc.store.SaveFile(manifestName, manifest); err != nil { + return err + } + } + + if pkgName != "" { + if err := svc.store.SaveFile(pkgName, pkg); err != nil { + return err + } + } + + return nil +} + +type appUploadRequest struct { + ManifestName string + ManifestFile io.Reader + + PKGFilename string + PKGFile io.Reader +} + +type appUploadResponse struct { + Err error `json:"err,omitempty"` +} + +func (r appUploadResponse) Failed() error { return r.Err } + +func decodeAppUploadRequest(ctx context.Context, r *http.Request) (interface{}, error) { + defer r.Body.Close() + appManifestFilename := r.FormValue("app_manifest_filename") + manifestFile, _, err := r.FormFile("app_manifest_filedata") + if err != nil && err != http.ErrMissingFile { + return nil, errors.Wrap(err, "manifest file") + } + pkgFilename := r.FormValue("pkg_name") + pkgFile, _, err := r.FormFile("pkg_filedata") + if err != nil && err != http.ErrMissingFile { + return nil, err + } + + return appUploadRequest{ + ManifestName: appManifestFilename, + ManifestFile: manifestFile, + PKGFilename: pkgFilename, + PKGFile: pkgFile, + }, nil +} + +func encodeUploadAppRequest(_ context.Context, r *http.Request, request interface{}) error { + req := request.(appUploadRequest) + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + if req.ManifestName != "" { + partManifest, err := writer.CreateFormFile("app_manifest_filedata", req.ManifestName) + if err != nil { + return err + } + _, err = io.Copy(partManifest, req.ManifestFile) + if err != nil { + return errors.Wrap(err, "copying appmanifest file to multipart writer") + } + writer.WriteField("app_manifest_filename", req.ManifestName) + } + + if req.PKGFilename != "" { + partPkg, err := writer.CreateFormFile("pkg_filedata", req.PKGFilename) + if err != nil { + return err + } + _, err = io.Copy(partPkg, req.PKGFile) + if err != nil { + return errors.Wrap(err, "copying pkg file to multipart writer") + } + writer.WriteField("pkg_name", req.PKGFilename) + } + if err := writer.Close(); err != nil { + return errors.Wrap(err, "closing multipart writer") + } + + r.Header.Set("Content-Type", writer.FormDataContentType()) + r.Body = ioutil.NopCloser(body) + return nil +} + +func decodeUploadAppResponse(_ context.Context, r *http.Response) (interface{}, error) { + var resp appUploadResponse + err := httputil.DecodeJSONResponse(r, &resp) + return resp, err +} + +func MakeUploadAppEndpiont(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(appUploadRequest) + err = svc.UploadApp(ctx, req.ManifestName, req.ManifestFile, req.PKGFilename, req.PKGFile) + return &appUploadResponse{ + Err: err, + }, nil + } +} + +func (e Endpoints) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error { + request := appUploadRequest{ + ManifestName: manifestName, + ManifestFile: manifest, + PKGFilename: pkgName, + PKGFile: pkg, + } + resp, err := e.AppUploadEndpoint(ctx, request) + if err != nil { + return err + } + return resp.(appUploadResponse).Err +} diff --git a/platform/api/server/apply/transport_http_test.go b/platform/appstore/upload_app_test.go similarity index 98% rename from platform/api/server/apply/transport_http_test.go rename to platform/appstore/upload_app_test.go index fd71e3f8..e1124336 100644 --- a/platform/api/server/apply/transport_http_test.go +++ b/platform/appstore/upload_app_test.go @@ -1,4 +1,4 @@ -package apply +package appstore import ( "bytes"