From 97a97d9378ca2ae73629c9d5b6afe5aefe4c5577 Mon Sep 17 00:00:00 2001 From: Mosen Date: Sun, 17 Jul 2016 00:21:14 +1000 Subject: [PATCH] Fix some error strings in application datastore which still referred to devices. Add method SaveApplicationByDeviceUUID to add rows to `devices_applications` for the specified device uuid Add method NewDatastore since NewDB was untestable using mocks. Connect service now takes an applications datastore as one of its parameters. ackInstalledApplicationList implemented but not tested for InstallApplicationList responses. --- applications/datastore.go | 32 +++-- applications/datastore_test.go | 240 +++++++++++++++++++++++++++++++++ connect/service.go | 66 ++++++++- main.go | 3 + 4 files changed, 329 insertions(+), 12 deletions(-) diff --git a/applications/datastore.go b/applications/datastore.go index b811d412..92528304 100644 --- a/applications/datastore.go +++ b/applications/datastore.go @@ -15,10 +15,16 @@ type Datastore interface { New(a *Application) (string, error) Applications(params ...interface{}) ([]Application, error) GetApplicationsByDeviceUUID(deviceUUID string) (*[]Application, error) + SaveApplicationByDeviceUUID(deviceUUID string, app *Application) error } type pgStore struct { *sqlx.DB + logger kitlog.Logger +} + +func NewDatastore(connection *sqlx.DB, logger kitlog.Logger) (Datastore, error) { + return pgStore{DB: connection, logger: logger}, nil } func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) { @@ -26,7 +32,7 @@ func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) { case "postgres": db, err := sqlx.Open(driver, conn) if err != nil { - return nil, errors.Wrap(err, "device datastore") + return nil, errors.Wrap(err, "applications datastore") } var dbError error maxAttempts := 20 @@ -39,7 +45,7 @@ func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) { time.Sleep(time.Duration(attempts) * time.Second) } if dbError != nil { - return nil, errors.Wrap(dbError, "device datastore") + return nil, errors.Wrap(dbError, "applications datastore") } return pgStore{DB: db}, nil default: @@ -83,13 +89,13 @@ func (store pgStore) New(a *Application) (string, error) { dynamic_size, is_validated ) - VALUES ($1, $2, $3, $4, $5, $6, $7) + VALUES ($0, $1, $2, $3, $4, $5, $6) ON CONFLICT (name, version) DO UPDATE SET - identifier=$2, - short_version=$3, - bundle_size=$5, - dynamic_size=$6, - is_validated=$7 + identifier=$1, + short_version=$2, + bundle_size=$4, + dynamic_size=$5, + is_validated=$6 RETURNING application_uuid;`, a.Name, a.Identifier, @@ -135,6 +141,16 @@ func (store pgStore) GetApplicationsByDeviceUUID(deviceUUID string) (*[]Applicat return &apps, nil } +// Associate the given applications with the given device uuid by inserting into `device_applications`. +func (store pgStore) SaveApplicationByDeviceUUID(deviceUUID string, app *Application) error { + stmt := `INSERT INTO devices_applications ( + device_uuid, application_uuid + ) VALUES ($1, $2)` + + _, err := store.Exec(stmt, deviceUUID, app.UUID) + return err +} + // whereer is for building args passed into a method which finds resources type whereer interface { where() string diff --git a/applications/datastore_test.go b/applications/datastore_test.go index b0580a35..0aaa8ceb 100644 --- a/applications/datastore_test.go +++ b/applications/datastore_test.go @@ -1 +1,241 @@ package applications + +import ( + "database/sql" + "github.com/go-kit/kit/log" + "github.com/jmoiron/sqlx" + "gopkg.in/DATA-DOG/go-sqlmock.v1" + "testing" +) + +const MockUUID string = "ABCD-EFGH-IJKL" +const MockName string = "Mock Application" + +//func TestNewDB(t *testing.T) { +// var log log.Logger = log.NewNopLogger() +// appsDB, err := NewDB("postgres", "host=localhost", log) +// +// if err != nil { +// t.Error(err) +// } +// +// if _, ok := appsDB.(Datastore); !ok { +// t.Log("Did not get a datastore") +// t.Fail() +// } +//} + +func TestNewDatastore(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + if _, err := NewDatastore(dbx, log); err != nil { + t.Error(err) + } +} + +func TestNewApplication(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + appsDs, err := NewDatastore(dbx, log) + if err != nil { + t.Error(err) + } + + // macOS style: no DynamicSize, no IsValidated + fixture := Application{ + Name: "Keychain Access", + Identifier: sql.NullString{"com.apple.keychainaccess", true}, + ShortVersion: sql.NullString{"9.0", true}, + Version: sql.NullString{"9.0", true}, + BundleSize: sql.NullInt64{14166172, true}, + } + + newRow := sqlmock.NewRows([]string{"application_uuid"}).AddRow(MockUUID) + mock.ExpectQuery("INSERT INTO applications").WithArgs( + fixture.Name, + fixture.Identifier.String, + fixture.ShortVersion.String, + fixture.Version.String, + fixture.BundleSize.Int64, + nil, + nil, + ).WillReturnRows(newRow) + + appUuid, err := appsDs.New(&fixture) + if err != nil { + t.Error(err) + } + + if appUuid != MockUUID { + t.Errorf("inserting a mock application did not return the mock uuid, got: %s", appUuid) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestApplications(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + appsDs, err := NewDatastore(dbx, log) + if err != nil { + t.Error(err) + } + + mock.ExpectQuery(`SELECT .* FROM applications`).WillReturnRows( + sqlmock.NewRows([]string{"application_uuid"}), + ) + + if _, err := appsDs.Applications(); err != nil { + t.Error(err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestApplicationsWhereUUID(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + appsDs, err := NewDatastore(dbx, log) + if err != nil { + t.Error(err) + } + + mockRow := sqlmock.NewRows([]string{"application_uuid"}).AddRow(MockUUID) + mock.ExpectQuery(`WHERE application_uuid =`).WithArgs(MockUUID).WillReturnRows(mockRow) + + apps, err := appsDs.Applications(UUID{MockUUID}) + if err != nil { + t.Error(err) + } + + if len(apps) != 1 { + t.Fatalf("unexpected number of results returned: %d", len(apps)) + } + + if apps[0].UUID != MockUUID { + t.Errorf("unexpected application uuid when querying: %s", apps[0].UUID) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestApplicationsWhereName(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + appsDs, err := NewDatastore(dbx, log) + if err != nil { + t.Error(err) + } + + mockRow := sqlmock.NewRows([]string{"application_uuid", "name"}).AddRow(MockUUID, MockName) + mock.ExpectQuery(`WHERE name =`).WithArgs(MockName).WillReturnRows(mockRow) + + apps, err := appsDs.Applications(Name{MockName}) + if err != nil { + t.Error(err) + } + + if len(apps) != 1 { + t.Fatalf("unexpected number of results returned: %d", len(apps)) + } + + if apps[0].UUID != MockUUID { + t.Errorf("unexpected application uuid when querying: %s", apps[0].UUID) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestGetApplicationsByDeviceUUID(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + appsDs, err := NewDatastore(dbx, log) + if err != nil { + t.Error(err) + } + + mockRow := sqlmock.NewRows([]string{"application_uuid", "name"}).AddRow(MockUUID, MockName) + mock.ExpectQuery(`WHERE devices_applications.device_uuid=`).WithArgs(MockUUID).WillReturnRows(mockRow) + + if _, err := appsDs.GetApplicationsByDeviceUUID(MockUUID); err != nil { + t.Error(err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestSaveApplicationByDeviceUUID(t *testing.T) { + var log log.Logger = log.NewNopLogger() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + dbx := sqlx.NewDb(db, "mock") + defer dbx.Close() + + appsDs, err := NewDatastore(dbx, log) + if err != nil { + t.Error(err) + } + + fixture := Application{ + Name: "Keychain Access", + Identifier: sql.NullString{"com.apple.keychainaccess", true}, + ShortVersion: sql.NullString{"9.0", true}, + Version: sql.NullString{"9.0", true}, + BundleSize: sql.NullInt64{14166172, true}, + } + if err := appsDs.SaveApplicationByDeviceUUID(MockUUID, &fixture); err != nil { + t.Error(err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} diff --git a/connect/service.go b/connect/service.go index 8a232bec..62c7c68b 100644 --- a/connect/service.go +++ b/connect/service.go @@ -2,7 +2,9 @@ package connect import ( "encoding/json" + "fmt" "github.com/micromdm/mdm" + "github.com/micromdm/micromdm/applications" "github.com/micromdm/micromdm/command" "github.com/micromdm/micromdm/device" "github.com/pkg/errors" @@ -18,15 +20,17 @@ type Service interface { } // NewService creates a mdm service -func NewService(devices device.Datastore, cs command.Service) Service { +func NewService(devices device.Datastore, apps applications.Datastore, cs command.Service) Service { return &service{ commands: cs, devices: devices, + apps: apps, } } type service struct { devices device.Datastore + apps applications.Datastore commands command.Service } @@ -126,6 +130,60 @@ func (svc service) ackQueryResponses(req mdm.Response) error { return svc.devices.Save("queryResponses", &existing) } -//func (svc service) ackInstalledApplicationList(req mdm.Response) error { -// -//} +// Acknowledge a response to `InstalledApplicationList`. +func (svc service) ackInstalledApplicationList(req mdm.Response) error { + device, err := svc.devices.GetDeviceByUDID(req.UDID) + if err != nil { + return err + } + + deviceApps, err := svc.apps.GetApplicationsByDeviceUUID(device.UUID) + if err != nil { + return err + } + + // Any installed applications that are already represented in the applications datastore should be skipped. + var updated []applications.Application = make([]applications.Application, len(req.InstalledApplicationList)) + +skip: + for _, ackApp := range req.InstalledApplicationList { + for _, app := range *deviceApps { + if app.Name == ackApp.Name && app.Version == ackApp.Version { + continue skip + } + } + + updated = append(updated, ackApp) + } + + if len(updated) == 0 { + return nil + } + + // Determine applications which we have no record of at all, then insert them (find or create). + for _, newApp := range updated { + existing, err := svc.apps.Applications(applications.Name{newApp.Name}, applications.Version{newApp.Version}) + if err != nil { + return err + } + + switch { + case len(existing) > 1: + return fmt.Errorf("expected a single application match for application name: %s, got %d results", newApp.Name, len(existing)) + case len(existing) == 0: // No record exists and therefore both the application row and device association must be created. + appUuid, err := svc.apps.New(newApp) + if err != nil { + return err + } + + newApp.UUID = appUuid + } + + // For both len(existing) == 0 and len(existing) == 1, the row must be inserted for devices_applications. + if err := svc.apps.SaveApplicationByDeviceUUID(device.UUID, newApp.UUID); err != nil { + return err + } + } + + return nil +} diff --git a/main.go b/main.go index b16118f3..fbb9a544 100644 --- a/main.go +++ b/main.go @@ -204,12 +204,15 @@ func main() { commandSvc := command.NewService(commandDB) checkinSvc := checkin.NewService(deviceDB, mgmtSvc, commandSvc, enrollmentProfile) connectSvc := connect.NewService(deviceDB, commandSvc) + connectSvc := connect.NewService(deviceDB, appsDB, commandSvc) + enrollSvc, _ := enroll.NewService(*flPushCert, *flPushPass, *flTLSCACert, *flSCEPURL, *flURL) httpLogger := log.NewContext(logger).With("component", "http") managementHandler := management.ServiceHandler(ctx, mgmtSvc, httpLogger) commandHandler := command.ServiceHandler(ctx, commandSvc, httpLogger) checkinHandler := checkin.ServiceHandler(ctx, checkinSvc, httpLogger) connectHandler := connect.ServiceHandler(ctx, connectSvc, httpLogger) + enrollHandler := enroll.ServiceHandler(ctx, enrollSvc, httpLogger) mux := http.NewServeMux()