From 3db7195ee9f15b768447ab5e11fd2da83c564eca Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Mon, 2 May 2016 16:44:33 -0400 Subject: [PATCH] add basic profile and workflow --- profile/profile.go | 5 +- workflow/workflow.go | 231 +++++++++++++++++++++++++++++++++++ workflow/workflow_db_test.go | 125 +++++++++++++++++++ 3 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 workflow/workflow.go create mode 100644 workflow/workflow_db_test.go diff --git a/profile/profile.go b/profile/profile.go index 55aaa302..07700575 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -5,12 +5,13 @@ import "time" // Profile is a configuration profile // See https://developer.apple.com/library/ios/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW4 type Profile struct { + UUID string `plist:"-" json:"-" db:"profile_uuid"` PayloadContent []PayloadDictionary PayloadDescription string `plist:",omitempty" json:",omitempty"` PayloadDisplayName string `plist:",omitempty" json:",omitempty"` PayloadExpirationDate time.Time `plist:",omitempty" json:",omitempty"` - PayloadIdentifier string - PayloadOrganization string `plist:",omitempty" json:",omitempty"` + PayloadIdentifier string `db:"identifier"` + PayloadOrganization string `plist:",omitempty" json:",omitempty"` PayloadUUID string PayloadRemovalDisallowed bool `plist:",omitempty" json:",omitempty"` PayloadType string diff --git a/workflow/workflow.go b/workflow/workflow.go new file mode 100644 index 00000000..8ad97bdb --- /dev/null +++ b/workflow/workflow.go @@ -0,0 +1,231 @@ +// Package workflow manages device workflows +package workflow + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/go-kit/kit/log" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" // postgres driver + "github.com/micromdm/micromdm/profile" + "golang.org/x/net/context" +) + +// ErrNoRowsModified is returned if insert didn't produce results +var ErrNoRowsModified = errors.New("DB: No rows affected") + +type application struct { + ManagementFlags int + ManifestURL string +} + +// Workflow is a device workflow +type Workflow struct { + UUID string `db:"workflow_uuid"` + Name string `db:"name"` + Profiles []profile.Profile + Applications []application + IncludedWorkflows []Workflow +} + +// Datastore manages interactions of workflows in a database +type Datastore interface { + CreateWorkflow(string) (*Workflow, error) + AddProfile(string, string) error + RemoveProfile(string, string) error + GetWorkflows() ([]Workflow, error) +} + +type pgDatastore struct { + *sqlx.DB +} + +func (db pgDatastore) CreateWorkflow(name string) (*Workflow, error) { + upsert := `INSERT INTO workflows + (name) + VALUES ($1) + ON CONFLICT ON CONSTRAINT workflows_name_key + DO NOTHING;` + + result, err := db.Exec( + upsert, + name, + ) + if err != nil { + return nil, err + } + if res, _ := result.RowsAffected(); res == 0 { + return nil, ErrNoRowsModified + } + var wf Workflow + err = db.Get(&wf, "SELECT * FROM workflows WHERE name=$1", name) + if err != nil { + return nil, err + } + return &wf, nil +} + +func (db pgDatastore) RemoveProfile(wfUUID, pfUUID string) error { + remove := `DELETE FROM workflow_profile + WHERE workflow_uuid=$1 AND profile_uuid=$2;` + result, err := db.Exec( + remove, + wfUUID, + pfUUID, + ) + if err != nil { + return err + } + if res, _ := result.RowsAffected(); res == 0 { + return ErrNoRowsModified + } + return nil +} + +func (db pgDatastore) GetWorkflows() ([]Workflow, error) { + var workflows []Workflow + err := db.Select(&workflows, "SELECT * FROM workflows") + if err != nil { + return nil, err + } + var withProfiles []Workflow + for _, wf := range workflows { + profiles, err := db.getProfilesForWorkflow(wf.UUID) + if err != nil { + return nil, err + } + wf.Profiles = profiles + withProfiles = append(withProfiles, wf) + } + return withProfiles, nil +} + +func (db pgDatastore) getProfilesForWorkflow(uuid string) ([]profile.Profile, error) { + var profileUUIDs []string + err := db.Select(&profileUUIDs, "SELECT profile_uuid FROM workflow_profile WHERE workflow_uuid=$1", uuid) + if err != nil { + return nil, err + } + + var profiles []profile.Profile + for _, id := range profileUUIDs { + var pf profile.Profile + err := db.Get(&pf, "SELECT * FROM profiles WHERE profile_uuid=$1", id) + if err != nil { + return nil, err + } + profiles = append(profiles, pf) + } + return profiles, nil +} + +func (db pgDatastore) AddProfile(wfUUID, pfUUID string) error { + update := `INSERT INTO workflow_profile + (workflow_uuid, profile_uuid) + VALUES ($1, $2);` + + result, err := db.Exec( + update, + wfUUID, + pfUUID, + ) + if err != nil { + return err + } + if res, _ := result.RowsAffected(); res == 0 { + return ErrNoRowsModified + } + return nil +} + +// Logger adds a logger to the database config +func Logger(logger log.Logger) func(*config) error { + return func(c *config) error { + c.logger = logger + return nil + } +} + +type config struct { + context context.Context + logger log.Logger +} + +// NewDB creates a new databases connection +func NewDB(driver, conn string, options ...func(*config) error) Datastore { + conf := &config{} + defaultLogger := log.NewLogfmtLogger(os.Stderr) + for _, option := range options { + if err := option(conf); err != nil { + defaultLogger.Log("err", err) + os.Exit(1) + } + } + if conf.logger == nil { + conf.logger = defaultLogger + } + switch driver { + case "postgres": + db, err := sqlx.Open(driver, conn) + if err != nil { + conf.logger.Log("err", err) + os.Exit(1) + } + var dbError error + maxAttempts := 20 + for attempts := 1; attempts <= maxAttempts; attempts++ { + dbError = db.Ping() + if dbError == nil { + break + } + conf.logger.Log("msg", fmt.Sprintf("could not connect to postgres: %v", dbError)) + time.Sleep(time.Duration(attempts) * time.Second) + } + if dbError != nil { + conf.logger.Log("err", dbError) + os.Exit(1) + } + migrate(db) + // TODO: configurable with default + db.SetMaxOpenConns(5) + store := pgDatastore{db} + return store + default: + conf.logger.Log("err", "unknown driver") + os.Exit(1) + return nil + } +} + +func migrate(db *sqlx.DB) { + schema := ` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE TABLE IF NOT EXISTS profiles ( + profile_uuid uuid PRIMARY KEY + DEFAULT uuid_generate_v4(), + identifier text UNIQUE NOT NULL + ); + + CREATE TABLE IF NOT EXISTS workflows ( + workflow_uuid uuid PRIMARY KEY + DEFAULT uuid_generate_v4(), + name text UNIQUE NOT NULL + ); + + CREATE TABLE IF NOT EXISTS workflow_profile ( + workflow_uuid uuid REFERENCES workflows, + profile_uuid uuid REFERENCES profiles, + PRIMARY KEY (workflow_uuid, profile_uuid) + ); + + CREATE TABLE IF NOT EXISTS workflow_workflow ( + workflow_uuid uuid REFERENCES workflows, + included_workflow_uuid uuid REFERENCES workflows(workflow_uuid), + PRIMARY KEY (workflow_uuid, included_workflow_uuid) + );` + + db.MustExec(schema) +} diff --git a/workflow/workflow_db_test.go b/workflow/workflow_db_test.go new file mode 100644 index 00000000..82ae485a --- /dev/null +++ b/workflow/workflow_db_test.go @@ -0,0 +1,125 @@ +package workflow + +import ( + "fmt" + "log" + "os" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/micromdm/micromdm/profile" +) + +var testConn = "user=micromdm password=micromdm dbname=micromdm sslmode=disable" + +func newDB(driver string) *sqlx.DB { + db, err := sqlx.Open(driver, testConn) + if err != nil { + panic(err) + } + return db +} + +func TestMain(m *testing.M) { + + db := newDB("postgres") + teardown(db) + retCode := m.Run() + teardown(db) + + // call with result of m.Run() + os.Exit(retCode) +} + +func TestNewDBConnection(t *testing.T) { + pg := newDB("postgres") + checkExists := `SELECT * from information_schema.tables WHERE table_name = 'workflows'` + t.Log("workflow: testing new postgres connection") + _ = NewDB("postgres", testConn) + _, err := pg.Query(checkExists) + if err != nil { + t.Fatal(err) + } +} + +func TestCreateWorkflow(t *testing.T) { + store := NewDB("postgres", testConn) + db := newDB("postgres") + db.MustExec(`INSERT INTO profiles (identifier) VALUES ($1);`, "com.micromdm.test") + db.MustExec(`INSERT INTO profiles (identifier) VALUES ($1);`, "com.micromdm.test2") + var pf profile.Profile + err := db.Get(&pf, "SELECT * FROM profiles WHERE identifier=$1", "com.micromdm.test") + if err != nil { + t.Fatal(err) + } + + wf, err := store.CreateWorkflow("test_workflow") + if err != nil { + t.Fatal(err) + } + + err = store.AddProfile(wf.UUID, pf.UUID) + if err != nil { + t.Fatal(err) + } + err = db.Get(&pf, "SELECT * FROM profiles WHERE identifier=$1", "com.micromdm.test2") + if err != nil { + t.Fatal(err) + } + err = store.AddProfile(wf.UUID, pf.UUID) + if err != nil { + t.Fatal(err) + } +} + +func TestGetWorkflows(t *testing.T) { + store := NewDB("postgres", testConn) + workflows, err := store.GetWorkflows() + if err != nil { + t.Fatal(err) + } + if len(workflows) == 0 { + t.Fatal("should have at least one workflow") + } + + if len(workflows[0].Profiles) == 0 { + t.Fatal("should have at least one profile") + } + fmt.Printf("%+v", workflows[0]) +} + +func TestRemoveProfile(t *testing.T) { + store := NewDB("postgres", testConn) + db := newDB("postgres") + var pf profile.Profile + err := db.Get(&pf, "SELECT * FROM profiles WHERE identifier=$1", "com.micromdm.test") + if err != nil { + t.Fatal(err) + } + + wf, err := store.CreateWorkflow("test_workflow_two") + if err != nil { + t.Fatal(err) + } + + err = store.AddProfile(wf.UUID, pf.UUID) + if err != nil { + t.Fatal(err) + } + + err = store.RemoveProfile(wf.UUID, pf.UUID) + if err != nil { + t.Fatal(err) + } +} + +func teardown(db *sqlx.DB) { + log.Println("workflow: dropping test tables") + drop := ` + DROP TABLE IF EXISTS workflow_workflow; + DROP TABLE IF EXISTS workflow_profile; + DROP TABLE IF EXISTS workflows; + DROP TABLE IF EXISTS profiles; + ` + db.MustExec(drop) +}