From 670b4399db9ec2fefea0f0dc52d9357ee8aeec7f Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Wed, 17 May 2017 00:19:26 -0700 Subject: [PATCH] Profile service/store, API, and mdmctl management (#156) * Profile service/store, API, and mdmctl management * Group methods with their types * Centralize error checking and remove profile ID specification on upload (unnecessary) * Move more error checking into methods --- cmd/mdmctl/apply.go | 43 +++++++++ cmd/mdmctl/get.go | 53 +++++++++++ core/apply/client.go | 11 +++ core/apply/endpoint.go | 34 ++++++- core/apply/service.go | 7 ++ core/apply/transport_http.go | 24 +++++ core/list/client.go | 10 ++ core/list/endpoint.go | 30 ++++++ core/list/service.go | 19 ++++ core/list/transport_http.go | 26 +++++ profile/db.go | 100 ++++++++++++++++++++ profile/internal/profileproto/profile.go | 3 + profile/internal/profileproto/profile.pb.go | 70 ++++++++++++++ profile/internal/profileproto/profile.proto | 8 ++ profile/profile.go | 70 ++++++++++++++ serve.go | 19 +++- 16 files changed, 524 insertions(+), 3 deletions(-) create mode 100644 profile/db.go create mode 100644 profile/internal/profileproto/profile.go create mode 100644 profile/internal/profileproto/profile.pb.go create mode 100644 profile/internal/profileproto/profile.proto create mode 100644 profile/profile.go diff --git a/cmd/mdmctl/apply.go b/cmd/mdmctl/apply.go index f80769de..18add44d 100644 --- a/cmd/mdmctl/apply.go +++ b/cmd/mdmctl/apply.go @@ -14,6 +14,7 @@ import ( httptransport "github.com/go-kit/kit/transport/http" "github.com/micromdm/micromdm/blueprint" "github.com/micromdm/micromdm/core/apply" + "github.com/micromdm/micromdm/profile" uuid "github.com/satori/go.uuid" ) @@ -51,6 +52,8 @@ func (cmd *applyCommand) Run(args []string) error { run = cmd.applyBlueprint case "dep-tokens": run = cmd.applyDEPTokens + case "profiles": + run = cmd.applyProfile default: cmd.Usage() os.Exit(1) @@ -65,6 +68,7 @@ Apply a resource. Valid resource types: * blueprints + * profiles * dep-tokens Examples: @@ -162,3 +166,42 @@ func (cmd *applyCommand) applyDEPTokens(args []string) error { fmt.Println("imported DEP token") return nil } + +func (cmd *applyCommand) applyProfile(args []string) error { + flagset := flag.NewFlagSet("profiles", flag.ExitOnError) + var ( + flProfilePath = flagset.String("f", "", "filename of profile to apply") + ) + flagset.Usage = usageFor(flagset, "mdmctl apply profiles [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + if *flProfilePath == "" { + return errors.New("must provide -f parameter") + } + if _, err := os.Stat(*flProfilePath); os.IsNotExist(err) { + return err + } + profileBytes, err := ioutil.ReadFile(*flProfilePath) + if err != nil { + return err + } + + // TODO: to consider just uploading the Mobileconfig data (without a + // Profile struct and doing init server side) + var p profile.Profile + p.Mobileconfig = profileBytes + p.Identifier, err = p.Mobileconfig.GetPayloadIdentifier() + if err != nil { + return err + } + + ctx := context.Background() + err = cmd.applysvc.ApplyProfile(ctx, &p) + if err != nil { + return err + } + + fmt.Println(fmt.Sprintf("applied blueprint id %s from %s", p.Identifier, *flProfilePath)) + return nil +} diff --git a/cmd/mdmctl/get.go b/cmd/mdmctl/get.go index 1a600891..1554f66e 100644 --- a/cmd/mdmctl/get.go +++ b/cmd/mdmctl/get.go @@ -55,6 +55,8 @@ func (cmd *getCommand) Run(args []string) error { run = cmd.getDepTokens case "blueprints": run = cmd.getBlueprints + case "profiles": + run = cmd.getProfiles default: cmd.Usage() os.Exit(1) @@ -72,6 +74,7 @@ Valid resource types: * devices * blueprints * dep-tokens + * profiles Examples: # Get a list of devices @@ -235,3 +238,53 @@ func (cmd *getCommand) getBlueprints(args []string) error { return nil } + +func (cmd *getCommand) getProfiles(args []string) error { + flagset := flag.NewFlagSet("profiles", flag.ExitOnError) + var ( + flProfilePath = flagset.String("f", "", "filename of profile to write") + flIdentifier = flagset.String("id", "", "profile Identifier") + ) + flagset.Usage = usageFor(flagset, "mdmctl get blueprints [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + + ctx := context.Background() + profiles, err := cmd.list.GetProfiles(ctx, list.GetProfilesOption{Identifier: *flIdentifier}) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintf(w, "Identifier\tLength\n") + for _, p := range profiles { + fmt.Fprintf( + w, + "%s\t%d\n", + p.Identifier, + len(p.Mobileconfig), + ) + } + w.Flush() + + if *flIdentifier != "" && *flProfilePath != "" { + p := profiles[0] + + newProfileFile, err := os.Create(*flProfilePath) + if err != nil { + return err + } + defer newProfileFile.Close() + + _, err = newProfileFile.Write([]byte(p.Mobileconfig)) + if err != nil { + return err + } + fmt.Printf("\nwrote profile id %s to: %s\n", p.Identifier, *flProfilePath) + + if len(profiles) > 1 { + fmt.Println("WARNING: more than one Profile returned; only saved first") + } + } + return nil +} diff --git a/core/apply/client.go b/core/apply/client.go index b3f3783d..d8d0c270 100644 --- a/core/apply/client.go +++ b/core/apply/client.go @@ -36,10 +36,21 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra opts..., ).Endpoint() } + var applyProfileEndpoint endpoint.Endpoint + { + applyProfileEndpoint = httptransport.NewClient( + "PUT", + copyURL(u, "/v1/profiles"), + encodeRequestWithToken(token, EncodeHTTPGenericRequest), + DecodeProfileRequest, + opts..., + ).Endpoint() + } return Endpoints{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, ApplyDEPTokensEndpoint: applyDEPTokensEndpoint, + ApplyProfileEndpoint: applyProfileEndpoint, }, nil } diff --git a/core/apply/endpoint.go b/core/apply/endpoint.go index 437fac22..1e6be6e4 100644 --- a/core/apply/endpoint.go +++ b/core/apply/endpoint.go @@ -5,11 +5,13 @@ import ( "github.com/go-kit/kit/endpoint" "github.com/micromdm/micromdm/blueprint" + "github.com/micromdm/micromdm/profile" ) type Endpoints struct { ApplyBlueprintEndpoint endpoint.Endpoint ApplyDEPTokensEndpoint endpoint.Endpoint + ApplyProfileEndpoint endpoint.Endpoint } func (e Endpoints) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error { @@ -30,6 +32,15 @@ func (e Endpoints) ApplyDEPToken(ctx context.Context, P7MContent []byte) error { return resp.(depTokensResponse).Err } +func (e Endpoints) ApplyProfile(ctx context.Context, p *profile.Profile) error { + request := profileRequest{Profile: p} + resp, err := e.ApplyProfileEndpoint(ctx, request) + if err != nil { + return err + } + return resp.(profileResponse).Err +} + func MakeApplyBlueprintEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(blueprintRequest) @@ -50,6 +61,16 @@ func MakeApplyDEPTokensEndpoint(svc Service) endpoint.Endpoint { } } +func MakeApplyProfileEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(profileRequest) + err = svc.ApplyProfile(ctx, req.Profile) + return profileResponse{ + Err: err, + }, nil + } +} + type blueprintRequest struct { Blueprint *blueprint.Blueprint `json:"blueprint"` } @@ -58,6 +79,18 @@ type blueprintResponse struct { Err error `json:"err,omitempty"` } +func (r blueprintResponse) error() error { return r.Err } + +type profileRequest struct { + Profile *profile.Profile `json:"profile"` +} + +type profileResponse struct { + Err error `json:"err,omitempty"` +} + +func (r profileResponse) error() error { return r.Err } + type depTokensRequest struct { P7MContent []byte `json:"p7m_content"` } @@ -67,4 +100,3 @@ type depTokensResponse struct { } func (r depTokensResponse) error() error { return r.Err } -func (r blueprintResponse) error() error { return r.Err } diff --git a/core/apply/service.go b/core/apply/service.go index ecd60084..cf3cf172 100644 --- a/core/apply/service.go +++ b/core/apply/service.go @@ -15,15 +15,18 @@ import ( "github.com/boltdb/bolt" "github.com/micromdm/micromdm/blueprint" "github.com/micromdm/micromdm/core/list" + "github.com/micromdm/micromdm/profile" ) 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 } type ApplyService struct { Blueprints *blueprint.DB + Profiles *profile.DB DB *bolt.DB // TODO: replace with reference to DEP token svc/pkg } @@ -115,3 +118,7 @@ func (svc *ApplyService) ApplyDEPToken(ctx context.Context, P7MContent []byte) e fmt.Println("stored DEP token with ck", depToken.ConsumerKey) return nil } + +func (svc *ApplyService) ApplyProfile(ctx context.Context, p *profile.Profile) error { + return svc.Profiles.Save(p) +} diff --git a/core/apply/transport_http.go b/core/apply/transport_http.go index 7fed715e..2092578b 100644 --- a/core/apply/transport_http.go +++ b/core/apply/transport_http.go @@ -14,6 +14,7 @@ import ( type HTTPHandlers struct { BlueprintHandler http.Handler DEPTokensHandler http.Handler + ProfileHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -30,6 +31,12 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran encodeResponse, opts..., ), + ProfileHandler: httptransport.NewServer( + endpoints.ApplyProfileEndpoint, + decodeProfileRequest, + encodeResponse, + opts..., + ), } return h } @@ -50,6 +57,14 @@ func decodeBlueprintRequest(ctx context.Context, r *http.Request) (interface{}, return bpReq, nil } +func decodeProfileRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req profileRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil +} + type errorWrapper struct { Error string `json:"error"` } @@ -112,3 +127,12 @@ func DecodeDEPTokensRequest(_ context.Context, r *http.Response) (interface{}, e err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } + +func DecodeProfileRequest(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp profileResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/core/list/client.go b/core/list/client.go index 360a7ffa..30f98c16 100644 --- a/core/list/client.go +++ b/core/list/client.go @@ -45,11 +45,21 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra DecodeGetBlueprintsResponse, ).Endpoint() } + var getProfilesEndpoint endpoint.Endpoint + { + getProfilesEndpoint = httptransport.NewClient( + "GET", + copyURL(u, "/v1/profiles"), + encodeRequestWithToken(token, EncodeHTTPGenericRequest), + DecodeGetProfilesResponse, + ).Endpoint() + } return Endpoints{ ListDevicesEndpoint: listDevicesEndpoint, GetDEPTokensEndpoint: getDEPTokensEndpoint, GetBlueprintsEndpoint: getBlueprintsEndpoint, + GetProfilesEndpoint: getProfilesEndpoint, }, nil } diff --git a/core/list/endpoint.go b/core/list/endpoint.go index 77a03a02..3c5abf3e 100644 --- a/core/list/endpoint.go +++ b/core/list/endpoint.go @@ -6,12 +6,14 @@ import ( "github.com/go-kit/kit/endpoint" "github.com/micromdm/micromdm/blueprint" + "github.com/micromdm/micromdm/profile" ) type Endpoints struct { ListDevicesEndpoint endpoint.Endpoint GetDEPTokensEndpoint endpoint.Endpoint GetBlueprintsEndpoint endpoint.Endpoint + GetProfilesEndpoint endpoint.Endpoint } func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) { @@ -40,6 +42,15 @@ func (e Endpoints) GetBlueprints(ctx context.Context, opt GetBlueprintsOption) ( return response.(blueprintsResponse).Blueprints, response.(blueprintsResponse).Err } +func (e Endpoints) GetProfiles(ctx context.Context, opt GetProfilesOption) ([]profile.Profile, error) { + request := profilesRequest{opt} + response, err := e.GetProfilesEndpoint(ctx, request.Opts) + if err != nil { + return nil, err + } + return response.(profilesResponse).Profiles, response.(profilesResponse).Err +} + func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(devicesRequest) @@ -73,6 +84,17 @@ func MakeGetBlueprintsEndpoint(svc Service) endpoint.Endpoint { } } +func MakeGetProfilesEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(profilesRequest) + profiles, err := svc.GetProfiles(ctx, req.Opts) + return profilesResponse{ + Profiles: profiles, + Err: err, + }, nil + } +} + type DeviceDTO struct { SerialNumber string `json:"serial_number"` UDID string `json:"udid"` @@ -107,3 +129,11 @@ type blueprintsResponse struct { } func (r blueprintsResponse) error() error { return r.Err } + +type profilesRequest struct{ Opts GetProfilesOption } +type profilesResponse struct { + Profiles []profile.Profile `json:"profiles"` + Err error `json:"err,omitempty"` +} + +func (r profilesResponse) error() error { return r.Err } diff --git a/core/list/service.go b/core/list/service.go index d3132b63..8f178b90 100644 --- a/core/list/service.go +++ b/core/list/service.go @@ -13,6 +13,7 @@ import ( "github.com/micromdm/micromdm/blueprint" "github.com/micromdm/micromdm/crypto" "github.com/micromdm/micromdm/device" + "github.com/micromdm/micromdm/profile" ) type ListDevicesOption struct { @@ -27,15 +28,21 @@ type GetBlueprintsOption struct { FilterName string } +type GetProfilesOption struct { + Identifier string `json:"id"` +} + type Service interface { ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error) GetBlueprints(ctx context.Context, opt GetBlueprintsOption) ([]blueprint.Blueprint, error) + GetProfiles(ctx context.Context, opt GetProfilesOption) ([]profile.Profile, error) } type ListService struct { Devices *device.DB Blueprints *blueprint.DB + Profiles *profile.DB DB *bolt.DB // TODO: replace with reference to DEP token svc/pkg } @@ -176,3 +183,15 @@ func (svc *ListService) GetBlueprints(ctx context.Context, opt GetBlueprintsOpti return bps, nil } } + +func (svc *ListService) GetProfiles(ctx context.Context, opt GetProfilesOption) ([]profile.Profile, error) { + if opt.Identifier != "" { + foundProf, err := svc.Profiles.ProfileById(opt.Identifier) + if err != nil { + return nil, err + } + return []profile.Profile{*foundProf}, nil + } else { + return svc.Profiles.List() + } +} diff --git a/core/list/transport_http.go b/core/list/transport_http.go index 430412ad..0149412d 100644 --- a/core/list/transport_http.go +++ b/core/list/transport_http.go @@ -15,6 +15,7 @@ type HTTPHandlers struct { ListDevicesHandler http.Handler GetDEPTokensHandler http.Handler GetBlueprintsHandler http.Handler + GetProfilesHandler http.Handler } func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { @@ -35,6 +36,11 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran decodeGetBlueprintsRequest, encodeResponse, opts...), + GetProfilesHandler: httptransport.NewServer( + endpoints.GetProfilesEndpoint, + decodeGetProfilesRequest, + encodeResponse, + opts...), } return h } @@ -61,6 +67,17 @@ func decodeGetBlueprintsRequest(ctx context.Context, r *http.Request) (interface return req, nil } +func decodeGetProfilesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var opts GetProfilesOption + if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { + return nil, err + } + req := profilesRequest{ + Opts: opts, + } + return req, nil +} + func errorDecoder(r *http.Response) error { var w errorWrapper if err := json.NewDecoder(r.Body).Decode(&w); err != nil { @@ -132,3 +149,12 @@ func DecodeGetBlueprintsResponse(_ context.Context, r *http.Response) (interface err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } + +func DecodeGetProfilesResponse(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp profilesResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/profile/db.go b/profile/db.go new file mode 100644 index 00000000..92c1afd4 --- /dev/null +++ b/profile/db.go @@ -0,0 +1,100 @@ +package profile + +import ( + "fmt" + + "github.com/boltdb/bolt" + "github.com/pkg/errors" +) + +const ( + ProfileBucket = "mdm.Profile" +) + +type DB struct { + *bolt.DB +} + +func NewDB(db *bolt.DB) (*DB, error) { + err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(ProfileBucket)) + return err + }) + if err != nil { + return nil, errors.Wrapf(err, "creating %s bucket", ProfileBucket) + } + datastore := &DB{ + DB: db, + } + return datastore, nil +} + +func (db *DB) List() ([]Profile, error) { + // TODO add filter/limit with ForEach + var list []Profile + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(ProfileBucket)) + c := b.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + var p Profile + if err := UnmarshalProfile(v, &p); err != nil { + return err + } + list = append(list, p) + } + return nil + }) + return list, err +} + +func (db *DB) Save(p *Profile) error { + err := p.Validate() + if err != nil { + return err + } + tx, err := db.DB.Begin(true) + if err != nil { + return errors.Wrap(err, "begin transaction") + } + bkt := tx.Bucket([]byte(ProfileBucket)) + if bkt == nil { + return fmt.Errorf("bucket %q not found!", ProfileBucket) + } + pproto, err := MarshalProfile(p) + if err != nil { + return errors.Wrap(err, "marshalling profile") + } + if err := bkt.Put([]byte(p.Identifier), pproto); err != nil { + return errors.Wrap(err, "put profile to boltdb") + } + return tx.Commit() +} + +func (db *DB) ProfileById(id string) (*Profile, error) { + var p Profile + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(ProfileBucket)) + v := b.Get([]byte(id)) + if v == nil { + return ¬Found{"Profile", fmt.Sprintf("id %s", id)} + } + return UnmarshalProfile(v, &p) + }) + return &p, err +} + +type notFound struct { + ResourceType string + Message string +} + +func (e *notFound) Error() string { + return fmt.Sprintf("not found: %s %s", e.ResourceType, e.Message) +} + +func isNotFound(err error) bool { + if _, ok := err.(*notFound); ok { + return true + } + return false +} diff --git a/profile/internal/profileproto/profile.go b/profile/internal/profileproto/profile.go new file mode 100644 index 00000000..73977478 --- /dev/null +++ b/profile/internal/profileproto/profile.go @@ -0,0 +1,3 @@ +package profileproto + +//go:generate protoc --go_out=. profile.proto diff --git a/profile/internal/profileproto/profile.pb.go b/profile/internal/profileproto/profile.pb.go new file mode 100644 index 00000000..e212ff42 --- /dev/null +++ b/profile/internal/profileproto/profile.pb.go @@ -0,0 +1,70 @@ +// Code generated by protoc-gen-go. +// source: profile.proto +// DO NOT EDIT! + +/* +Package profileproto is a generated protocol buffer package. + +It is generated from these files: + profile.proto + +It has these top-level messages: + Profile +*/ +package profileproto + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Profile struct { + Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` + Mobileconfig []byte `protobuf:"bytes,2,opt,name=mobileconfig,proto3" json:"mobileconfig,omitempty"` +} + +func (m *Profile) Reset() { *m = Profile{} } +func (m *Profile) String() string { return proto.CompactTextString(m) } +func (*Profile) ProtoMessage() {} +func (*Profile) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Profile) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *Profile) GetMobileconfig() []byte { + if m != nil { + return m.Mobileconfig + } + return nil +} + +func init() { + proto.RegisterType((*Profile)(nil), "profileproto.Profile") +} + +func init() { proto.RegisterFile("profile.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 97 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x28, 0xca, 0x4f, + 0xcb, 0xcc, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x81, 0x72, 0xc1, 0x3c, 0x25, + 0x5b, 0x2e, 0xf6, 0x00, 0x08, 0x5f, 0x88, 0x8f, 0x8b, 0x29, 0x33, 0x45, 0x82, 0x51, 0x81, 0x51, + 0x83, 0x33, 0x88, 0x29, 0x33, 0x45, 0x48, 0x89, 0x8b, 0x27, 0x37, 0x3f, 0x29, 0x33, 0x27, 0x35, + 0x39, 0x3f, 0x2f, 0x2d, 0x33, 0x5d, 0x82, 0x49, 0x81, 0x51, 0x83, 0x27, 0x08, 0x45, 0x2c, 0x89, + 0x0d, 0x6c, 0x8a, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xea, 0x33, 0x2f, 0xfa, 0x64, 0x00, 0x00, + 0x00, +} diff --git a/profile/internal/profileproto/profile.proto b/profile/internal/profileproto/profile.proto new file mode 100644 index 00000000..f34c04df --- /dev/null +++ b/profile/internal/profileproto/profile.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package profileproto; + +message Profile { + string id = 1; + bytes mobileconfig = 2; +} diff --git a/profile/profile.go b/profile/profile.go new file mode 100644 index 00000000..9546867f --- /dev/null +++ b/profile/profile.go @@ -0,0 +1,70 @@ +package profile + +import ( + "errors" + + "github.com/gogo/protobuf/proto" + "github.com/groob/plist" + "github.com/micromdm/micromdm/profile/internal/profileproto" +) + +type Mobileconfig []byte + +// only used to parse plists to get the PayloadIdentifier +type payloadIdentifier struct { + PayloadIdentifier string +} + +func (mc *Mobileconfig) GetPayloadIdentifier() (string, error) { + // TODO: support CMS signed profiles + var pId payloadIdentifier + err := plist.Unmarshal(*mc, &pId) + if err != nil { + return "", err + } + if pId.PayloadIdentifier == "" { + return "", errors.New("empty PayloadIdentifier in profile") + } + return pId.PayloadIdentifier, err +} + +type Profile struct { + Identifier string + Mobileconfig Mobileconfig +} + +// Validate checks the internal consistency and validity of a Profile structure +func (p *Profile) Validate() error { + if p.Identifier == "" { + return errors.New("Profile struct must have Identifier") + } + if len(p.Mobileconfig) < 1 { + return errors.New("no Mobileconfig data") + } + payloadId, err := p.Mobileconfig.GetPayloadIdentifier() + if err != nil { + return err + } + if payloadId != p.Identifier { + return errors.New("payload Identifier does not match Profile") + } + return nil +} + +func MarshalProfile(p *Profile) ([]byte, error) { + protobp := profileproto.Profile{ + Id: p.Identifier, + Mobileconfig: p.Mobileconfig, + } + return proto.Marshal(&protobp) +} + +func UnmarshalProfile(data []byte, p *Profile) error { + var pb profileproto.Profile + if err := proto.Unmarshal(data, &pb); err != nil { + return err + } + p.Identifier = pb.GetId() + p.Mobileconfig = pb.GetMobileconfig() + return nil +} diff --git a/serve.go b/serve.go index 00d00f1f..38e3a8b1 100644 --- a/serve.go +++ b/serve.go @@ -50,6 +50,7 @@ import ( "github.com/micromdm/micromdm/depsync" "github.com/micromdm/micromdm/device" "github.com/micromdm/micromdm/enroll" + "github.com/micromdm/micromdm/profile" "github.com/micromdm/micromdm/pubsub" nanopush "github.com/micromdm/micromdm/push" "github.com/micromdm/micromdm/queue" @@ -151,6 +152,11 @@ func serve(args []string) error { stdlog.Fatal(err) } + profDB, err := profile.NewDB(sm.db) + if err != nil { + stdlog.Fatal(err) + } + ctx := context.Background() httpLogger := log.With(logger, "transport", "http") var checkinEndpoint endpoint.Endpoint @@ -192,7 +198,7 @@ func serve(args []string) error { var listsvc list.Service { - listsvc = &list.ListService{Devices: devDB, DB: sm.db, Blueprints: bpDB} + listsvc = &list.ListService{Devices: devDB, DB: sm.db, Blueprints: bpDB, Profiles: profDB} } var listDevicesEndpoint endpoint.Endpoint { @@ -203,11 +209,12 @@ func serve(args []string) error { ListDevicesEndpoint: listDevicesEndpoint, GetDEPTokensEndpoint: list.MakeGetDEPTokensEndpoint(listsvc), GetBlueprintsEndpoint: list.MakeGetBlueprintsEndpoint(listsvc), + GetProfilesEndpoint: list.MakeGetProfilesEndpoint(listsvc), } var applysvc apply.Service { - applysvc = &apply.ApplyService{Blueprints: bpDB, DB: sm.db} + applysvc = &apply.ApplyService{Blueprints: bpDB, DB: sm.db, Profiles: profDB} } var applyBlueprintEndpoint endpoint.Endpoint @@ -215,9 +222,15 @@ func serve(args []string) error { applyBlueprintEndpoint = apply.MakeApplyBlueprintEndpoint(applysvc) } + var applyProfileEndpoint endpoint.Endpoint + { + applyProfileEndpoint = apply.MakeApplyProfileEndpoint(applysvc) + } + applyEndpoints := apply.Endpoints{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, ApplyDEPTokensEndpoint: apply.MakeApplyDEPTokensEndpoint(applysvc), + ApplyProfileEndpoint: applyProfileEndpoint, } applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...) @@ -249,6 +262,8 @@ func serve(args []string) error { r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DEPTokensHandler)).Methods("PUT") r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetBlueprintsHandler)).Methods("GET") r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.BlueprintHandler)).Methods("PUT") + r.Handle("/v1/profiles", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetProfilesHandler)).Methods("GET") + r.Handle("/v1/profiles", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.ProfileHandler)).Methods("PUT") } if *flRepoPath != "" {