From a9e7dbb7b43c9b9a716ead4e69e59d938e0ffca7 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Tue, 5 Dec 2017 15:31:01 -0500 Subject: [PATCH] Support MongoDB session-wide write concern (#3646) * Initial work on write concern support, set for the lifetime of the session * Add base64 encoded value support, include docs and tests * Handle error from json.Unmarshal, fix test and docs * Remove writeConcern struct, move JSON unmarshal to Initialize * Return error on empty mapping of write_concern into mgo.Safe struct --- .../database/mongodb/connection_producer.go | 33 +++++++++++++++ plugins/database/mongodb/mongodb_test.go | 40 +++++++++++++++++++ .../api/secret/databases/mongodb.html.md | 21 +++++++--- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/plugins/database/mongodb/connection_producer.go b/plugins/database/mongodb/connection_producer.go index f802dc35e5aa..a9ff64a943fa 100644 --- a/plugins/database/mongodb/connection_producer.go +++ b/plugins/database/mongodb/connection_producer.go @@ -2,6 +2,8 @@ package mongodb import ( "crypto/tls" + "encoding/base64" + "encoding/json" "errors" "fmt" "net" @@ -21,10 +23,12 @@ import ( // interface for databases to make connections. type mongoDBConnectionProducer struct { ConnectionURL string `json:"connection_url" structs:"connection_url" mapstructure:"connection_url"` + WriteConcern string `json:"write_concern" structs:"write_concern" mapstructure:"write_concern"` Initialized bool Type string session *mgo.Session + safe *mgo.Safe sync.Mutex } @@ -42,6 +46,30 @@ func (c *mongoDBConnectionProducer) Initialize(conf map[string]interface{}, veri return fmt.Errorf("connection_url cannot be empty") } + if c.WriteConcern != "" { + input := c.WriteConcern + + // Try to base64 decode the input. If successful, consider the decoded + // value as input. + inputBytes, err := base64.StdEncoding.DecodeString(input) + if err == nil { + input = string(inputBytes) + } + + concern := &mgo.Safe{} + err = json.Unmarshal([]byte(input), concern) + if err != nil { + return fmt.Errorf("error mashalling write_concern: %s", err) + } + + // Guard against empty, non-nil mgo.Safe object; we don't want to pass that + // into mgo.SetSafe in Connection(). + if (mgo.Safe{} == *concern) { + return fmt.Errorf("provided write_concern values did not map to any mgo.Safe fields") + } + c.safe = concern + } + // Set initialized to true at this point since all fields are set, // and the connection can be established at a later time. c.Initialized = true @@ -78,6 +106,11 @@ func (c *mongoDBConnectionProducer) Connection() (interface{}, error) { if err != nil { return nil, err } + + if c.safe != nil { + c.session.SetSafe(c.safe) + } + c.session.SetSyncTimeout(1 * time.Minute) c.session.SetSocketTimeout(1 * time.Minute) diff --git a/plugins/database/mongodb/mongodb_test.go b/plugins/database/mongodb/mongodb_test.go index 95f6e90888c3..4c1eacb66cb5 100644 --- a/plugins/database/mongodb/mongodb_test.go +++ b/plugins/database/mongodb/mongodb_test.go @@ -16,6 +16,8 @@ import ( const testMongoDBRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }` +const testMongoDBWriteConcern = `{ "wmode": "majority", "wtimeout": 5000 }` + func prepareMongoDBTestContainer(t *testing.T) (cleanup func(), retURL string) { if os.Getenv("MONGODB_URL") != "" { return func() {}, os.Getenv("MONGODB_URL") @@ -129,6 +131,44 @@ func TestMongoDB_CreateUser(t *testing.T) { } } +func TestMongoDB_CreateUser_writeConcern(t *testing.T) { + cleanup, connURL := prepareMongoDBTestContainer(t) + defer cleanup() + + connectionDetails := map[string]interface{}{ + "connection_url": connURL, + "write_concern": testMongoDBWriteConcern, + } + + dbRaw, err := New() + if err != nil { + t.Fatalf("err: %s", err) + } + db := dbRaw.(*MongoDB) + err = db.Initialize(connectionDetails, true) + if err != nil { + t.Fatalf("err: %s", err) + } + + statements := dbplugin.Statements{ + CreationStatements: testMongoDBRole, + } + + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := testCredsExist(t, connURL, username, password); err != nil { + t.Fatalf("Could not connect with new credentials: %s", err) + } +} + func TestMongoDB_RevokeUser(t *testing.T) { cleanup, connURL := prepareMongoDBTestContainer(t) defer cleanup() diff --git a/website/source/api/secret/databases/mongodb.html.md b/website/source/api/secret/databases/mongodb.html.md index 48a8ae2c401d..0d4857b6828b 100644 --- a/website/source/api/secret/databases/mongodb.html.md +++ b/website/source/api/secret/databases/mongodb.html.md @@ -20,10 +20,17 @@ has a number of parameters to further configure a connection. | Method | Path | Produces | | :------- | :--------------------------- | :--------------------- | -| `POST` | `/database/config/:name` | `204 (empty body)` | +| `POST` | `/database/config/:name` | `204 (empty body)` | ### Parameters -- `connection_url` `(string: )` – Specifies the MongoDB standard connection string (URI). + +- `connection_url` `(string: )` – Specifies the MongoDB standard + connection string (URI). +- `write_concern` `(string: "")` - Specifies the MongoDB [write + concern][mongodb-write-concern]. This is set for the entirety of the session, + maintained for the lifecycle of the plugin process. Must be a serialized JSON + object, or a base64-encoded serialized JSON object. The JSON payload values + map to the values in the [Safe][mgo-safe] struct from the mgo driver. ### Sample Payload @@ -31,7 +38,8 @@ has a number of parameters to further configure a connection. { "plugin_name": "mongodb-database-plugin", "allowed_roles": "readonly", - "connection_url": "mongodb://admin:Password!@mongodb.acme.com:27017/admin?ssl=true" + "connection_url": "mongodb://admin:Password!@mongodb.acme.com:27017/admin?ssl=true", + "write_concern": "{ \"wmode\": \"majority\", \"wtimeout\": 5000 }" } ``` @@ -68,7 +76,7 @@ list the plugin does not support that statement type. [MongoDB's documentation](https://docs.mongodb.com/manual/reference/method/db.createUser/). - `revocation_statements` `(string: "")` – Specifies the database statements to - be executed to revoke a user. Must be a serialized JSON object, or a base64-encoded + be executed to revoke a user. Must be a serialized JSON object, or a base64-encoded serialized JSON object. The object can optionally contain a "db" string. If no "db" value is provided, it defaults to the "admin" database. @@ -84,4 +92,7 @@ list the plugin does not support that statement type. } ] } -``` \ No newline at end of file +``` + +[mongodb-write-concern]: https://docs.mongodb.com/manual/reference/write-concern/ +[mgo-safe]: https://godoc.org/gopkg.in/mgo.v2#Safe \ No newline at end of file