From cc53d2812187825f4f83945deff4fbc03b59e878 Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Mon, 5 Jul 2021 10:19:11 -0700 Subject: [PATCH] Implement a TokenUpdate "tally" for each backend. This is a simple counter that is bumped for each TokenUpdate that is later read for the TokenUpdate webhook in a new attribute. Resolves #16. --- cmd/nanomdm/main.go | 2 +- service/microwebhook/event.go | 4 ++++ service/microwebhook/service.go | 12 ++++++++++- storage/all.go | 1 + storage/allmulti/allmulti.go | 9 ++++++++ storage/file/file.go | 37 +++++++++++++++++++++++++++++++++ storage/mysql/mysql.go | 20 ++++++++++++++---- storage/mysql/schema.00003.sql | 1 + storage/mysql/schema.sql | 3 ++- storage/storage.go | 5 +++++ 10 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 storage/mysql/schema.00003.sql diff --git a/cmd/nanomdm/main.go b/cmd/nanomdm/main.go index 36925dc..a4cb365 100644 --- a/cmd/nanomdm/main.go +++ b/cmd/nanomdm/main.go @@ -96,7 +96,7 @@ func main() { if !*flDisableMDM { var mdmService service.CheckinAndCommandService = nano if *flWebhook != "" { - webhookService := microwebhook.New(*flWebhook) + webhookService := microwebhook.New(*flWebhook, mdmStorage) mdmService = multi.New(logger.With("service", "multi"), mdmService, webhookService) } certAuthOpts := []certauth.Option{certauth.WithLogger(logger.With("service", "certauth"))} diff --git a/service/microwebhook/event.go b/service/microwebhook/event.go index fecb780..9306ad2 100644 --- a/service/microwebhook/event.go +++ b/service/microwebhook/event.go @@ -25,4 +25,8 @@ type CheckinEvent struct { EnrollmentID string `json:"enrollment_id,omitempty"` Params map[string]string `json:"url_params"` RawPayload []byte `json:"raw_payload"` + + // signals which tokenupdate this is to be able to tell whether this + // is the initial enrollment vs. a following tokenupdate + TokenUpdateTally *int `json:"token_update_tally,omitempty"` } diff --git a/service/microwebhook/service.go b/service/microwebhook/service.go index d4184e6..6668f50 100644 --- a/service/microwebhook/service.go +++ b/service/microwebhook/service.go @@ -6,17 +6,20 @@ import ( "time" "github.com/micromdm/nanomdm/mdm" + "github.com/micromdm/nanomdm/storage" ) type MicroWebhook struct { url string client *http.Client + store storage.TokenUpdateTallyStore } -func New(url string) *MicroWebhook { +func New(url string, store storage.TokenUpdateTallyStore) *MicroWebhook { return &MicroWebhook{ url: url, client: http.DefaultClient, + store: store, } } @@ -45,6 +48,13 @@ func (w *MicroWebhook) TokenUpdate(r *mdm.Request, m *mdm.TokenUpdate) error { Params: r.Params, }, } + if w.store != nil { + tally, err := w.store.RetrieveTokenUpdateTally(r.Context, r.ID) + if err != nil { + return err + } + ev.CheckinEvent.TokenUpdateTally = &tally + } return postWebhookEvent(r.Context, w.client, w.url, ev) } diff --git a/storage/all.go b/storage/all.go index 9329418..bb69851 100644 --- a/storage/all.go +++ b/storage/all.go @@ -8,4 +8,5 @@ type AllStorage interface { CommandEnqueuer CertAuthStore StoreMigrator + TokenUpdateTallyStore } diff --git a/storage/allmulti/allmulti.go b/storage/allmulti/allmulti.go index 705a149..16a0b71 100644 --- a/storage/allmulti/allmulti.go +++ b/storage/allmulti/allmulti.go @@ -1,6 +1,8 @@ package allmulti import ( + "context" + "github.com/micromdm/nanomdm/log" "github.com/micromdm/nanomdm/mdm" "github.com/micromdm/nanomdm/storage" @@ -70,6 +72,13 @@ func (ms *MultiAllStorage) StoreTokenUpdate(r *mdm.Request, msg *mdm.TokenUpdate return err } +func (ms *MultiAllStorage) RetrieveTokenUpdateTally(ctx context.Context, id string) (int, error) { + val, err := ms.execStores(func(s storage.AllStorage) (interface{}, error) { + return s.RetrieveTokenUpdateTally(ctx, id) + }) + return val.(int), err +} + func (ms *MultiAllStorage) StoreUserAuthenticate(r *mdm.Request, msg *mdm.UserAuthenticate) error { _, err := ms.execStores(func(s storage.AllStorage) (interface{}, error) { return nil, s.StoreUserAuthenticate(r, msg) diff --git a/storage/file/file.go b/storage/file/file.go index f4962ae..31c1833 100644 --- a/storage/file/file.go +++ b/storage/file/file.go @@ -2,10 +2,12 @@ package file import ( + "context" "errors" "io/ioutil" "os" "path" + "strconv" "github.com/micromdm/nanomdm/cryptoutil" "github.com/micromdm/nanomdm/mdm" @@ -20,6 +22,8 @@ const ( DisabledFilename = "Disabled" BootstrapTokenFile = "BootstrapToken.dat" + TokenUpdateTallyFilename = "TokenUpdate.tally.txt" + UserAuthFilename = "UserAuthenticate.plist" UserAuthDigestFilename = "UserAuthenticate.Digest.plist" @@ -94,6 +98,28 @@ func (e *enrollment) fileExists(name string) (bool, error) { return true, nil } +func (e *enrollment) bumpNumericFile(name string) error { + ctr, err := e.readNumericFile(name) + if err != nil { + return err + } + ctr += 1 + return e.writeFile(name, []byte(strconv.Itoa(ctr))) +} + +func (e *enrollment) resetNumericFile(name string) error { + return e.writeFile(name, []byte{48}) +} + +func (e *enrollment) readNumericFile(name string) (int, error) { + val, err := e.readFile(name) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return 0, err + } + ctr, _ := strconv.Atoi(string(val)) + return ctr, nil +} + // assocSubEnrollment writes an empty file of the sub (user) enrollment for tracking. func (e *enrollment) assocSubEnrollment(id string) error { subPath := e.dirPrefix(SubEnrollmentPathname) @@ -162,6 +188,9 @@ func (s *FileStorage) StoreTokenUpdate(r *mdm.Request, msg *mdm.TokenUpdate) err if err := e.writeFile(TokenUpdateFilename, []byte(msg.Raw)); err != nil { return err } + if err := e.bumpNumericFile(TokenUpdateTallyFilename); err != nil { + return err + } // delete the disabled flag to let signify this enrollment is enabled if err := os.Remove(e.dirPrefix(DisabledFilename)); err != nil && !errors.Is(err, os.ErrNotExist) { return err @@ -169,6 +198,11 @@ func (s *FileStorage) StoreTokenUpdate(r *mdm.Request, msg *mdm.TokenUpdate) err return nil } +func (s *FileStorage) RetrieveTokenUpdateTally(_ context.Context, id string) (int, error) { + e := s.newEnrollment(id) + return e.readNumericFile(TokenUpdateTallyFilename) +} + func (s *FileStorage) StoreUserAuthenticate(r *mdm.Request, msg *mdm.UserAuthenticate) error { e := s.newEnrollment(r.ID) filename := UserAuthFilename @@ -195,6 +229,9 @@ func (s *FileStorage) Disable(r *mdm.Request) error { if err := e.writeFile(DisabledFilename, nil); err != nil { return err } + if err := e.resetNumericFile(TokenUpdateTallyFilename); err != nil { + return err + } } return e.removeSubEnrollments() } diff --git a/storage/mysql/mysql.go b/storage/mysql/mysql.go index 664dc1a..10476a9 100644 --- a/storage/mysql/mysql.go +++ b/storage/mysql/mysql.go @@ -2,6 +2,7 @@ package mysql import ( + "context" "database/sql" "errors" @@ -124,9 +125,9 @@ func (s *MySQLStorage) StoreTokenUpdate(r *mdm.Request, msg *mdm.TokenUpdate) er _, err = s.db.ExecContext( r.Context, ` INSERT INTO enrollments - (id, device_id, user_id, type, topic, push_magic, token_hex) + (id, device_id, user_id, type, topic, push_magic, token_hex, token_update_tally) VALUES - (?, ?, ?, ?, ?, ?, ?) AS new + (?, ?, ?, ?, ?, ?, ?, 1) AS new ON DUPLICATE KEY UPDATE device_id = new.device_id, @@ -135,7 +136,8 @@ UPDATE topic = new.topic, push_magic = new.push_magic, token_hex = new.token_hex, - enabled = 1;`, + enabled = 1, + enrollments.token_update_tally = enrollments.token_update_tally + 1;`, r.ID, deviceId, nullEmptyString(userId), @@ -147,6 +149,16 @@ UPDATE return err } +func (s *MySQLStorage) RetrieveTokenUpdateTally(ctx context.Context, id string) (int, error) { + var tally int + err := s.db.QueryRowContext( + ctx, + `SELECT token_update_tally FROM enrollments WHERE id = ?;`, + id, + ).Scan(&tally) + return tally, err +} + func (s *MySQLStorage) StoreUserAuthenticate(r *mdm.Request, msg *mdm.UserAuthenticate) error { colName := "user_authenticate" colAtName := "user_authenticate_at" @@ -184,7 +196,7 @@ func (s *MySQLStorage) Disable(r *mdm.Request) error { } _, err := s.db.ExecContext( r.Context, - `UPDATE enrollments SET enabled = 0 WHERE device_id = ? AND enabled = 1;`, + `UPDATE enrollments SET enabled = 0, token_update_tally = 0 WHERE device_id = ? AND enabled = 1;`, r.ID, ) return err diff --git a/storage/mysql/schema.00003.sql b/storage/mysql/schema.00003.sql new file mode 100644 index 0000000..8aa154c --- /dev/null +++ b/storage/mysql/schema.00003.sql @@ -0,0 +1 @@ +ALTER TABLE enrollments ADD COLUMN token_update_tally INTEGER NOT NULL DEFAULT 1; \ No newline at end of file diff --git a/storage/mysql/schema.sql b/storage/mysql/schema.sql index 8b3b24e..036a4b7 100644 --- a/storage/mysql/schema.sql +++ b/storage/mysql/schema.sql @@ -99,7 +99,8 @@ CREATE TABLE enrollments ( push_magic VARCHAR(127) NOT NULL, token_hex VARCHAR(255) NOT NULL, -- TODO: Perhaps just CHAR(64)? - enabled BOOLEAN NOT NULL DEFAULT 1, + enabled BOOLEAN NOT NULL DEFAULT 1, + token_update_tally INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, diff --git a/storage/storage.go b/storage/storage.go index e0859e7..22a1976 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -74,3 +74,8 @@ type StoreMigrator interface { // follow the device channel TokenUpdate. RetrieveMigrationCheckins(context.Context, chan<- interface{}) error } + +// TokenUpdateTallyStore retrieves the TokenUpdate tally (count) for an id +type TokenUpdateTallyStore interface { + RetrieveTokenUpdateTally(ctx context.Context, id string) (int, error) +}