Skip to content
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 18 commits into from
May 11, 2017
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions plugins/database/mongodb/mongodb-database-plugin/main.go
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)
}
}
167 changes: 167 additions & 0 deletions plugins/database/mongodb/mongodb.go
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
}
Copy link
Contributor

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.


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
}
184 changes: 184 additions & 0 deletions plugins/database/mongodb/mongodb_test.go
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()
}
Loading