-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mongodb plugin #2698
Merged
Merged
Mongodb plugin #2698
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
580e497
WIP on mongodb plugin
calvn 059d1d9
Add mongodb plugin
calvn 981a41a
Add tests
calvn b468e74
Update mongodb.CreateUser() comment
calvn 814e691
Update docs
calvn 7c0bad8
Add missing docs
calvn 5bbc74e
Fix mongodb docs
calvn adfa25e
Minor comment and test updates
calvn 510729e
Fix imports
calvn 592c0cf
Fix dockertest import
calvn 539b19e
Set c.Initialized at the end, check for empty CreationStmts first on …
calvn 3646765
Remove Initialized check on Connection()
calvn f5c66f7
Add back Initialized check
calvn d0e2404
Update docs
calvn 5426b78
Move connProducer and credsProducer into pkg for mongodb and cassandra
calvn 3d180a7
Chage parseMongoURL to be a private func
calvn 445b94f
Default to admin if no db is provided in creation_statements
calvn 65b6cf7
Update comments and docs
calvn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package main | ||
|
||
import ( | ||
"log" | ||
"os" | ||
|
||
"github.com/hashicorp/vault/helper/pluginutil" | ||
"github.com/hashicorp/vault/plugins/database/mongodb" | ||
) | ||
|
||
func main() { | ||
apiClientMeta := &pluginutil.APIClientMeta{} | ||
flags := apiClientMeta.FlagSet() | ||
flags.Parse(os.Args) | ||
|
||
err := mongodb.Run(apiClientMeta.GetTLSConfig()) | ||
if err != nil { | ||
log.Println(err) | ||
os.Exit(1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package mongodb | ||
|
||
import ( | ||
"time" | ||
|
||
"encoding/json" | ||
|
||
"fmt" | ||
|
||
"github.com/hashicorp/vault/api" | ||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin" | ||
"github.com/hashicorp/vault/plugins" | ||
"github.com/hashicorp/vault/plugins/helper/database/connutil" | ||
"github.com/hashicorp/vault/plugins/helper/database/credsutil" | ||
"github.com/hashicorp/vault/plugins/helper/database/dbutil" | ||
"gopkg.in/mgo.v2" | ||
) | ||
|
||
const mongoDBTypeName = "mongodb" | ||
|
||
// MongoDB is an implementation of Database interface | ||
type MongoDB struct { | ||
connutil.ConnectionProducer | ||
credsutil.CredentialsProducer | ||
} | ||
|
||
// New returns a new MongoDB instance | ||
func New() (interface{}, error) { | ||
connProducer := &connutil.MongoDBConnectionProducer{} | ||
connProducer.Type = mongoDBTypeName | ||
|
||
credsProducer := &credsutil.MongoDBCredentialsProducer{} | ||
|
||
dbType := &MongoDB{ | ||
ConnectionProducer: connProducer, | ||
CredentialsProducer: credsProducer, | ||
} | ||
return dbType, nil | ||
} | ||
|
||
// Run instantiates a MongoDB object, and runs the RPC server for the plugin | ||
func Run(apiTLSConfig *api.TLSConfig) error { | ||
dbType, err := New() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
plugins.Serve(dbType.(*MongoDB), apiTLSConfig) | ||
|
||
return nil | ||
} | ||
|
||
// Type returns the TypeName for this backend | ||
func (m *MongoDB) Type() (string, error) { | ||
return mongoDBTypeName, nil | ||
} | ||
|
||
func (m *MongoDB) getConnection() (*mgo.Session, error) { | ||
session, err := m.Connection() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return session.(*mgo.Session), nil | ||
} | ||
|
||
// CreateUser generates the username/password on the underlying secret backend as instructed by | ||
// the CreationStatement provided. The creation statement is a JSON blob that has a db value, | ||
// and an array of roles that accepts a role, and an optional db value pair. This array will | ||
// be normalized the format specified in the mongoDB docs: | ||
// https://docs.mongodb.com/manual/reference/command/createUser/#dbcmd.createUser | ||
// | ||
// JSON Example: | ||
// { "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] } | ||
func (m *MongoDB) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { | ||
// Grab the lock | ||
m.Lock() | ||
defer m.Unlock() | ||
|
||
session, err := m.getConnection() | ||
if err != nil { | ||
return "", "", err | ||
} | ||
if statements.CreationStatements == "" { | ||
return "", "", dbutil.ErrEmptyCreationStatement | ||
} | ||
|
||
username, err = m.GenerateUsername(usernamePrefix) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
password, err = m.GeneratePassword() | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
// Unmarshal statements.CreationStatements into mongodbRoles | ||
var mongoCS mongoDBStatement | ||
err = json.Unmarshal([]byte(statements.CreationStatements), &mongoCS) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
// Check for db string | ||
if mongoCS.DB == "" { | ||
return "", "", fmt.Errorf("db value is required in creation statement") | ||
} | ||
|
||
if len(mongoCS.Roles) == 0 { | ||
return "", "", fmt.Errorf("roles array is required in creation statement") | ||
} | ||
|
||
createUserCmd := createUserCommand{ | ||
Username: username, | ||
Password: password, | ||
Roles: mongoCS.Roles.toStandardRolesArray(), | ||
} | ||
|
||
err = session.DB(mongoCS.DB).Run(createUserCmd, nil) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
return username, password, nil | ||
} | ||
|
||
// RenewUser is not supported on MongoDB, so this is a no-op. | ||
func (m *MongoDB) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error { | ||
// NOOP | ||
return nil | ||
} | ||
|
||
// RevokeUser drops the specified user from the authentication databse. If none is provided | ||
// in the revocation statement, the default "admin" authentication database will be assumed. | ||
func (m *MongoDB) RevokeUser(statements dbplugin.Statements, username string) error { | ||
session, err := m.getConnection() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// If no revocation statements provided, pass in empty JSON | ||
revocationStatement := statements.RevocationStatements | ||
if revocationStatement == "" { | ||
revocationStatement = `{}` | ||
} | ||
|
||
// Unmarshal revocation statements into mongodbRoles | ||
var mongoCS mongoDBStatement | ||
err = json.Unmarshal([]byte(revocationStatement), &mongoCS) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
db := mongoCS.DB | ||
// If db is not specified, use the default authenticationDatabase "admin" | ||
if db == "" { | ||
db = "admin" | ||
} | ||
|
||
err = session.DB(db).RemoveUser(username) | ||
if err != nil && err != mgo.ErrNotFound { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package mongodb | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"testing" | ||
"time" | ||
|
||
mgo "gopkg.in/mgo.v2" | ||
|
||
"strings" | ||
|
||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin" | ||
"github.com/hashicorp/vault/plugins/helper/database/connutil" | ||
dockertest "gopkg.in/ory-am/dockertest.v3" | ||
) | ||
|
||
const testMongoDBRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }` | ||
|
||
func prepareMongoDBTestContainer(t *testing.T) (cleanup func(), retURI string) { | ||
if os.Getenv("MONGODB_URI") != "" { | ||
return func() {}, os.Getenv("MONGODB_URI") | ||
} | ||
|
||
pool, err := dockertest.NewPool("") | ||
if err != nil { | ||
t.Fatalf("Failed to connect to docker: %s", err) | ||
} | ||
|
||
resource, err := pool.Run("mongo", "latest", []string{}) | ||
if err != nil { | ||
t.Fatalf("Could not start local mongo docker container: %s", err) | ||
} | ||
|
||
cleanup = func() { | ||
err := pool.Purge(resource) | ||
if err != nil { | ||
t.Fatalf("Failed to cleanup local container: %s", err) | ||
} | ||
} | ||
|
||
retURI = fmt.Sprintf("mongodb://localhost:%s", resource.GetPort("27017/tcp")) | ||
|
||
// exponential backoff-retry | ||
if err = pool.Retry(func() error { | ||
var err error | ||
dialInfo, err := connutil.ParseMongoURI(retURI) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
session, err := mgo.DialWithInfo(dialInfo) | ||
if err != nil { | ||
return err | ||
} | ||
session.SetSyncTimeout(1 * time.Minute) | ||
session.SetSocketTimeout(1 * time.Minute) | ||
return session.Ping() | ||
}); err != nil { | ||
t.Fatalf("Could not connect to mongo docker container: %s", err) | ||
} | ||
|
||
return | ||
} | ||
|
||
func TestMongoDB_Initialize(t *testing.T) { | ||
cleanup, connURI := prepareMongoDBTestContainer(t) | ||
defer cleanup() | ||
|
||
connectionDetails := map[string]interface{}{ | ||
"uri": connURI, | ||
} | ||
|
||
dbRaw, err := New() | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
db := dbRaw.(*MongoDB) | ||
connProducer := db.ConnectionProducer.(*connutil.MongoDBConnectionProducer) | ||
|
||
err = db.Initialize(connectionDetails, true) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if !connProducer.Initialized { | ||
t.Fatal("Database should be initialized") | ||
} | ||
|
||
err = db.Close() | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
} | ||
|
||
func TestMongoDB_CreateUser(t *testing.T) { | ||
cleanup, connURI := prepareMongoDBTestContainer(t) | ||
defer cleanup() | ||
|
||
connectionDetails := map[string]interface{}{ | ||
"uri": connURI, | ||
} | ||
|
||
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, | ||
} | ||
|
||
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if err := testCredsExist(t, connURI, username, password); err != nil { | ||
t.Fatalf("Could not connect with new credentials: %s", err) | ||
} | ||
} | ||
|
||
func TestMongoDB_RevokeUser(t *testing.T) { | ||
cleanup, connURI := prepareMongoDBTestContainer(t) | ||
defer cleanup() | ||
|
||
connectionDetails := map[string]interface{}{ | ||
"uri": connURI, | ||
} | ||
|
||
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, | ||
} | ||
|
||
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if err := testCredsExist(t, connURI, username, password); err != nil { | ||
t.Fatalf("Could not connect with new credentials: %s", err) | ||
} | ||
|
||
// Test default revocation statememt | ||
err = db.RevokeUser(statements, username) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if err = testCredsExist(t, connURI, username, password); err == nil { | ||
t.Fatal("Credentials were not revoked") | ||
} | ||
} | ||
|
||
func testCredsExist(t testing.TB, connURI, username, password string) error { | ||
connURI = strings.Replace(connURI, "mongodb://", fmt.Sprintf("mongodb://%s:%s@", username, password), 1) | ||
dialInfo, err := connutil.ParseMongoURI(connURI) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
session, err := mgo.DialWithInfo(dialInfo) | ||
if err != nil { | ||
return err | ||
} | ||
session.SetSyncTimeout(1 * time.Minute) | ||
session.SetSocketTimeout(1 * time.Minute) | ||
return session.Ping() | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this check to the top? No need to connect to the database if there is no statement provided.