update device datastore

This commit is contained in:
Victor Vrantchan
2016-05-16 15:40:41 -04:00
parent 9ab4fdd2e0
commit 4103418f55
8 changed files with 389 additions and 27 deletions

View File

@@ -23,9 +23,10 @@ var (
dep_profile_assign_time,
dep_profile_push_time,
dep_profile_assigned_date,
dep_profile_assigned_by
dep_profile_assigned_by,
dep_device
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (serial_number)
DO UPDATE SET
model = $2,
@@ -37,7 +38,8 @@ var (
dep_profile_assign_time = $8,
dep_profile_push_time = $9,
dep_profile_assigned_date = $10,
dep_profile_assigned_by = $11
dep_profile_assigned_by = $11,
dep_device = $12
RETURNING device_uuid;`
authenticateMDM = `INSERT INTO devices (
@@ -62,12 +64,21 @@ var (
imei=$7,
meid=$8
RETURNING device_uuid;`
selectDevicesStmt = `SELECT device_uuid,
udid,
serial_number,
dep_profile_status,
model,
workflow_uuid
FROM devices`
)
// Datastore manages devices in a database
type Datastore interface {
New(src string, d *Device) (string, error)
GetDeviceByUDID(udid string, fields ...string) (*Device, error)
Devices(params ...interface{}) ([]Device, error)
}
type pgStore struct {
@@ -97,6 +108,7 @@ func (store pgStore) New(src string, d *Device) (string, error) {
d.DEPProfilePushTime,
d.DEPProfileAssignedDate,
d.DEPProfileAssignedBy,
true,
).Scan(&d.UUID)
if err != nil {
return "", err
@@ -123,6 +135,38 @@ func (store pgStore) New(src string, d *Device) (string, error) {
}
}
func (store pgStore) Devices(params ...interface{}) ([]Device, error) {
stmt := selectDevicesStmt
stmt = addWhereFilters(stmt, params...)
var devices []Device
err := store.Select(&devices, stmt)
if err != nil {
return nil, errors.Wrap(err, "pgStore Devices")
}
return devices, nil
}
// whereer is for building args passed into a method which finds resources
type whereer interface {
where() string
}
// add WHERE clause from params
func addWhereFilters(stmt string, params ...interface{}) string {
var where []string
for _, param := range params {
if f, ok := param.(whereer); ok {
where = append(where, f.where())
}
}
if len(where) != 0 {
whereFilter := strings.Join(where, ",")
stmt = fmt.Sprintf("%s WHERE %s", stmt, whereFilter)
}
return stmt
}
//NewDB creates a Datastore
func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
switch driver {
@@ -157,7 +201,7 @@ func migrate(db *sqlx.DB) {
CREATE TABLE IF NOT EXISTS devices (
device_uuid uuid PRIMARY KEY
DEFAULT uuid_generate_v4(),
udid text,
udid text NOT NULL DEFAULT '',
serial_number text,
os_version text,
model text,
@@ -178,6 +222,8 @@ func migrate(db *sqlx.DB) {
apple_mdm_topic text,
apple_push_magic text,
mdm_enrolled boolean,
workflow_uuid text NOT NULL DEFAULT '',
dep_device boolean,
awaiting_configuration boolean
);
CREATE UNIQUE INDEX serial_idx ON devices (serial_number);`

View File

@@ -18,10 +18,19 @@ func TestNewDB(t *testing.T) {
}
}
func TestInsertFetch(t *testing.T) {
func TestRetrieveDevices(t *testing.T) {
ds := datastore(t)
defer teardown()
addTestDevices(t, ds)
_, err := ds.Devices()
if err != nil {
t.Fatal(err)
}
}
func addTestDevices(t *testing.T, ds Datastore) {
now := time.Now()
var devicetests = []struct {
in Device
}{
@@ -56,7 +65,62 @@ func TestInsertFetch(t *testing.T) {
Description: "It's a tablet",
Color: "pink",
AssetTag: "foo",
DEPProfileAssignTime: time.Now(),
DEPProfileAssignTime: &now,
},
},
}
for _, tt := range devicetests {
uuid, err := ds.New("fetch", &tt.in)
if err != nil {
t.Log("failed at", tt.in.SerialNumber)
t.Fatal(err)
}
if len(uuid) != 36 {
t.Errorf("newdevice fetch: expected uuid got %q", uuid)
}
}
}
func TestInsertFetch(t *testing.T) {
ds := datastore(t)
defer teardown()
now := time.Now()
var devicetests = []struct {
in Device
}{
{
Device{
SerialNumber: "DEADBEEF123A",
Model: "Macbook",
Description: "It's a laptop",
Color: "red",
},
},
{
Device{
SerialNumber: "DEADBEEF123A",
Model: "Macbook",
Description: "It's a laptop",
Color: "red",
},
},
{
Device{
SerialNumber: "DEADBEEF123B",
Model: "Macbook",
Description: "It's a laptop",
Color: "blue",
},
},
{
Device{
SerialNumber: "DEADBEEF123C",
Model: "iPad",
Description: "It's a tablet",
Color: "pink",
AssetTag: "foo",
DEPProfileAssignTime: &now,
},
},
}
@@ -77,6 +141,7 @@ func TestInsertAuthenticate(t *testing.T) {
ds := datastore(t)
defer teardown()
var now = time.Now()
var devicetests = []struct {
in Device
}{
@@ -129,7 +194,7 @@ func TestInsertAuthenticate(t *testing.T) {
Description: "It's a tablet",
Color: "pink",
AssetTag: "foo",
DEPProfileAssignTime: time.Now(),
DEPProfileAssignTime: &now,
},
},
}
@@ -149,6 +214,7 @@ func TestInsertAuthenticate(t *testing.T) {
func TestGetDeviceByUDID(t *testing.T) {
ds := datastore(t)
defer teardown()
var now = time.Now()
var devicetests = []struct {
in Device
}{
@@ -205,7 +271,7 @@ func TestGetDeviceByUDID(t *testing.T) {
Description: "It's a tablet",
Color: "pink",
AssetTag: "foo",
DEPProfileAssignTime: time.Now(),
DEPProfileAssignTime: &now,
},
},
}

View File

@@ -12,7 +12,7 @@ import (
type Device struct {
// Primary key is UUID
UUID string `json:"uuid" db:"device_uuid"`
UDID string `json:"udid"`
UDID string `json:"udid,omitempty"`
SerialNumber string `json:"serial_number,omitempty" db:"serial_number,omitempty"`
OSVersion string `json:"os_version,omitempty" db:"os_version,omitempty"`
BuildVersion string `json:"build_version,omitempty" db:"build_version,omitempty"`
@@ -26,7 +26,7 @@ type Device struct {
Token string `json:"token,omitempty" db:"apple_mdm_token,omitempty"`
UnlockToken string `json:"unlock_token,omitempty" db:"unlock_token,omitempty"`
Enrolled bool `json:"enrolled,omitempty" db:"mdm_enrolled,omitempty"`
Workflow string `json:"workflow,omitempty" db:"workflow_uuid"`
Workflow string `json:"workflow,omitempty" db:"workflow_uuid,omitempty"`
DEPDevice bool `json:"dep_device,omitempty" db:"dep_device,omitempty"`
Description string `json:"description,omitempty" db:"description"`
Model string `json:"model,omitempty" db:"model"`
@@ -34,10 +34,10 @@ type Device struct {
AssetTag string `json:"asset_tag,omitempty" db:"asset_tag"`
DEPProfileStatus DEPProfileStatus `json:"dep_profile_status,omitempty" db:"dep_profile_status"`
DEPProfileUUID string `json:"dep_profile_uuid,omitempty" db:"dep_profile_uuid"`
DEPProfileAssignTime time.Time `json:"dep_profile_assign_time,omitempty" db:"dep_profile_assign_time"`
DEPProfilePushTime time.Time `json:"dep_profile_push_time,omitempty" db:"dep_profile_push_time"`
DEPProfileAssignedDate time.Time `json:"dep_profile_assigned_date" db:"dep_profile_assigned_date"`
DEPProfileAssignedBy string `json:"dep_profile_assigned_by" db:"dep_profile_assigned_by"`
DEPProfileAssignTime *time.Time `json:"dep_profile_assign_time,omitempty" db:"dep_profile_assign_time"`
DEPProfilePushTime *time.Time `json:"dep_profile_push_time,omitempty" db:"dep_profile_push_time"`
DEPProfileAssignedDate *time.Time `json:"dep_profile_assigned_date,omitempty" db:"dep_profile_assigned_date"`
DEPProfileAssignedBy string `json:"dep_profile_assigned_by,omitempty" db:"dep_profile_assigned_by"`
}
// DEPProfileStatus is the status of the DEP Profile
@@ -53,7 +53,12 @@ const (
)
// Value implements Valuer from database/sql
func (status DEPProfileStatus) Value() (driver.Value, error) { return string(status), nil }
func (status DEPProfileStatus) Value() (driver.Value, error) {
if status == "" {
return "empty", nil
}
return string(status), nil
}
// Scan implements Scanner from database/sql
func (status *DEPProfileStatus) Scan(value interface{}) error {
@@ -62,7 +67,7 @@ func (status *DEPProfileStatus) Scan(value interface{}) error {
return nil
}
if sv, err := driver.String.ConvertValue(value); err == nil {
if v, ok := sv.(string); ok {
if v, ok := sv.([]byte); ok {
*status = DEPProfileStatus(v)
return nil
}
@@ -82,9 +87,9 @@ func NewFromDEP(dd dep.Device) *Device {
AssetTag: dd.AssetTag,
DEPProfileStatus: DEPProfileStatus(dd.ProfileStatus),
DEPProfileUUID: dd.ProfileUUID,
DEPProfileAssignTime: dd.ProfileAssignTime,
DEPProfilePushTime: dd.ProfilePushTime,
DEPProfileAssignedDate: dd.DeviceAssignedDate,
DEPProfileAssignTime: &dd.ProfileAssignTime,
DEPProfilePushTime: &dd.ProfilePushTime,
DEPProfileAssignedDate: &dd.DeviceAssignedDate,
DEPProfileAssignedBy: dd.DeviceAssignedBy,
}
}

View File

@@ -12,7 +12,7 @@ import (
var (
// ErrEmptyRequest is returned if the request body is empty
errEmptyRequest = errors.New("request must contain a profile identifier")
errEmptyRequest = errors.New("request must contain all required fields")
errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)")
)

View File

@@ -0,0 +1,57 @@
package management
import (
"encoding/json"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/micromdm/micromdm/workflow"
"golang.org/x/net/context"
)
type addWorkflowRequest struct {
*workflow.Workflow
}
type addWorkflowResponse struct {
*workflow.Workflow
Err error `json:"error,omitempty"`
}
func (r addWorkflowResponse) status() int { return http.StatusCreated }
func (r addWorkflowResponse) error() error { return r.Err }
func makeAddWorkflowEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(addWorkflowRequest)
wf, err := svc.AddWorkflow(req.Workflow)
return addWorkflowResponse{Err: err, Workflow: wf}, nil
}
}
type listWorkflowsRequest struct{}
type listWorkflowsResponse struct {
workflows []workflow.Workflow
Err error `json:"error,omitempty"`
}
func (r listWorkflowsResponse) error() error { return r.Err }
func (r listWorkflowsResponse) encodeList(w http.ResponseWriter) error {
jsn, err := json.MarshalIndent(r.workflows, "", " ")
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(jsn)
return nil
}
func makeListWorkflowsEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
workflows, err := svc.Workflows()
return listWorkflowsResponse{Err: err, workflows: workflows}, nil
}
}

View File

@@ -12,10 +12,15 @@ var ErrNotFound = errors.New("not found")
// Service is the interface that provides methods for managing devices
type Service interface {
// profiles
AddProfile(prf *workflow.Profile) (*workflow.Profile, error)
Profiles() ([]workflow.Profile, error)
Profile(uuid string) (*workflow.Profile, error)
DeleteProfile(uuid string) error
// workflows
AddWorkflow(wf *workflow.Workflow) (*workflow.Workflow, error)
Workflows() ([]workflow.Workflow, error)
// dep
FetchDEPDevices() error
}
@@ -73,6 +78,15 @@ func (svc service) FetchDEPDevices() error {
return nil
}
// workflows svc
func (svc service) AddWorkflow(wf *workflow.Workflow) (*workflow.Workflow, error) {
return svc.workflows.CreateWorkflow(wf)
}
func (svc service) Workflows() ([]workflow.Workflow, error) {
return svc.workflows.Workflows()
}
// NewService creates a management service
func NewService(ds device.Datastore, ws workflow.Datastore, dc dep.Client) Service {
return &service{

View File

@@ -59,13 +59,33 @@ func ServiceHandler(ctx context.Context, svc Service, logger kitlog.Logger) http
opts...,
)
addWorkflowHandler := kithttp.NewServer(
ctx,
makeAddWorkflowEndpoint(svc),
decodeAddWorkflowRequest,
encodeResponse,
opts...,
)
listWorkflowsHandler := kithttp.NewServer(
ctx,
makeListWorkflowsEndpoint(svc),
decodeListWorkflowsRequest,
encodeResponse,
opts...,
)
r := mux.NewRouter()
// dep
r.Handle("/management/v1/devices/fetch", fetchDEPHandler).Methods("POST")
// profiles
r.Handle("/management/v1/profiles", addProfileHandler).Methods("POST")
r.Handle("/management/v1/profiles", listProfilesHandler).Methods("GET")
r.Handle("/management/v1/profiles/{uuid}", showProfileHandler).Methods("GET")
r.Handle("/management/v1/profiles/{uuid}", deleteProfileHandler).Methods("DELETE")
// workflows
r.Handle("/management/v1/workflows", addWorkflowHandler).Methods("POST")
r.Handle("/management/v1/workflows", listWorkflowsHandler).Methods("GET")
return r
}
@@ -116,6 +136,23 @@ func decodeDeleteProfileRequest(_ context.Context, r *http.Request) (interface{}
return deleteProfileRequest{UUID: uuid}, nil
}
// workflow
func decodeAddWorkflowRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request addWorkflowRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err == io.EOF {
return nil, errEmptyRequest
}
if request.Name == "" {
return nil, errEmptyRequest
}
return request, err
}
func decodeListWorkflowsRequest(_ context.Context, r *http.Request) (interface{}, error) {
return listWorkflowsRequest{}, nil
}
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
if e, ok := response.(errorer); ok && e.error() != nil {
encodeError(ctx, e.error(), w)

View File

@@ -17,6 +17,110 @@ import (
"golang.org/x/net/context"
)
func TestAddWorkflowWithProfiles(t *testing.T) {
server, svc := newServer(t)
defer teardown()
defer server.Close()
profileData := []byte(`{
"payload_identifier": "com.micromdm.example2",
"data" : "fooProfile"
}`)
pfBody := testAddHTTP("profiles", t, svc, server, profileData, http.StatusCreated)
var pf workflow.Profile
err := json.NewDecoder(pfBody).Decode(&pf)
if err != nil {
t.Fatal(err)
}
wf := workflow.Workflow{
Name: "test_workflow",
Profiles: []workflow.Profile{pf},
}
wfData, err := json.Marshal(wf)
if err != nil {
t.Fatal(err)
}
wfBody := testAddHTTP("workflows", t, svc, server, wfData, http.StatusCreated)
var returned workflow.Workflow
err = json.NewDecoder(wfBody).Decode(&returned)
if err != nil {
t.Fatal(err)
}
if !returned.HasProfile(pf.PayloadIdentifier) {
t.Fatal("returned workflow must have profile")
}
}
// create some workflows to be used by tests
func addWorkflows(t *testing.T, server *httptest.Server, svc Service) []workflow.Workflow {
profileData := []byte(`{
"payload_identifier": "com.micromdm.example2",
"data" : "fooProfile"
}`)
pfBody := testAddHTTP("profiles", t, svc, server, profileData, http.StatusCreated)
var pf workflow.Profile
err := json.NewDecoder(pfBody).Decode(&pf)
if err != nil {
t.Fatal(err)
}
wf := workflow.Workflow{
Name: "test_workflow",
Profiles: []workflow.Profile{pf},
}
wfData, err := json.Marshal(wf)
if err != nil {
t.Fatal(err)
}
wfBody := testAddHTTP("workflows", t, svc, server, wfData, http.StatusCreated)
var returned workflow.Workflow
err = json.NewDecoder(wfBody).Decode(&returned)
if err != nil {
t.Fatal(err)
}
return []workflow.Workflow{returned}
}
func TestAddWorkflow(t *testing.T) {
server, svc := newServer(t)
defer teardown()
defer server.Close()
workflowData := []byte(`{
"name": "testWorkflow"
}`)
var addTests = []struct {
in []byte
expected int
}{
{
in: nil,
expected: http.StatusBadRequest,
},
{
in: workflowData,
expected: http.StatusCreated,
},
{
in: workflowData,
expected: http.StatusConflict,
},
}
for _, tt := range addTests {
testAddHTTP("workflows", t, svc, server, tt.in, tt.expected)
}
}
func TestDeleteProfile(t *testing.T) {
server, svc := newServer(t)
defer teardown()
@@ -29,7 +133,7 @@ func TestDeleteProfile(t *testing.T) {
"data" : "fooProfile"
}`)
testAddHTTP(t, svc, server, profileData, http.StatusCreated)
testAddHTTP("profiles", t, svc, server, profileData, http.StatusCreated)
profiles := testListHTTP(t, svc, server, http.StatusOK)
for _, p := range profiles {
testDeleteHTTP(t, svc, server, p.UUID, http.StatusNoContent)
@@ -49,7 +153,7 @@ func TestShowProfile(t *testing.T) {
testGetHTTP(t, svc, server, "foo", http.StatusBadRequest)
testGetHTTP(t, svc, server, "036d339c-4fe4-4d6e-a051-65fafbec8c93", http.StatusNotFound)
testAddHTTP(t, svc, server, profileData, http.StatusCreated)
testAddHTTP("profiles", t, svc, server, profileData, http.StatusCreated)
profiles := testListHTTP(t, svc, server, http.StatusOK)
for _, p := range profiles {
returned := testGetHTTP(t, svc, server, p.UUID, http.StatusOK)
@@ -98,6 +202,14 @@ func testGetHTTP(t *testing.T, svc Service, server *httptest.Server, uuid string
return &profile
}
func TestListWorkflows(t *testing.T) {
server, svc := newServer(t)
defer teardown()
defer server.Close()
addWorkflows(t, server, svc)
testListWorkflowsHTTP(t, svc, server, http.StatusOK)
}
func TestListProfiles(t *testing.T) {
server, svc := newServer(t)
defer teardown()
@@ -108,7 +220,7 @@ func TestListProfiles(t *testing.T) {
"data" : "fooProfile"
}`)
testAddHTTP(t, svc, server, profileData, http.StatusCreated)
testAddHTTP("profiles", t, svc, server, profileData, http.StatusCreated)
testListHTTP(t, svc, server, http.StatusOK)
}
@@ -142,7 +254,7 @@ func TestAddProfile(t *testing.T) {
}
for _, tt := range addTests {
testAddHTTP(t, svc, server, tt.in, tt.expected)
testAddHTTP("profiles", t, svc, server, tt.in, tt.expected)
}
}
@@ -156,6 +268,7 @@ func newServer(t *testing.T) (*httptest.Server, Service) {
if err != nil {
t.Fatal(err)
}
ps, err := workflow.NewDB("postgres", testConn, logger)
if err != nil {
t.Fatal(err)
@@ -167,6 +280,28 @@ func newServer(t *testing.T) (*httptest.Server, Service) {
return server, svc
}
func testListWorkflowsHTTP(t *testing.T, svc Service, server *httptest.Server, expectedStatus int) []workflow.Workflow {
client := http.DefaultClient
theURL := server.URL + "/management/v1/workflows"
resp, err := client.Get(theURL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != expectedStatus {
io.Copy(os.Stdout, resp.Body)
t.Fatal("expected", expectedStatus, "got", resp.StatusCode)
}
// test decoding the result into a struct
var workflows []workflow.Workflow
if err := json.NewDecoder(resp.Body).Decode(&workflows); err != nil {
t.Log("failed to decode profiles from list response")
t.Fatal(err)
}
return workflows
}
func testListHTTP(t *testing.T, svc Service, server *httptest.Server, expectedStatus int) []workflow.Profile {
client := http.DefaultClient
theURL := server.URL + "/management/v1/profiles"
@@ -189,11 +324,11 @@ func testListHTTP(t *testing.T, svc Service, server *httptest.Server, expectedSt
return profiles
}
func testAddHTTP(t *testing.T, svc Service, server *httptest.Server, profile []byte, expectedStatus int) {
body := &nopCloser{bytes.NewBuffer(profile)}
func testAddHTTP(endpoint string, t *testing.T, svc Service, server *httptest.Server, data []byte, expectedStatus int) io.Reader {
body := &nopCloser{bytes.NewBuffer(data)}
client := http.DefaultClient
theURL := server.URL + "/management/v1/profiles"
theURL := server.URL + "/management/v1/" + endpoint
resp, err := client.Post(theURL, "application/json", body)
if err != nil {
t.Fatal(err)
@@ -203,6 +338,8 @@ func testAddHTTP(t *testing.T, svc Service, server *httptest.Server, profile []b
io.Copy(os.Stdout, resp.Body)
t.Fatal("expected", expectedStatus, "got", resp.StatusCode)
}
return resp.Body
}
func TestFetchDEPDevices(t *testing.T) {