mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-13 05:45:41 +08:00
add basic profile and workflow
This commit is contained in:
@@ -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
|
||||
|
||||
231
workflow/workflow.go
Normal file
231
workflow/workflow.go
Normal file
@@ -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)
|
||||
}
|
||||
125
workflow/workflow_db_test.go
Normal file
125
workflow/workflow_db_test.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user