diff --git a/Makefile b/Makefile index d18dfa0..d1ed77a 100644 --- a/Makefile +++ b/Makefile @@ -62,4 +62,12 @@ release: \ test: go test -v -cover -race ./... -.PHONY: my docker $(DEPTOKENS) $(DEPSERVER) $(DEPSYNCER) clean release test \ No newline at end of file +.PHONY: my docker $(DEPTOKENS) $(DEPSERVER) $(DEPSYNCER) clean release test + +# Local Development +export MONGO_ROOT_USER = root +export MONGO_ROOT_PASSWORD = root + +.PHONY: docker-run-mongo +docker-run-mongo: + @ docker-compose -f storage/mongodb/docker-compose.yml up -d \ No newline at end of file diff --git a/cli/storage.go b/cli/storage.go index 221eb00..5108e64 100644 --- a/cli/storage.go +++ b/cli/storage.go @@ -7,6 +7,7 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/micromdm/nanodep/storage" "github.com/micromdm/nanodep/storage/file" + "github.com/micromdm/nanodep/storage/mongodb" "github.com/micromdm/nanodep/storage/mysql" ) @@ -22,6 +23,8 @@ func Storage(storageName, dsn string) (storage.AllStorage, error) { store, err = file.New(dsn) case "mysql": store, err = mysql.New(mysql.WithDSN(dsn)) + case "mongodb": + store, err = mongodb.New(dsn) default: return nil, fmt.Errorf("unknown storage: %q", storageName) } diff --git a/docs/mongodb-development.md b/docs/mongodb-development.md new file mode 100644 index 0000000..5172b22 --- /dev/null +++ b/docs/mongodb-development.md @@ -0,0 +1,30 @@ +# MongoDB Development + +This is just a quick walkthrough on how to setup a local development environment with MongoDB. Useful for building features or testing PRs. + +## Tools + +Docker +[MongoDB Compass](https://www.mongodb.com/try/download/compass) + +## Getting Started + +### Starting the mongodb container + +The `Makefile` has a quick way to spin up mongodb in docker using `make docker-run-mongo` + +You can change the default auth (username:password) credentials which are exported in the `Makefile` if you wish. + +The `make` command calls the docker-compose file located at `storage/mongodb/docker-compose.yml`. It uses (currently) mongodb version 4.4 because its compatible with apple silicon devices. + +### Connecting to the container + +This document wont cover using Compass to connect. The visualization is handy but not required. + +Start depserver/syncer + +``` +./depserver-darwin-amd64 -api supersecret -storage=mongodb -storage-dsn=mongodb://root:root@127.0.0.1:27017 + +./depsyncer-darwin-amd64 -storage=mongodb -storage-dsn=mongodb://root:root@127.0.0.1:27017 nanomdmdev +``` \ No newline at end of file diff --git a/docs/operations-guide.md b/docs/operations-guide.md index 36bf105..b92260b 100644 --- a/docs/operations-guide.md +++ b/docs/operations-guide.md @@ -58,6 +58,14 @@ Be sure to create the storage tables with the [schema.sql](../storage/mysql/sche *Example:* `-storage mysql -dsn nanodep:nanodep/mydepdb` +##### mongodb storage backend + +* `-storage mongodb` + +Configures the MongoDB storage backend. The `-dsn` flag should be in the [format the SQL driver expects](https://github.com/go-sql-driver/mysql#dsn-data-source-name). + +*Example:* `-storage=mongodb -storage-dsn=mongodb://root:root@127.0.0.1:27017` + #### -version * print version diff --git a/go.mod b/go.mod index 50cb683..be34b00 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,23 @@ require go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 require github.com/gomodule/oauth1 v0.2.0 -require github.com/go-sql-driver/mysql v1.6.0 +require ( + github.com/go-sql-driver/mysql v1.6.0 + go.mongodb.org/mongo-driver v1.10.3 + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 +) + +require ( + 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 + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum index d59153d..56e3e4c 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,66 @@ +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/gomodule/oauth1 v0.2.0 h1:/nNHAD99yipOEspQFbAnNmwGTZ1UNXiD/+JLxwx79fo= github.com/gomodule/oauth1 v0.2.0/go.mod h1:4r/a8/3RkhMBxJQWL5qzbOEcaQmNPIkNoI7P8sXeI08= +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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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.3 h1:XDQEvmh6z1EUsXuIkXE9TaVeqHw6SwS1uf93jFs0HBA= +go.mongodb.org/mongo-driver v1.10.3/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-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-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-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.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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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/docker-compose.yml b/storage/mongodb/docker-compose.yml new file mode 100644 index 0000000..4f53528 --- /dev/null +++ b/storage/mongodb/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.5" +name: nanomdm-dev +services: + mongo: + container_name: mongodb-dev + image: mongo:4.4 #4.4 is compatible with apple silicon + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USER} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD} + ports: + - 27017:27017 diff --git a/storage/mongodb/mongodb.go b/storage/mongodb/mongodb.go new file mode 100644 index 0000000..6aaadbe --- /dev/null +++ b/storage/mongodb/mongodb.go @@ -0,0 +1,366 @@ +package mongodb + +import ( + "context" + "time" + + "github.com/micromdm/nanodep/client" + "github.com/micromdm/nanodep/storage" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "gopkg.in/mgo.v2/bson" +) + +const ( + databaseName = "nanodep" + + tokenPKIStoreName = "token_pki_store" + authTokenStoreName = "auth_token_store" + configStoreName = "config_store" + cursorStoreName = "cursor_store" + profileStoreName = "profile_store" +) + +var latestSort = bson.M{ + "$natural": -1, +} + +type MongoDBStorage struct { + MongoClient *mongo.Client + TokenPKICollection *mongo.Collection + AuthTokenCollection *mongo.Collection + ConfigCollection *mongo.Collection + CursorCollection *mongo.Collection + ProfileCollection *mongo.Collection +} + +type AuthTokenRecord struct { + Name string `bson:"name"` + ConsumerKey string `bson:"consumer_key,omitempty"` + ConsumerSecret string `bson:"consumer_secret,omitempty"` + AccessToken string `bson:"access_token,omitempty"` + AccessSecret string `bson:"access_secret,omitempty"` + AccessTokenExpiry time.Time `bson:"access_token_expiry,omitempty"` +} + +type TokenPKIRecord struct { + Name string `bson:"name"` + Certificate string `bson:"certificate,omitempty"` + PrivateKey string `bson:"key,omitempty"` +} + +type ConfigRecord struct { + Name string `bson:"name"` + BaseURL string `bson:"base_url,omitempty"` +} + +type CursorRecord struct { + Name string `bson:"name"` + Cursor string `bson:"cursor,omitempty"` +} + +type ProfileRecord struct { + Name string `bson:"name"` + ProfileUUID string `bson:"profile_uuid,omitempty"` + Timestamp time.Time `bson:"timestamp,omitempty"` +} + +func New(uri string) (*MongoDBStorage, error) { + var err error + storage := &MongoDBStorage{} + + mongoOpts := options.Client().ApplyURI(uri) + + storage.MongoClient, err = mongo.NewClient(mongoOpts) + if err != nil { + return nil, err + } + + err = storage.MongoClient.Connect(context.TODO()) + if err != nil { + return nil, err + } + + storage.TokenPKICollection = storage.MongoClient.Database(databaseName).Collection(tokenPKIStoreName) + _, err = storage.TokenPKICollection.Indexes().CreateMany(context.TODO(), []mongo.IndexModel{ + { + Keys: bson.M{ + "tokenpki_cert_pem": 1, + }, + }, + { + Keys: bson.M{ + "tokenpki_key_pem": 2, + }, + }, + }) + if err != nil { + return nil, err + } + + storage.AuthTokenCollection = storage.MongoClient.Database(databaseName).Collection(authTokenStoreName) + _, err = storage.AuthTokenCollection.Indexes().CreateOne(context.TODO(), mongo.IndexModel{ + Keys: bson.M{ + "name": 1, + }, + }, + ) + if err != nil { + return nil, err + } + + storage.ConfigCollection = storage.MongoClient.Database(databaseName).Collection(configStoreName) + _, err = storage.ConfigCollection.Indexes().CreateOne(context.TODO(), mongo.IndexModel{ + Keys: bson.M{ + "name": 1, + }, + }, + ) + if err != nil { + return nil, err + } + + storage.CursorCollection = storage.MongoClient.Database(databaseName).Collection(cursorStoreName) + _, err = storage.CursorCollection.Indexes().CreateOne(context.TODO(), mongo.IndexModel{ + Keys: bson.M{ + "name": 1, + }, + }, + ) + if err != nil { + return nil, err + } + + storage.ProfileCollection = storage.MongoClient.Database(databaseName).Collection(profileStoreName) + _, err = storage.ProfileCollection.Indexes().CreateOne(context.TODO(), mongo.IndexModel{ + Keys: bson.M{ + "name": 1, + }, + }, + ) + if err != nil { + return nil, err + } + + return storage, nil +} + +// RetrieveAuthTokens reads the JSON DEP OAuth tokens from mongodb for name DEP name. +// +// In order to seed the database correctly, and pass the ckcheck (http/api/ckcheck.go) an empty consumer key +// should be returned if the ErrNoDocuments type is returned from the FindOne query. This will be true if the +// database is empty +// https://pkg.go.dev/go.mongodb.org/mongo-driver/mongo#ErrNoDocuments +func (s *MongoDBStorage) RetrieveAuthTokens(_ context.Context, name string) (*client.OAuth1Tokens, error) { + tokens := new(client.OAuth1Tokens) + resp := new(AuthTokenRecord) + + filter := bson.M{ + "name": name, + } + + err := s.AuthTokenCollection.FindOne(context.TODO(), filter).Decode(&resp) + if err != nil { + if err == mongo.ErrNoDocuments { + return &client.OAuth1Tokens{ + ConsumerKey: "", + }, storage.ErrNotFound + } + return nil, err + } + + tokens.ConsumerKey = resp.ConsumerKey + tokens.ConsumerSecret = resp.ConsumerSecret + tokens.AccessToken = resp.AccessToken + tokens.AccessSecret = resp.AccessSecret + tokens.AccessTokenExpiry = resp.AccessTokenExpiry + + return tokens, nil +} + +// StoreAuthTokens saves the DEP OAuth tokens to mongodb for name DEP name. +func (s *MongoDBStorage) StoreAuthTokens(_ context.Context, name string, tokens *client.OAuth1Tokens) error { + upsert := true + filter := bson.M{ + "name": name, + } + update := bson.M{ + "$set": &AuthTokenRecord{ + Name: name, + ConsumerKey: tokens.ConsumerKey, + ConsumerSecret: tokens.ConsumerSecret, + AccessToken: tokens.AccessToken, + AccessSecret: tokens.AccessSecret, + AccessTokenExpiry: tokens.AccessTokenExpiry, + }, + } + + _, err := s.AuthTokenCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + if err != nil { + return err + } + return nil +} + +// RetrieveConfig reads the JSON DEP config of a DEP name. +// +// Returns (nil, nil) if the DEP name does not exist, or if the config +// for the DEP name does not exist. +func (s *MongoDBStorage) RetrieveConfig(_ context.Context, name string) (*client.Config, error) { + config := new(client.Config) + resp := new(ConfigRecord) + + filter := bson.M{ + "name": name, + } + + err := s.ConfigCollection.FindOne(context.TODO(), filter).Decode(&resp) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + + config.BaseURL = resp.BaseURL + + return config, nil +} + +// StoreConfig saves the DEP config to mongodb for name DEP name. +func (s *MongoDBStorage) StoreConfig(_ context.Context, name string, config *client.Config) error { + upsert := true + filter := bson.M{ + "name": name, + } + update := bson.M{ + "$set": &ConfigRecord{ + Name: name, + BaseURL: config.BaseURL, + }, + } + + _, err := s.ConfigCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + if err != nil { + return err + } + return nil +} + +// RetrieveAssignerProfile reads the assigner profile UUID and its configured +// timestamp from mongodb for name DEP name. +// +// Returns an empty profile if it does not exist. +func (s *MongoDBStorage) RetrieveAssignerProfile(_ context.Context, name string) (string, time.Time, error) { + resp := new(ProfileRecord) + + filter := bson.M{ + "name": name, + } + + err := s.ProfileCollection.FindOne(context.TODO(), filter).Decode(&resp) + if err != nil { + if err == mongo.ErrNoDocuments { + return "", time.Time{}, nil + } + return "", time.Time{}, err + } + + return resp.ProfileUUID, resp.Timestamp, nil +} + +// StoreAssignerProfile saves the assigner profile UUID to disk for name DEP name. +func (s *MongoDBStorage) StoreAssignerProfile(_ context.Context, name string, profileUUID string) error { + upsert := true + filter := bson.M{ + "name": name, + } + update := bson.M{ + "$set": &ProfileRecord{ + Name: name, + ProfileUUID: profileUUID, + Timestamp: time.Now(), + }, + } + + _, err := s.ProfileCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + if err != nil { + return err + } + return nil +} + +// RetrieveCursor reads the reads the DEP fetch and sync cursor from mongodb +// for name DEP name. We return an empty cursor if the cursor does not exist +// in the database. +func (s *MongoDBStorage) RetrieveCursor(_ context.Context, name string) (string, error) { + resp := new(CursorRecord) + + filter := bson.M{ + "name": name, + } + + err := s.CursorCollection.FindOne(context.TODO(), filter).Decode(&resp) + if err != nil { + if err == mongo.ErrNoDocuments { + return "", nil + } + return "", err + } + + return resp.Cursor, nil +} + +// StoreCursor saves the DEP fetch and sync cursor to mongodb for name DEP name. +func (s *MongoDBStorage) StoreCursor(_ context.Context, name, cursor string) error { + upsert := true + filter := bson.M{ + "name": name, + } + update := bson.M{ + "$set": &CursorRecord{ + Name: name, + Cursor: cursor, + }, + } + + _, err := s.CursorCollection.UpdateOne(context.TODO(), filter, update, &options.UpdateOptions{Upsert: &upsert}) + if err != nil { + return err + } + + return nil +} + +// TokenPKIRecord Store and Retrieve + +// StoreTokenPKI stores the PEM bytes in pemCert and pemKey to mongodb for name DEP name. +func (s *MongoDBStorage) StoreTokenPKI(_ context.Context, name string, pemCert []byte, pemKey []byte) error { + _, err := s.TokenPKICollection.InsertOne(context.TODO(), TokenPKIRecord{ + Certificate: string(pemCert), + PrivateKey: string(pemKey), + Name: name, + }) + if err != nil { + return err + } + return nil +} + +// RetrieveTokenPKI reads and returns the PEM bytes for the DEP token exchange +// certificate and private key from mongodb using name DEP name. +func (s *MongoDBStorage) RetrieveTokenPKI(_ context.Context, name string) ([]byte, []byte, error) { + filter := bson.M{ + "name": name, + } + res := TokenPKIRecord{} + err := s.TokenPKICollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(latestSort)).Decode(&res) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil, storage.ErrNotFound + } + return nil, nil, err + } + + return []byte(res.Certificate), []byte(res.PrivateKey), nil +} diff --git a/storage/mongodb/mongodb_test.go b/storage/mongodb/mongodb_test.go new file mode 100644 index 0000000..64fbb8d --- /dev/null +++ b/storage/mongodb/mongodb_test.go @@ -0,0 +1,70 @@ +package mongodb + +import ( + "context" + "os" + "testing" + + "github.com/micromdm/nanodep/storage" + "github.com/micromdm/nanodep/storage/storagetest" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func TestMongoDBStorage(t *testing.T) { + + testDSN := os.Getenv("NANODEP_MONGODB_STORAGE_TEST") + if testDSN == "" { + t.Skip("NANODEP_MONGODB_STORAGE_TEST not set") + } + initTestDB(t) + + storagetest.Run(t, func(t *testing.T) storage.AllStorage { + var err error + dsn := "mongodb://root:root@127.0.0.1:27017" + storage := &MongoDBStorage{} + mongoOpts := options.Client().ApplyURI(dsn) + + storage.MongoClient, err = mongo.NewClient(mongoOpts) + if err != nil { + t.Fatal(err) + } + s, err := New(dsn) + if err != nil { + t.Fatal(err) + } + return s + }) +} + +// initTestDB clears any existing data from the database. +func initTestDB(t *testing.T) error { + var err error + ctx := context.TODO() + dsn := "mongodb://root:root@127.0.0.1:27017" + storage := &MongoDBStorage{} + mongoOpts := options.Client().ApplyURI(dsn) + + storage.MongoClient, err = mongo.NewClient(mongoOpts) + if err != nil { + t.Fatal(err) + } + + err = storage.MongoClient.Connect(ctx) + if err != nil { + t.Fatal(err) + } + storage.TokenPKICollection = storage.MongoClient.Database(databaseName).Collection(tokenPKIStoreName) + storage.ProfileCollection = storage.MongoClient.Database(databaseName).Collection(profileStoreName) + storage.CursorCollection = storage.MongoClient.Database(databaseName).Collection(cursorStoreName) + storage.ConfigCollection = storage.MongoClient.Database(databaseName).Collection(configStoreName) + storage.AuthTokenCollection = storage.MongoClient.Database(databaseName).Collection(authTokenStoreName) + + storage.AuthTokenCollection.Drop(ctx) + storage.ConfigCollection.Drop(ctx) + storage.CursorCollection.Drop(ctx) + storage.ProfileCollection.Drop(ctx) + storage.TokenPKICollection.Drop(ctx) + + return nil +}