mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-07 01:55:48 +08:00
add package and manifest file uploads (#204)
Add support to manage package imports and generating/editing appmanifest files. Closes #93
This commit is contained in:
97
appstore/appstore.go
Normal file
97
appstore/appstore.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
62
cmd/mdmctl/get_app.go
Normal file
62
cmd/mdmctl/get_app.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
65
core/apply/transport_http_test.go
Normal file
65
core/apply/transport_http_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
30
serve.go
30
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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user