diff --git a/checkin/encode_decode.go b/checkin/encode_decode.go index 6b82397a..81e8a748 100644 --- a/checkin/encode_decode.go +++ b/checkin/encode_decode.go @@ -1,9 +1,11 @@ package checkin import ( + "io/ioutil" "log" "net/http" + "github.com/fullsailor/pkcs7" "github.com/groob/plist" ) @@ -15,6 +17,26 @@ func decodeMDMCheckinRequest(r *http.Request) (interface{}, error) { return request, nil } +// The enrollment request is PkCS7 signed. +// We'll ignore everything but the content for now +func decodeMDMEnrollmentRequest(r *http.Request) (interface{}, error) { + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + p7, err := pkcs7.Parse(data) + if err != nil { + return nil, err + } + // TODO: We should verify but not currently possible. Apple + // does no provide a cert for the CA. + var request depEnrollmentRequest + if err := plist.Unmarshal(p7.Content, &request); err != nil { + return nil, err + } + return request, nil +} + // errorer is implemented by all concrete response types. It allows us to // change the HTTP response code without needing to trigger an endpoint // (transport-level) error. For more information, read the big comment in @@ -39,6 +61,18 @@ func encodeResponse(w http.ResponseWriter, response interface{}) error { return enc.Encode(response) } +func enrollResponse(w http.ResponseWriter, response interface{}) error { + if e, ok := response.(errorer); ok && e.error() != nil { + // Not a Go kit transport error, but a business-logic error. + // Provide those as HTTP errors. + encodeError(w, e.error()) + return nil + } + resp := response.(depEnrollmentResponse) + w.Write(resp.Profile) + return nil +} + func encodeError(w http.ResponseWriter, err error) { w.WriteHeader(codeFrom(err)) response := map[string]interface{}{ diff --git a/checkin/endpoint.go b/checkin/endpoint.go index 4e9d3a1c..4121233d 100644 --- a/checkin/endpoint.go +++ b/checkin/endpoint.go @@ -25,3 +25,14 @@ func makeCheckinEndpoint(svc MDMCheckinService) endpoint.Endpoint { return mdmCheckinResponse{}, nil } } + +func makeEnrollmentEndpoint(svc MDMCheckinService) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(depEnrollmentRequest) + profile, err := svc.Enroll(req.DEPEnrollmentRequest.UDID) + if err != nil { + return depEnrollmentResponse{Err: err}, nil + } + return depEnrollmentResponse{Profile: []byte(*profile)}, nil + } +} diff --git a/checkin/request_response.go b/checkin/request_response.go index 4b8d2e71..59b43204 100644 --- a/checkin/request_response.go +++ b/checkin/request_response.go @@ -11,3 +11,14 @@ type mdmCheckinResponse struct { } func (r mdmCheckinResponse) error() error { return r.Err } + +type depEnrollmentRequest struct { + mdm.DEPEnrollmentRequest +} + +type depEnrollmentResponse struct { + Profile []byte // MDM Enrollment Profile + Err error `plist:"error,omitempty"` +} + +func (r depEnrollmentResponse) error() error { return r.Err } diff --git a/checkin/service.go b/checkin/service.go index 0d44cb59..0ba0a9ee 100644 --- a/checkin/service.go +++ b/checkin/service.go @@ -12,6 +12,7 @@ import ( "github.com/go-kit/kit/metrics" kitprometheus "github.com/go-kit/kit/metrics/prometheus" httptransport "github.com/go-kit/kit/transport/http" + "github.com/gorilla/mux" "github.com/micromdm/mdm" "github.com/micromdm/micromdm/device" stdprometheus "github.com/prometheus/client_golang/prometheus" @@ -25,6 +26,7 @@ type MDMCheckinService interface { Authenticate(mdm.CheckinCommand) error TokenUpdate(mdm.CheckinCommand) error Checkout(mdm.CheckinCommand) error + Enroll(udid string) (*device.Profile, error) } // NewCheckinService creates a new MDM Checkin Service @@ -121,9 +123,21 @@ func (svc mdmCheckinService) Checkout(cmd mdm.CheckinCommand) error { return err } existing.Enrolled = boolPtr(false) + err = svc.db.SaveDevice(existing) + if err != nil { + return err + } return nil } +func (svc mdmCheckinService) Enroll(udid string) (*device.Profile, error) { + profile, err := svc.db.GetProfileForDevice(udid) + if err != nil { + return nil, err + } + return profile, nil +} + // return a pointer to a boolean func boolPtr(b bool) *bool { return &b @@ -141,5 +155,18 @@ func ServiceHandler(ctx context.Context, svc MDMCheckinService) http.Handler { decodeMDMCheckinRequest, encodeResponse, ) - return checkinHandler + + enroll := makeEnrollmentEndpoint(svc) + + enrollmentHandler := httptransport.NewServer( + ctx, + enroll, + decodeMDMEnrollmentRequest, + enrollResponse, + ) + + r := mux.NewRouter() + r.Methods("PUT").Path("/mdm/checkin").Handler(checkinHandler) + r.Methods("POST").Path("/mdm/checkin").Handler(enrollmentHandler) + return r } diff --git a/device/device.go b/device/device.go index ae300b6e..8acd0382 100644 --- a/device/device.go +++ b/device/device.go @@ -3,6 +3,7 @@ package device import ( "errors" "fmt" + "io/ioutil" "os" "time" @@ -36,6 +37,10 @@ type Device struct { Enrolled *bool `json:"enrolled,omitempty" db:"mdm_enrolled,omitempty"` } +// Profile is an Enrollment profile. +// For now we just have one. +type Profile []byte + // Datastore manages interactions of devices in a database type Datastore interface { AddDevice(*Device) error @@ -43,6 +48,7 @@ type Datastore interface { SaveDevice(*Device) error // RemoveDevice() error // AllDevices() DeviceList,error + GetProfileForDevice(udid string) (*Profile, error) } type config struct { @@ -173,6 +179,17 @@ func (db pgDatastore) SaveDevice(dev *Device) error { return nil } +// GetProfileForDevice returns an enrollment profile for a specific device +// For now there's a single profile stored on disk +func (db pgDatastore) GetProfileForDevice(uuid string) (*Profile, error) { + data, err := ioutil.ReadFile("data/profiles/Enrollment.mobileconfig") + if err != nil { + return nil, err + } + profile := Profile(data) + return &profile, nil +} + func migrate(db *sqlx.DB) { schema := ` CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; diff --git a/main.go b/main.go index 1288fc4e..1ef03512 100644 --- a/main.go +++ b/main.go @@ -119,7 +119,7 @@ func main() { // router r := mux.NewRouter() - r.Methods("PUT").Path("/mdm/checkin").Handler(checkinHandler) + r.Methods("PUT", "POST").Path("/mdm/checkin").Handler(checkinHandler) r.Methods("PUT").Path("/mdm/connect").Handler(connectHandler) r.Handle("/mdm/commands", commandHandler) r.Methods("POST").Path("/mdm/commands").Handler(commandHandler)