From 41b47ef6a932bc02c23f5eec07dbafbc17c4a925 Mon Sep 17 00:00:00 2001 From: Johan McGwire Date: Tue, 4 Oct 2022 09:48:11 -0400 Subject: [PATCH 1/2] MongoDB Storage Capabilites --- cli/cli.go | 25 +++ docs/operations-guide.md | 16 ++ go.mod | 18 ++- go.sum | 57 ++++++- storage/mongodb/bootstrapTokenStore.go | 46 ++++++ storage/mongodb/certauth.go | 92 +++++++++++ storage/mongodb/checkinStore.go | 178 ++++++++++++++++++++++ storage/mongodb/commandAndResultsStore.go | 152 ++++++++++++++++++ storage/mongodb/migrate.go | 93 +++++++++++ storage/mongodb/mongodb.go | 132 ++++++++++++++++ storage/mongodb/push.go | 44 ++++++ storage/mongodb/pushcert.go | 72 +++++++++ 12 files changed, 920 insertions(+), 5 deletions(-) create mode 100644 storage/mongodb/bootstrapTokenStore.go create mode 100644 storage/mongodb/certauth.go create mode 100644 storage/mongodb/checkinStore.go create mode 100644 storage/mongodb/commandAndResultsStore.go create mode 100644 storage/mongodb/migrate.go create mode 100644 storage/mongodb/mongodb.go create mode 100644 storage/mongodb/push.go create mode 100644 storage/mongodb/pushcert.go diff --git a/cli/cli.go b/cli/cli.go index d058ba2..aa93daa 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -2,6 +2,7 @@ package cli import ( + "context" "errors" "fmt" "strings" @@ -10,6 +11,7 @@ import ( "github.com/micromdm/nanomdm/storage" "github.com/micromdm/nanomdm/storage/allmulti" "github.com/micromdm/nanomdm/storage/file" + "github.com/micromdm/nanomdm/storage/mongodb" "github.com/micromdm/nanomdm/storage/mysql" "github.com/micromdm/nanomdm/storage/pgsql" @@ -80,6 +82,12 @@ func (s *Storage) Parse(logger log.Logger) (storage.AllStorage, error) { return nil, err } mdmStorage = append(mdmStorage, pgsqlStorage) + case "mongodb": + mongodbStorage, err := mongodbStorageConfig(dsn, options) + if err != nil { + return nil, err + } + mdmStorage = append(mdmStorage, mongodbStorage) default: return nil, fmt.Errorf("unknown storage: %s", storage) } @@ -166,3 +174,20 @@ func pgsqlStorageConfig(dsn, options string, logger log.Logger) (*pgsql.PgSQLSto } return pgsql.New(opts...) } + +func mongodbStorageConfig(dsn, options string) (*mongodb.MongoDBStorage, error) { + var username, password string + if options != "" { + for k, v := range splitOptions(options) { + switch k { + case "username": + username = v + case "password": + password = v + default: + return nil, fmt.Errorf("invalid option: %q", k) + } + } + } + return mongodb.New(context.TODO(), dsn, username, password) +} diff --git a/docs/operations-guide.md b/docs/operations-guide.md index 82e4d3e..d597034 100644 --- a/docs/operations-guide.md +++ b/docs/operations-guide.md @@ -99,6 +99,22 @@ Options are specified as a comma-separated list of "key=value" pairs. The pgsql *Example:* `-storage pgsql -dsn postgres://postgres:toor@localhost/nanomdm -storage-options delete=1` +#### mongodb storage backend + +* `-storage mongodb` + +Configures the MongoDB storage backend. The `-dsn` flag should be in the [mongodb connection URI](https://www.mongodb.com/docs/drivers/go/current/fundamentals/connection/#connection-uri). The username and password should be omitted from the URI and specified in the `-storage-options` flag. + +*Example:* `-storage mongodb -dsn mongodb://localhost:27017` + +Options are specified as a comma-separated list of "key=value" pairs. The mongodb backend supports these options: +* `username=user` + * This option specifies the username to utilize when connecting to mongo +* `password=hunter2` + * This option specifies the password to utilize when connecting to mongo + +*Example:* `-storage mongodb -dsn mongodb://localhost:27017 -storage-options username=root,password=root` + #### multi-storage backend You can configure multiple storage backends to be used simultaneously. Specifying multiple sets of `-storage`, `-dsn`, & `-storage-options` flags will configure the "multi-storage" adapter. The flags must be specified in sets and are related to each other in the order they're specified: for example the first `-storage` flag corresponds to the first `-dsn` flag and so forth. diff --git a/go.mod b/go.mod index ecc6b48..e24f8c4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,20 @@ require ( ) require ( - golang.org/x/net v0.0.0-20191009170851-d66e71096ffb // indirect - golang.org/x/text v0.3.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect +) + +require ( + go.mongodb.org/mongo-driver v1.10.2 + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index 9c62244..35b2a61 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,73 @@ github.com/RobotsAndPencils/buford v0.14.0 h1:+d18IMEisYlRZZYfe6uFlmQGbT07kWro25V35fGptZM= github.com/RobotsAndPencils/buford v0.14.0/go.mod h1:F5FvdB/nkMby8Pge6HFpPHgLOeUZne/iE5wKzvx64Y0= github.com/aai/gocrypto v0.0.0-20160205191751-93df0c47f8b8/go.mod h1:nE/FnVUmtbP0EbgMVCUtDrm1+86H47QfJIdcmZb+J1s= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/groob/plist v0.0.0-20220217120414-63fa881b19a5 h1:saaSiB25B1wgaxrshQhurfPKUGJ4It3OxNJUy0rdOjU= github.com/groob/plist v0.0.0-20220217120414-63fa881b19a5/go.mod h1:itkABA+w2cw7x5nYUS/pLRef6ludkZKOigbROmCTaFw= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.10.2 h1:4Wk3cnqOrQCn0P92L3/mmurMxzdvWWs5J9jinAVKD+k= +go.mongodb.org/mongo-driver v1.10.2/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko= golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/storage/mongodb/bootstrapTokenStore.go b/storage/mongodb/bootstrapTokenStore.go new file mode 100644 index 0000000..c56228d --- /dev/null +++ b/storage/mongodb/bootstrapTokenStore.go @@ -0,0 +1,46 @@ +package mongodb + +import ( + "context" + + "github.com/micromdm/nanomdm/mdm" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type BootstrapTokenRecord struct { + UDID string `bson:"udid,omitempty"` + BootstrapToken []byte `bson:"bootstrap_token,omitempty"` +} + +func (m MongoDBStorage) StoreBootstrapToken(r *mdm.Request, msg *mdm.SetBootstrapToken) error { + upsert := true + filter := bson.M{ + "udid": r.ID, + } + update := bson.M{ + "$set": BootstrapTokenRecord{ + UDID: r.ID, + BootstrapToken: msg.BootstrapToken.BootstrapToken, + }, + } + + _, err := m.BootstrapTokenCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + + return err +} + +func (m MongoDBStorage) RetrieveBootstrapToken(r *mdm.Request, msg *mdm.GetBootstrapToken) (*mdm.BootstrapToken, error) { + filter := bson.M{ + "udid": r.ID, + } + + tokenRecord := &BootstrapTokenRecord{} + err := m.BootstrapTokenCollection.FindOne(context.TODO(), filter).Decode(tokenRecord) + if err != nil { + return nil, err + } + return &mdm.BootstrapToken{ + BootstrapToken: tokenRecord.BootstrapToken, + }, nil +} diff --git a/storage/mongodb/certauth.go b/storage/mongodb/certauth.go new file mode 100644 index 0000000..a5f11e0 --- /dev/null +++ b/storage/mongodb/certauth.go @@ -0,0 +1,92 @@ +package mongodb + +import ( + "context" + "errors" + + "github.com/micromdm/nanomdm/mdm" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type CertEnrollment struct { + EnrollmentID string `bson:"enrollment_id"` + CertHash string `bson:"cert_hash,omitempty"` + PreviousCertHashes []string `bson:"previous_cert_hashes,omitempty"` +} + +// Checks if the specific hash cert exists anywhere +func (m MongoDBStorage) HasCertHash(r *mdm.Request, hash string) (bool, error) { + filter := bson.M{ + "cert_hash": hash, + } + + res := CertEnrollment{} + err := m.CertAuthCollection.FindOne(context.TODO(), filter).Decode(&res) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return false, nil + } + return false, err + } + + // found a document which matches the cert hash, contents does not matter + return true, nil +} + +// Checks if an enrollment any cert hash +func (m MongoDBStorage) EnrollmentHasCertHash(r *mdm.Request, hash string) (bool, error) { + filter := bson.M{ + "cert_hash": hash, + "enrollment_id": r.ID, + } + + res := CertEnrollment{} + err := m.CertAuthCollection.FindOne(context.TODO(), filter).Decode(&res) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return false, nil + } + return false, err + } + + // found a document which matches the cert hash, contents does not matter + return true, nil +} + +// Checks if the cert hash matches the one associated with the enrollment +func (m MongoDBStorage) IsCertHashAssociated(r *mdm.Request, hash string) (bool, error) { + filter := bson.M{ + "enrollment_id": r.ID, + } + res := CertEnrollment{} + err := m.CertAuthCollection.FindOne(context.TODO(), filter).Decode(&res) + if err != nil { + return false, err + } + return hash == res.CertHash, nil +} + +// Associate the cert hash with the requested enrollment +func (m MongoDBStorage) AssociateCertHash(r *mdm.Request, hash string) error { + upsert := true + filter := bson.M{ + "enrollment_id": r.ID, + } + update := bson.M{ + "$set": CertEnrollment{ + EnrollmentID: r.ID, + CertHash: hash, + }, + "$push": bson.M{ + "previous_cert_hashes": hash, + }, + } + _, err := m.CertAuthCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + if err != nil { + return err + } + + return nil +} diff --git a/storage/mongodb/checkinStore.go b/storage/mongodb/checkinStore.go new file mode 100644 index 0000000..f00f5ec --- /dev/null +++ b/storage/mongodb/checkinStore.go @@ -0,0 +1,178 @@ +package mongodb + +import ( + "context" + "crypto/x509" + "errors" + + "github.com/micromdm/nanomdm/mdm" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type DeviceCheckinRecord struct { + UDID string `bson:"udid,omitempty"` + SerialNumber string `bson:"serial_number,omitempty"` + AuthenticateRequest string `bson:"authenticate_request,omitempty"` + Identity *x509.Certificate `bson:"identity,omitempty"` + UnlockToken []byte `bson:"unlock_token,omitempty"` + Children []string `bson:"children,omitempty"` + Parent string `bson:"parent,omitempty"` + TokenUpdate string `bson:"token_update,omitempty"` + TokenUpdateTally int `bson:"token_update_tally,omitempty"` + Disabled bool `bson:"disabled,omitempty"` + UserAuthenticateRequest string `bson:"user_authenticate_request,omitempty"` + DigestResponse string `bson:"digest_response,omitempty"` +} + +func (m MongoDBStorage) StoreAuthenticate(r *mdm.Request, msg *mdm.Authenticate) error { + upsert := true + filter := bson.M{ + "udid": r.ID, + } + update := bson.M{ + "$set": DeviceCheckinRecord{ + UDID: r.ID, + Identity: r.Certificate, + SerialNumber: msg.SerialNumber, + AuthenticateRequest: string(msg.Raw), + }, + } + + _, err := m.CheckinCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + + return err +} + +func (m MongoDBStorage) StoreTokenUpdate(r *mdm.Request, msg *mdm.TokenUpdate) error { + record := DeviceCheckinRecord{ + UDID: r.ID, + } + + if len(msg.UnlockToken) > 0 { + record.UnlockToken = msg.UnlockToken + } + + if r.ParentID != "" { + filter := bson.M{ + "udid": r.ParentID, + } + update := bson.M{ + "$push": bson.M{ + "children": r.ID, + }, + } + + res, err := m.CheckinCollection.UpdateOne(context.TODO(), filter, update) + if err != nil { + return err + } + if res.MatchedCount != 1 { + return errors.New("parent device enrollment missing on user token update") + } + } + + upsert := true + filter := bson.M{ + "udid": r.ID, + } + update := bson.M{ + "$set": DeviceCheckinRecord{ + UDID: r.ID, + Parent: r.ParentID, + TokenUpdate: string(msg.Raw), + }, + "$inc": DeviceCheckinRecord{ + TokenUpdateTally: 1, + }, + "$unset": bson.M{ + "disabled": "", + }, + } + _, err := m.CheckinCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + + return err +} + +func (m MongoDBStorage) StoreUserAuthenticate(r *mdm.Request, msg *mdm.UserAuthenticate) error { + upsert := true + filter := bson.M{ + "udid": r.ID, + } + update := bson.M{ + "$set": DeviceCheckinRecord{ + UDID: r.ID, + UserAuthenticateRequest: string(msg.Raw), + DigestResponse: msg.DigestResponse, + }, + } + _, err := m.CheckinCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + + return err +} + +func (m MongoDBStorage) Disable(r *mdm.Request) error { + childEnrollments, err := m.listChildEnrollments(r.ParentID) + if err != nil { + return err + } + + // Disable Child Enrollments + disableUpdate := bson.M{ + "$set": DeviceCheckinRecord{ + Disabled: true, + }, + } + + if len(childEnrollments) > 0 { + childFilter := bson.M{ + "udid": bson.M{ + "$in": childEnrollments, + }, + } + + _, err := m.CheckinCollection.UpdateOne(context.TODO(), childFilter, disableUpdate) + if err != nil { + return err + } + } + + // Disable Parent Enrollment + parentFilter := bson.M{ + "udid": r.ID, + } + _, err = m.CheckinCollection.UpdateOne(context.TODO(), parentFilter, disableUpdate) + + return err +} + +func (m MongoDBStorage) listChildEnrollments(udid string) ([]string, error) { + + filter := bson.M{ + "udid": udid, + } + + parentRecord := &DeviceCheckinRecord{} + res := m.CheckinCollection.FindOne(context.TODO(), filter) + err := res.Decode(parentRecord) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return []string{}, nil + } + return []string{}, err + } + + return parentRecord.Children, nil +} + +func (m MongoDBStorage) RetrieveTokenUpdateTally(ctx context.Context, id string) (int, error) { + filter := bson.M{ + "udid": id, + } + + res := DeviceCheckinRecord{} + err := m.CheckinCollection.FindOne(ctx, filter).Decode(&res) + + return res.TokenUpdateTally, err +} diff --git a/storage/mongodb/commandAndResultsStore.go b/storage/mongodb/commandAndResultsStore.go new file mode 100644 index 0000000..d2b0abc --- /dev/null +++ b/storage/mongodb/commandAndResultsStore.go @@ -0,0 +1,152 @@ +package mongodb + +import ( + "context" + "errors" + "strconv" + "time" + + "github.com/micromdm/nanomdm/mdm" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type PendingCommand struct { + UUID string `bson:"uuid,omitempty"` + EnrollmentUDID string `bson:"enrollment_udid,omitempty"` + EntryTimestamp string `bson:"entry_timestamp,omitempty"` + Status string `bson:"status,omitempty"` + StatusTimestamp string `bson:"status_timestamp,omitempty"` + Command *mdm.Command `bson:"command,omitempty"` +} + +type CompletedCommand struct { + Command *PendingCommand `bson:"command,omitempty"` + Result *mdm.CommandResults `bson:"result,omitempty"` + Timestamp string `bson:"timestamp,omitempty"` +} + +const ( + CommandStatusNotNow = "NotNow" + CommandStatusIdle = "Idle" +) + +func (m MongoDBStorage) StoreCommandReport(r *mdm.Request, report *mdm.CommandResults) error { + filter := bson.M{ + "uuid": report.CommandUUID, + "enrollment_udid": r.ID, + } + + if report.Status == CommandStatusNotNow || report.Status == CommandStatusIdle { + update := bson.M{ + "$set": PendingCommand{ + Status: report.Status, + StatusTimestamp: strconv.FormatInt(time.Now().Unix(), 10), + }, + } + + _, err := m.CommandPendingCollection.UpdateOne(context.TODO(), filter, update) + if err != nil { + return err + } + + return nil + } + + command := &PendingCommand{} + err := m.CommandPendingCollection.FindOneAndDelete(context.TODO(), filter).Decode(command) + if err != nil { + return err + } + + _, err = m.CommandResultCollection.InsertOne(context.TODO(), CompletedCommand{ + Command: command, + Result: report, + Timestamp: strconv.FormatInt(time.Now().Unix(), 10), + }) + if err != nil { + return err + } + + return nil +} + +func (m MongoDBStorage) RetrieveNextCommand(r *mdm.Request, skipNotNow bool) (*mdm.Command, error) { + filter := bson.M{ + "enrollment_udid": r.ID, + } + if skipNotNow { + filter = bson.M{ + "enrollment_udid": r.ID, + "status": bson.M{ + "$ne": CommandStatusNotNow, + }, + } + } + + earliestSort := bson.M{ + "$natural": 0, + } + + // Returning in LIFO ordering to ensure a single blocking command does not fail future commands + command := &PendingCommand{} + err := m.CommandPendingCollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(earliestSort)).Decode(command) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, nil + } + return nil, err + } + + return command.Command, nil +} + +func (m MongoDBStorage) EnqueueCommand(ctx context.Context, ids []string, cmd *mdm.Command) (map[string]error, error) { + commands := []interface{}{} + + for _, id := range ids { + commands = append(commands, PendingCommand{ + EnrollmentUDID: id, + UUID: cmd.CommandUUID, + Command: cmd, + EntryTimestamp: strconv.FormatInt(time.Now().Unix(), 10), + }) + } + + _, err := m.CommandPendingCollection.InsertMany(context.TODO(), commands) + if err != nil { + return nil, err + } + + // TODO (feature) - Perform validation on the inserted commands if not all commands were created successfully + + return nil, nil +} + +func (m MongoDBStorage) ClearQueue(r *mdm.Request) error { + if r.ParentID != "" { + return errors.New("can only clear a device channel queue") + } + + childEnrollments, err := m.listChildEnrollments(r.ID) + if err != nil { + return err + } + allEnrollments := []string{r.ID} + allEnrollments = append(allEnrollments, childEnrollments...) + + filter := bson.M{ + "enrollment_udid": bson.M{ + "$in": allEnrollments, + }, + } + + // TODO (feature) - Archive deleted commands + _, err = m.CommandPendingCollection.DeleteMany(context.Background(), filter) + if err != nil { + return err + } + + return nil +} diff --git a/storage/mongodb/migrate.go b/storage/mongodb/migrate.go new file mode 100644 index 0000000..156d159 --- /dev/null +++ b/storage/mongodb/migrate.go @@ -0,0 +1,93 @@ +package mongodb + +import ( + "context" + + "github.com/micromdm/nanomdm/mdm" + "go.mongodb.org/mongo-driver/bson" +) + +func (m MongoDBStorage) RetrieveMigrationCheckins(ctx context.Context, c chan<- interface{}) error { + + // Devices + deviceFilter := bson.M{ + "parent": bson.TypeNull, + } + + deviceCursor, err := m.CheckinCollection.Find(ctx, deviceFilter) + if err != nil { + return err + } + + deviceEnrollments := []DeviceCheckinRecord{} + if err := deviceCursor.All(ctx, &deviceEnrollments); err != nil { + return err + } + + for _, deviceEnrollment := range deviceEnrollments { + if deviceEnrollment.AuthenticateRequest != "" { + msg, err := mdm.DecodeCheckin([]byte(deviceEnrollment.AuthenticateRequest)) + if err != nil { + c <- err + continue + } + c <- msg + } else { + continue + } + + if deviceEnrollment.TokenUpdate != "" { + msg, err := mdm.DecodeCheckin([]byte(deviceEnrollment.TokenUpdate)) + if err != nil { + c <- err + continue + } + c <- msg + } else { + continue + } + } + + // Users + userFilter := bson.M{ + "parent": bson.M{ + "$ne": bson.TypeNull, + }, + } + + userCursor, err := m.CheckinCollection.Find(ctx, userFilter) + if err != nil { + return err + } + + userEnrollments := []DeviceCheckinRecord{} + if err := userCursor.All(ctx, &userEnrollments); err != nil { + return err + } + + for _, userEnrollments := range userEnrollments { + if userEnrollments.UserAuthenticateRequest != "" { + msg, err := mdm.DecodeCheckin([]byte(userEnrollments.UserAuthenticateRequest)) + if err != nil { + c <- err + continue + } + c <- msg + } else { + continue + } + + if userEnrollments.TokenUpdate != "" { + msg, err := mdm.DecodeCheckin([]byte(userEnrollments.TokenUpdate)) + if err != nil { + c <- err + continue + } + c <- msg + } else { + continue + } + } + + return nil +} diff --git a/storage/mongodb/mongodb.go b/storage/mongodb/mongodb.go new file mode 100644 index 0000000..1c7e007 --- /dev/null +++ b/storage/mongodb/mongodb.go @@ -0,0 +1,132 @@ +package mongodb + +import ( + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type MongoDBStorage struct { + MongoClient *mongo.Client + CheckinCollection *mongo.Collection + CommandResultCollection *mongo.Collection + CommandPendingCollection *mongo.Collection + BootstrapTokenCollection *mongo.Collection + PushCertCollection *mongo.Collection + CertAuthCollection *mongo.Collection +} + +// TODO (feature) - Enable configuration of these names +const ( + databaseName = "nanomdm" + + checkinStoreName = "checkin_store" + commandResultStoreName = "command_result_store" + commandPendingStoreName = "command_pending_store" + bootstrapTokenStoreName = "bootstrap_token_store" + pushCertStoreName = "push_cert_store" + certAuthStoreName = "cert_auth_store" +) + +func New(ctx context.Context, uri string, username string, password string) (*MongoDBStorage, error) { + var err error + storage := &MongoDBStorage{} + + mongoOpts := options.Client().ApplyURI(uri) + mongoOpts.SetAuth(options.Credential{Username: username, Password: password}) + + storage.MongoClient, err = mongo.NewClient(mongoOpts) + if err != nil { + return nil, err + } + + err = storage.MongoClient.Connect(ctx) + if err != nil { + return nil, err + } + + storage.CheckinCollection = storage.MongoClient.Database(databaseName).Collection(checkinStoreName) + _, err = storage.CheckinCollection.Indexes().CreateOne(context.TODO(), mongo.IndexModel{ + Keys: bson.M{ + "udid": 1, + }, + Options: options.Index().SetUnique(true), + }) + if err != nil { + return nil, err + } + + storage.CommandResultCollection = storage.MongoClient.Database(databaseName).Collection(commandResultStoreName) + storage.CommandPendingCollection = storage.MongoClient.Database(databaseName).Collection(commandPendingStoreName) + _, err = storage.CheckinCollection.Indexes().CreateMany(context.TODO(), []mongo.IndexModel{ + { + Keys: bson.M{ + "uuid": 1, + }, + }, + { + Keys: bson.M{ + "enrollment_udid": 2, + }, + }, + { + Keys: bson.M{ + "status": 3, + }, + }, + }) + if err != nil { + return nil, err + } + + storage.BootstrapTokenCollection = storage.MongoClient.Database(databaseName).Collection(checkinStoreName) + _, err = storage.BootstrapTokenCollection.Indexes().CreateOne(context.TODO(), mongo.IndexModel{ + Keys: bson.M{ + "udid": 1, + }, + Options: options.Index().SetUnique(true), + }) + if err != nil { + return nil, err + } + + storage.PushCertCollection = storage.MongoClient.Database(databaseName).Collection(pushCertStoreName) + _, err = storage.PushCertCollection.Indexes().CreateMany(context.TODO(), []mongo.IndexModel{ + { + Keys: bson.M{ + "ts": 1, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.M{ + "topic": 2, + }, + }, + }) + if err != nil { + return nil, err + } + + storage.CertAuthCollection = storage.MongoClient.Database(databaseName).Collection(certAuthStoreName) + _, err = storage.CertAuthCollection.Indexes().CreateMany(context.TODO(), []mongo.IndexModel{ + { + Keys: bson.M{ + "enrollment_id": 1, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.M{ + "cert_hash": 2, + }, + Options: options.Index().SetUnique(true), + }, + }) + if err != nil { + return nil, err + } + return storage, nil +} diff --git a/storage/mongodb/push.go b/storage/mongodb/push.go new file mode 100644 index 0000000..1e65283 --- /dev/null +++ b/storage/mongodb/push.go @@ -0,0 +1,44 @@ +package mongodb + +import ( + "context" + "errors" + + "github.com/micromdm/nanomdm/mdm" + "go.mongodb.org/mongo-driver/bson" +) + +func (m MongoDBStorage) RetrievePushInfo(ctx context.Context, ids []string) (map[string]*mdm.Push, error) { + pushInfos := make(map[string]*mdm.Push) + + filter := bson.M{ + "udid": bson.M{ + "$in": ids, + }, + } + cursor, err := m.CheckinCollection.Find(context.TODO(), filter) + if err != nil { + return nil, err + } + + records := []DeviceCheckinRecord{} + err = cursor.All(context.TODO(), &records) + if err != nil { + return nil, err + } + + for _, record := range records { + msg, err := mdm.DecodeCheckin([]byte(record.TokenUpdate)) + if err != nil { + return nil, err + } + + message, ok := msg.(*mdm.TokenUpdate) + if !ok { + return nil, errors.New("saved TokenUpdate is not a TokenUpdate") + } + + pushInfos[record.UDID] = &message.Push + } + return pushInfos, nil +} diff --git a/storage/mongodb/pushcert.go b/storage/mongodb/pushcert.go new file mode 100644 index 0000000..dec63ce --- /dev/null +++ b/storage/mongodb/pushcert.go @@ -0,0 +1,72 @@ +package mongodb + +import ( + "context" + "crypto/tls" + "strconv" + "time" + + "github.com/micromdm/nanomdm/cryptoutil" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type PushCertRecord struct { + Timestamp string `bson:"ts,omitempty"` + Certificate string `bson:"certificate,omitempty"` + PrivateKey string `bson:"key,omitempty"` + Topic string `bson:"topic,omitempty"` +} + +var latestSort = bson.M{ + "$natural": -1, +} + +func (m MongoDBStorage) IsPushCertStale(ctx context.Context, topic string, staleToken string) (bool, error) { + filter := bson.M{ + "topic": topic, + } + res := PushCertRecord{} + err := m.PushCertCollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(latestSort)).Decode(&res) + if err != nil { + return false, err + } + return res.Timestamp == staleToken, nil +} + +func (m MongoDBStorage) RetrievePushCert(ctx context.Context, topic string) (cert *tls.Certificate, staleToken string, err error) { + filter := bson.M{ + "topic": topic, + } + res := PushCertRecord{} + err = m.PushCertCollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(latestSort)).Decode(&res) + if err != nil { + return nil, "", err + } + + pushCert, err := tls.X509KeyPair([]byte(res.Certificate), []byte(res.PrivateKey)) + if err != nil { + return nil, "", err + } + + return &pushCert, res.Timestamp, nil +} + +func (m MongoDBStorage) StorePushCert(ctx context.Context, pemCert, pemKey []byte) error { + topic, err := cryptoutil.TopicFromPEMCert(pemCert) + if err != nil { + return err + } + + _, err = m.PushCertCollection.InsertOne(context.TODO(), PushCertRecord{ + Timestamp: strconv.FormatInt(time.Now().UnixNano(), 10), + Certificate: string(pemCert), + PrivateKey: string(pemKey), + Topic: topic, + }) + if err != nil { + return err + } + + return nil +} From 96e93e5a5a888290ae41fc2db3c1d540992d91a1 Mon Sep 17 00:00:00 2001 From: Johan McGwire Date: Fri, 21 Oct 2022 13:40:47 -0400 Subject: [PATCH 2/2] fix context.TODO() --- storage/mongodb/push.go | 4 ++-- storage/mongodb/pushcert.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/storage/mongodb/push.go b/storage/mongodb/push.go index 1e65283..e096122 100644 --- a/storage/mongodb/push.go +++ b/storage/mongodb/push.go @@ -16,13 +16,13 @@ func (m MongoDBStorage) RetrievePushInfo(ctx context.Context, ids []string) (map "$in": ids, }, } - cursor, err := m.CheckinCollection.Find(context.TODO(), filter) + cursor, err := m.CheckinCollection.Find(ctx, filter) if err != nil { return nil, err } records := []DeviceCheckinRecord{} - err = cursor.All(context.TODO(), &records) + err = cursor.All(ctx, &records) if err != nil { return nil, err } diff --git a/storage/mongodb/pushcert.go b/storage/mongodb/pushcert.go index dec63ce..69e9500 100644 --- a/storage/mongodb/pushcert.go +++ b/storage/mongodb/pushcert.go @@ -27,7 +27,7 @@ func (m MongoDBStorage) IsPushCertStale(ctx context.Context, topic string, stale "topic": topic, } res := PushCertRecord{} - err := m.PushCertCollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(latestSort)).Decode(&res) + err := m.PushCertCollection.FindOne(ctx, filter, options.FindOne().SetSort(latestSort)).Decode(&res) if err != nil { return false, err } @@ -39,7 +39,7 @@ func (m MongoDBStorage) RetrievePushCert(ctx context.Context, topic string) (cer "topic": topic, } res := PushCertRecord{} - err = m.PushCertCollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(latestSort)).Decode(&res) + err = m.PushCertCollection.FindOne(ctx, filter, options.FindOne().SetSort(latestSort)).Decode(&res) if err != nil { return nil, "", err } @@ -58,7 +58,7 @@ func (m MongoDBStorage) StorePushCert(ctx context.Context, pemCert, pemKey []byt return err } - _, err = m.PushCertCollection.InsertOne(context.TODO(), PushCertRecord{ + _, err = m.PushCertCollection.InsertOne(ctx, PushCertRecord{ Timestamp: strconv.FormatInt(time.Now().UnixNano(), 10), Certificate: string(pemCert), PrivateKey: string(pemKey),