From d352d6cc95415b05ebc94a70fe9397a45a69531b Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Mon, 5 Jun 2017 00:27:14 -0700 Subject: [PATCH] add package and manifest file uploads (#204) Add support to manage package imports and generating/editing appmanifest files. Closes #93 --- appstore/appstore.go | 97 +++++++++++++++++++++++++++++++ cmd/mdmctl/apply_app.go | 28 +++++++-- cmd/mdmctl/get.go | 3 + cmd/mdmctl/get_app.go | 62 ++++++++++++++++++++ core/apply/client.go | 12 ++++ core/apply/endpoint.go | 40 +++++++++++++ core/apply/service.go | 19 ++++++ core/apply/transport_http.go | 78 ++++++++++++++++++++++++- core/apply/transport_http_test.go | 65 +++++++++++++++++++++ core/list/client.go | 12 ++++ core/list/endpoint.go | 37 ++++++++++++ core/list/service.go | 32 ++++++++++ core/list/transport_http.go | 28 ++++++++- serve.go | 30 +++++++++- 14 files changed, 532 insertions(+), 11 deletions(-) create mode 100644 appstore/appstore.go create mode 100644 cmd/mdmctl/get_app.go create mode 100644 core/apply/transport_http_test.go diff --git a/appstore/appstore.go b/appstore/appstore.go new file mode 100644 index 00000000..b731b597 --- /dev/null +++ b/appstore/appstore.go @@ -0,0 +1,97 @@ +// package appstore provides an abstraction for uploading files and manifests +// to a repository. +package appstore + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/groob/plist" + "github.com/micromdm/micromdm/appmanifest" + "github.com/pkg/errors" +) + +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 +} + +func (r *Repo) SaveFile(name string, f io.Reader) error { + fname := filepath.Join(r.Path, name) + file, err := os.Create(fname) + if err != nil { + return errors.Wrapf(err, "saving file %s", name) + } + defer file.Close() + + _, err = io.Copy(file, f) + return err +} + +func (r *Repo) Manifest(name string) (*appmanifest.Manifest, error) { + manifestName := name + if !strings.HasSuffix(name, ".plist") { + manifestName = name + ".plist" + + } + fname := filepath.Join(r.Path, manifestName) + file, err := os.Open(fname) + if err != nil { + return nil, errors.Wrapf(err, "reading manifest %s", name) + } + defer file.Close() + + var m appmanifest.Manifest + if err := plist.NewDecoder(file).Decode(&m); err != nil { + return nil, errors.Wrap(err, "decoding manifest file") + } + + return &m, nil +} + +func (r *Repo) Apps(name string) (map[string]appmanifest.Manifest, error) { + manifests := make(map[string]appmanifest.Manifest) + if name != "" { + mf, err := os.Open(filepath.Join(r.Path, name)) + if err != nil { + return nil, err + } + var m appmanifest.Manifest + if err := plist.NewDecoder(mf).Decode(&m); err != nil { + return nil, err + } + manifests[name] = m + return manifests, nil + } + + files, err := ioutil.ReadDir(r.Path) + if err != nil { + return nil, err + } + + for _, file := range files { + manifestName := file.Name() + if file.IsDir() || filepath.Ext(manifestName) != ".plist" { + continue + } + mf, err := os.Open(filepath.Join(r.Path, manifestName)) + if err != nil { + return nil, err + } + var m appmanifest.Manifest + if err := plist.NewDecoder(mf).Decode(&m); err != nil { + return nil, err + } + manifests[manifestName] = m + } + + return manifests, nil +} diff --git a/cmd/mdmctl/apply_app.go b/cmd/mdmctl/apply_app.go index b4aec7e8..8796c4ae 100644 --- a/cmd/mdmctl/apply_app.go +++ b/cmd/mdmctl/apply_app.go @@ -3,6 +3,7 @@ package main import ( "bytes" "compress/zlib" + "context" "encoding/binary" "flag" "fmt" @@ -29,6 +30,7 @@ func (cmd *applyCommand) applyApp(args []string) error { flHashSize = flagset.Int64("md5size", appmanifest.DefaultMD5Size, "md5 hash size in bytes (optional)") flSign = flagset.String("sign", "", "sign package before importing, requires specifying a product ID (optional)") + flUpload = flagset.Bool("upload", false, "upload package and/or manifest to micromdm repository.") ) flagset.Usage = usageFor(flagset, "mdmctl apply app [flags]") if err := flagset.Parse(args); err != nil { @@ -98,6 +100,13 @@ Please rebuild the package and re-run the command. return err } + if *flUpload { + err := cmd.applysvc.UploadApp(context.TODO(), nameMannifest(f.Name()), &buf, filepath.Base(f.Name()), f) + if err != nil { + return err + } + } + switch *flAppManifest { case "": case "-": @@ -110,6 +119,11 @@ Please rebuild the package and re-run the command. return nil } +func nameMannifest(pkgName string) string { + trimmed := strings.TrimSuffix(filepath.Base(pkgName), filepath.Ext(pkgName)) + return trimmed + ".plist" +} + func checkDistribution(pkgPath string) (bool, error) { const ( xarHeaderMagic = 0x78617221 @@ -147,7 +161,15 @@ func checkDistribution(pkgPath string) (bool, error) { } func (cmd *applyCommand) serverRepoURL() (string, error) { - serverURL, err := url.Parse(cmd.config.ServerURL) + return repoURL(cmd.config.ServerURL) +} + +func pkgURL(repoURL, pkgPath string) string { + return path.Join(repoURL, filepath.Base(pkgPath)) +} + +func repoURL(server string) (string, error) { + serverURL, err := url.Parse(server) if err != nil { return "", err } @@ -155,10 +177,6 @@ func (cmd *applyCommand) serverRepoURL() (string, error) { return serverURL.String(), nil } -func pkgURL(repoURL, pkgPath string) string { - return path.Join(repoURL, filepath.Base(pkgPath)) -} - // replaces .pkg with .plist func manifestURL(repoURL, pkgPath string) string { pu := pkgURL(repoURL, pkgPath) diff --git a/cmd/mdmctl/get.go b/cmd/mdmctl/get.go index 16499d5d..3e0f034c 100644 --- a/cmd/mdmctl/get.go +++ b/cmd/mdmctl/get.go @@ -63,6 +63,8 @@ func (cmd *getCommand) Run(args []string) error { run = cmd.getBlueprints case "profiles": run = cmd.getProfiles + case "apps": + run = cmd.getApps default: cmd.Usage() os.Exit(1) @@ -84,6 +86,7 @@ Valid resource types: * dep-account * dep-profiles * profiles + * apps Examples: # Get a list of devices diff --git a/cmd/mdmctl/get_app.go b/cmd/mdmctl/get_app.go new file mode 100644 index 00000000..c1450835 --- /dev/null +++ b/cmd/mdmctl/get_app.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "os" + "text/tabwriter" + + "github.com/micromdm/micromdm/core/list" +) + +type appsTableOutput struct{ w *tabwriter.Writer } + +func (out *appsTableOutput) BasicHeader() { + fmt.Fprintf(out.w, "Name\t,ManifestURL\n") +} + +func (out *appsTableOutput) BasicFooter() { + out.w.Flush() +} + +func (cmd *getCommand) getApps(args []string) error { + flagset := flag.NewFlagSet("apps", flag.ExitOnError) + var ( + flNameFilter = flagset.String("name", "", "specify the name of the app to get full details") + flOutputPath = flagset.String("f", "-", "path to save file to. defaults to stdout.") + ) + flagset.Usage = usageFor(flagset, "mdmctl get apps [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + ctx := context.Background() + apps, err := cmd.list.ListApplications(ctx, list.ListAppsOption{ + FilterName: []string{*flNameFilter}, + }) + if err != nil { + return err + } + if *flNameFilter != "" && (len(apps) != 0) { + payload := apps[0].Payload + if *flOutputPath == "-" { + fmt.Println(string(payload)) + return nil + } + return ioutil.WriteFile(*flOutputPath, payload, 0644) + } + rURL, err := repoURL(cmd.config.ServerURL) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0) + out := appsTableOutput{w} + out.BasicHeader() + defer out.BasicFooter() + for _, a := range apps { + manifestURL := rURL + "/" + a.Name + fmt.Fprintf(out.w, "%s\t%s\n", a.Name, manifestURL) + } + return nil +} diff --git a/core/apply/client.go b/core/apply/client.go index 1011193a..c8b445bf 100644 --- a/core/apply/client.go +++ b/core/apply/client.go @@ -58,11 +58,23 @@ 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{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, ApplyDEPTokensEndpoint: applyDEPTokensEndpoint, ApplyProfileEndpoint: applyProfileEndpoint, DefineDEPProfileEndpoint: defineDEPProfileEndpoint, + AppUploadEndpoint: uploadAppEndpoint, }, nil } diff --git a/core/apply/endpoint.go b/core/apply/endpoint.go index f8a059f0..e790b668 100644 --- a/core/apply/endpoint.go +++ b/core/apply/endpoint.go @@ -2,6 +2,7 @@ package apply import ( "context" + "io" "github.com/go-kit/kit/endpoint" "github.com/micromdm/dep" @@ -14,6 +15,21 @@ type Endpoints struct { ApplyDEPTokensEndpoint endpoint.Endpoint ApplyProfileEndpoint endpoint.Endpoint 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) { @@ -94,6 +110,30 @@ 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 blueprintRequest struct { Blueprint *blueprint.Blueprint `json:"blueprint"` } diff --git a/core/apply/service.go b/core/apply/service.go index e6d0bf17..90e86078 100644 --- a/core/apply/service.go +++ b/core/apply/service.go @@ -15,6 +15,7 @@ import ( "github.com/fullsailor/pkcs7" "github.com/micromdm/dep" + "github.com/micromdm/micromdm/appstore" "github.com/micromdm/micromdm/blueprint" "github.com/micromdm/micromdm/deptoken" "github.com/micromdm/micromdm/profile" @@ -25,6 +26,7 @@ type Service interface { ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error ApplyDEPToken(ctx context.Context, P7MContent []byte) error ApplyProfile(ctx context.Context, p *profile.Profile) error + UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error DEPService } @@ -35,6 +37,23 @@ type ApplyService struct { Blueprints *blueprint.DB Profiles *profile.DB Tokens *deptoken.DB + 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/core/apply/transport_http.go b/core/apply/transport_http.go index ce9a2b8d..1087021e 100644 --- a/core/apply/transport_http.go +++ b/core/apply/transport_http.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "encoding/json" - "errors" + "io" "io/ioutil" + "mime/multipart" "net/http" httptransport "github.com/go-kit/kit/transport/http" + "github.com/pkg/errors" ) type HTTPHandlers struct { @@ -16,6 +18,7 @@ type HTTPHandlers struct { DEPTokensHandler http.Handler ProfileHandler http.Handler DefineDEPProfileHandler http.Handler + AppUploadHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -44,6 +47,12 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran encodeResponse, opts..., ), + AppUploadHandler: httptransport.NewServer( + endpoints.AppUploadEndpoint, + decodeAppUploadRequest, + encodeResponse, + opts..., + ), } return h } @@ -80,6 +89,64 @@ 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"` } @@ -160,3 +227,12 @@ 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/core/apply/transport_http_test.go b/core/apply/transport_http_test.go new file mode 100644 index 00000000..fd71e3f8 --- /dev/null +++ b/core/apply/transport_http_test.go @@ -0,0 +1,65 @@ +package apply + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http/httptest" + "testing" +) + +func TestDecodeUploadRequest(t *testing.T) { + var aFile, bFile bytes.Buffer + aFile.Write([]byte("hello")) + bFile.Write([]byte("world")) + + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("app_manifest_filedata", "manifest.plist") + if err != nil { + t.Fatal(err) + } + _, err = io.Copy(part, &aFile) + if err != nil { + t.Fatal(err) + } + writer.WriteField("app_manifest_filename", "manifest.plist") + + partPkg, err := writer.CreateFormFile("pkg_filedata", "foo.pkg") + if err != nil { + t.Fatal(err) + } + _, err = io.Copy(partPkg, &bFile) + if err != nil { + t.Fatal(err) + } + writer.WriteField("pkg_name", "hello.pkg") + writer.Close() + + req := httptest.NewRequest("POST", "https://mdm.acme.co/", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + request, err := decodeAppUploadRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + decoded := request.(appUploadRequest) + + if have, want := decoded.ManifestName, "manifest.plist"; have != want { + t.Errorf("have %s, want %s", have, want) + } + if have, want := decoded.PKGFilename, "hello.pkg"; have != want { + t.Errorf("have %s, want %s", have, want) + } + + var a, b bytes.Buffer + io.Copy(&a, decoded.ManifestFile) + io.Copy(&b, decoded.PKGFile) + if have, want := a.String(), "hello"; have != want { + t.Errorf("have %s, want %s", have, want) + } + if have, want := b.String(), "world"; have != want { + t.Errorf("have %s, want %s", have, want) + } +} diff --git a/core/list/client.go b/core/list/client.go index 1b984b88..01cb9046 100644 --- a/core/list/client.go +++ b/core/list/client.go @@ -90,6 +90,17 @@ 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, GetDEPTokensEndpoint: getDEPTokensEndpoint, @@ -98,6 +109,7 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra GetDEPAccountInfoEndpoint: getDEPAccountInfoEndpoint, GetDEPDeviceEndpoint: getDEPDeviceDetailsEndpoint, GetDEPProfileEndpoint: getDEPProfilesEndpoint, + ListAppsEndpont: listAppsEndpoint, }, nil } diff --git a/core/list/endpoint.go b/core/list/endpoint.go index c954f699..d8960280 100644 --- a/core/list/endpoint.go +++ b/core/list/endpoint.go @@ -19,6 +19,7 @@ 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) { @@ -30,6 +31,15 @@ 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) GetDEPTokens(ctx context.Context) ([]deptoken.DEPToken, []byte, error) { resp, err := e.GetDEPTokensEndpoint(ctx, nil) if err != nil { @@ -85,6 +95,17 @@ 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) @@ -213,3 +234,19 @@ 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/core/list/service.go b/core/list/service.go index 790a2674..4a28060d 100644 --- a/core/list/service.go +++ b/core/list/service.go @@ -6,12 +6,15 @@ import ( "log" "sync" + "github.com/groob/plist" "github.com/micromdm/dep" + "github.com/micromdm/micromdm/appstore" "github.com/micromdm/micromdm/blueprint" "github.com/micromdm/micromdm/deptoken" "github.com/micromdm/micromdm/device" "github.com/micromdm/micromdm/profile" "github.com/micromdm/micromdm/pubsub" + "github.com/pkg/errors" ) type ListDevicesOption struct { @@ -30,11 +33,16 @@ type GetProfilesOption struct { Identifier string `json:"id"` } +type ListAppsOption struct { + FilterName []string `json:"filter_name"` +} + type Service interface { ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) GetDEPTokens(ctx context.Context) ([]deptoken.DEPToken, []byte, error) GetBlueprints(ctx context.Context, opt GetBlueprintsOption) ([]blueprint.Blueprint, error) GetProfiles(ctx context.Context, opt GetProfilesOption) ([]profile.Profile, error) + ListApplications(ctx context.Context, opt ListAppsOption) ([]AppDTO, error) DEPService } @@ -46,6 +54,30 @@ type ListService struct { Blueprints *blueprint.DB Profiles *profile.DB Tokens *deptoken.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/core/list/transport_http.go b/core/list/transport_http.go index 0fcde59e..fbcd0e0a 100644 --- a/core/list/transport_http.go +++ b/core/list/transport_http.go @@ -17,8 +17,9 @@ type HTTPHandlers struct { GetBlueprintsHandler http.Handler GetProfilesHandler http.Handler GetDEPAccountInfoHandler http.Handler - GetDEPProfileHander http.Handler + GetDEPProfileHandler http.Handler GetDEPDeviceDetailsHandler http.Handler + ListAppsHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -56,12 +57,18 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran encodeResponse, opts..., ), - GetDEPProfileHander: httptransport.NewServer( + GetDEPProfileHandler: httptransport.NewServer( endpoints.GetDEPProfileEndpoint, decodeDEPProfileRequest, encodeResponse, opts..., ), + ListAppsHandler: httptransport.NewServer( + endpoints.ListAppsEndpont, + decodeListAppsRequest, + encodeResponse, + opts..., + ), } return h } @@ -119,6 +126,14 @@ 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 { @@ -226,3 +241,12 @@ 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/serve.go b/serve.go index 10f3e488..a5bbd3cf 100644 --- a/serve.go +++ b/serve.go @@ -40,6 +40,7 @@ import ( boltdepot "github.com/micromdm/scep/depot/bolt" scep "github.com/micromdm/scep/server" + "github.com/micromdm/micromdm/appstore" "github.com/micromdm/micromdm/blueprint" "github.com/micromdm/micromdm/checkin" "github.com/micromdm/micromdm/command" @@ -213,9 +214,17 @@ func serve(args []string) error { stdlog.Fatalf("creating DEP client %s\n", err) } tokenDB := &deptoken.DB{DB: sm.db, Publisher: sm.pubclient} + appDB := &appstore.Repo{Path: *flRepoPath} var listsvc list.Service { - l := &list.ListService{DEPClient: dc, Devices: devDB, Tokens: tokenDB, Blueprints: bpDB, Profiles: profDB} + l := &list.ListService{ + DEPClient: dc, + Devices: devDB, + Tokens: tokenDB, + Blueprints: bpDB, + Profiles: profDB, + Apps: appDB, + } listsvc = l if err := l.WatchTokenUpdates(sm.pubclient); err != nil { @@ -235,11 +244,18 @@ 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, Blueprints: bpDB, Tokens: tokenDB, Profiles: profDB} + l := &apply.ApplyService{ + DEPClient: dc, + Blueprints: bpDB, + Tokens: tokenDB, + Profiles: profDB, + Apps: appDB, + } applysvc = l if err := l.WatchTokenUpdates(sm.pubclient); err != nil { stdlog.Fatal(err) @@ -261,11 +277,17 @@ func serve(args []string) error { defineDEPProfileEndpoint = apply.MakeDefineDEPProfile(applysvc) } + var appUploadEndpoint endpoint.Endpoint + { + appUploadEndpoint = apply.MakeUploadAppEndpiont(applysvc) + } + applyEndpoints := apply.Endpoints{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, ApplyDEPTokensEndpoint: apply.MakeApplyDEPTokensEndpoint(applysvc), ApplyProfileEndpoint: applyProfileEndpoint, DefineDEPProfileEndpoint: defineDEPProfileEndpoint, + AppUploadEndpoint: appUploadEndpoint, } applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...) @@ -306,8 +328,10 @@ func serve(args []string) error { 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") + 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 != "" {