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

database: update plugin to adhere to Database v5 interface #14

Merged
merged 8 commits into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 2 additions & 36 deletions connection_producer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ package mongodbatlas

import (
"context"
"errors"
"sync"

"github.com/Sectorbob/mlab-ns2/gae/ns/digest"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/mitchellh/mapstructure"
"github.com/mongodb/go-client-mongodb-atlas/mongodbatlas"
)

Expand All @@ -23,40 +21,8 @@ type mongoDBAtlasConnectionProducer struct {
sync.Mutex
}

func (c *mongoDBAtlasConnectionProducer) Initialize(ctx context.Context, conf map[string]interface{}, verifyConnection bool) error {
_, err := c.Init(ctx, conf, verifyConnection)
return err
}

// Initialize parses connection configuration.
func (c *mongoDBAtlasConnectionProducer) Init(ctx context.Context, conf map[string]interface{}, verifyConnection bool) (map[string]interface{}, error) {
c.Lock()
defer c.Unlock()

err := mapstructure.WeakDecode(conf, c)
if err != nil {
return nil, err
}

if len(c.PublicKey) == 0 {
return nil, errors.New("public Key is not set")
}

if len(c.PrivateKey) == 0 {
return nil, errors.New("private Key is not set")
}

c.RawConfig = conf

// 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

return conf, nil
}

func (c *mongoDBAtlasConnectionProducer) secretValues() map[string]interface{} {
return map[string]interface{}{
func (c *mongoDBAtlasConnectionProducer) secretValues() map[string]string {
return map[string]string{
c.PrivateKey: "[private_key]",
}
}
Expand Down
11 changes: 3 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ go 1.12

require (
github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a
github.com/go-test/deep v1.0.2 // indirect
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f
github.com/hashicorp/vault/sdk v0.1.14-0.20201001203959-0ff44e5a3c48
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/mitchellh/mapstructure v1.1.2
github.com/mitchellh/mapstructure v1.3.2
github.com/mongodb/go-client-mongodb-atlas v0.1.2
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64 // indirect
)
240 changes: 230 additions & 10 deletions go.sum

Large diffs are not rendered by default.

134 changes: 79 additions & 55 deletions mongodbatlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import (
"encoding/json"
"errors"
"fmt"
"time"

"github.com/mitchellh/mapstructure"

"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/database/dbplugin"
calvn marked this conversation as resolved.
Show resolved Hide resolved
"github.com/hashicorp/vault/sdk/database/helper/credsutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/database/newdbplugin"
"github.com/mongodb/go-client-mongodb-atlas/mongodbatlas"
)

const mongoDBAtlasTypeName = "mongodbatlas"

// Verify interface is implemented
var _ dbplugin.Database = &MongoDBAtlas{}
var _ newdbplugin.Database = &MongoDBAtlas{}

type MongoDBAtlas struct {
*mongoDBAtlasConnectionProducer
Expand All @@ -26,13 +28,14 @@ type MongoDBAtlas struct {

func New() (interface{}, error) {
db := new()
dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues)
dbType := newdbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues)
return dbType, nil
}

func new() *MongoDBAtlas {
connProducer := &mongoDBAtlasConnectionProducer{}
connProducer.Type = mongoDBAtlasTypeName
connProducer := &mongoDBAtlasConnectionProducer{
Type: mongoDBAtlasTypeName,
}

credsProducer := &credsutil.SQLCredentialsProducer{
DisplayNameLen: credsutil.NoneLength,
Expand All @@ -59,37 +62,68 @@ func Run(apiTLSConfig *api.TLSConfig) error {
return nil
}

func (m *MongoDBAtlas) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) {
// Grab the lock
func (m *MongoDBAtlas) Initialize(ctx context.Context, req newdbplugin.InitializeRequest) (newdbplugin.InitializeResponse, error) {
m.Lock()
defer m.Unlock()

statements = dbutil.StatementCompatibilityHelper(statements)
m.RawConfig = req.Config

err := mapstructure.WeakDecode(req.Config, m.mongoDBAtlasConnectionProducer)
if err != nil {
return newdbplugin.InitializeResponse{}, err
}

if len(m.PublicKey) == 0 {
return newdbplugin.InitializeResponse{}, errors.New("public Key is not set")
}

if len(m.PrivateKey) == 0 {
return newdbplugin.InitializeResponse{}, errors.New("private Key is not set")
}

// Set initialized to true at this point since all fields are set,
// and the connection can be established at a later time.
m.Initialized = true

if len(statements.Creation) == 0 {
return "", "", dbutil.ErrEmptyCreationStatement
resp := newdbplugin.InitializeResponse{
Config: req.Config,
}

return resp, nil
}

func (m *MongoDBAtlas) NewUser(ctx context.Context, req newdbplugin.NewUserRequest) (newdbplugin.NewUserResponse, error) {
// Grab the lock
m.Lock()
defer m.Unlock()

if len(req.Statements.Commands) == 0 {
return newdbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement
}

client, err := m.getConnection(ctx)
if err != nil {
return "", "", err
return newdbplugin.NewUserResponse{}, err
}

username, err = m.GenerateUsername(usernameConfig)
username, err := m.GenerateUsername(dbplugin.UsernameConfig{
DisplayName: req.UsernameConfig.DisplayName,
RoleName: req.UsernameConfig.RoleName,
})
calvn marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", "", err
return newdbplugin.NewUserResponse{}, err
}

password, err = m.GeneratePassword()
password, err := m.GeneratePassword()
if err != nil {
return "", "", err
return newdbplugin.NewUserResponse{}, err
}

// Unmarshal statements.CreationStatements into mongodbRoles
var databaseUser mongoDBAtlasStatement
err = json.Unmarshal([]byte(statements.Creation[0]), &databaseUser)
err = json.Unmarshal([]byte(req.Statements.Commands[0]), &databaseUser)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since any statements beyond the first one are ignored, can you return an error if there are more than one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we are currently ignoring the other statements if they are provided (same for the MongoDB implementation/update). Would this be a breaking behavior if this is changed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it would since we'd be erroring if they provide more than one command rather than ignoring them, however I think this a bad user experience if we leave it as-is since we claim that we'll do something (additional commands) but don't actually.

Copy link
Member Author

@calvn calvn Oct 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theron pointed me to the elasticsearch bit of code where we do this. I think it's a fair point, though we should probably do the same for the mongodb (non-Atlas) db engine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the non-Atlas plugin

if err != nil {
return "", "", fmt.Errorf("Error unmarshalling statement %s", err)
return newdbplugin.NewUserResponse{}, fmt.Errorf("Error unmarshalling statement %s", err)
}

// Default to "admin" if no db provided
Expand All @@ -98,7 +132,7 @@ func (m *MongoDBAtlas) CreateUser(ctx context.Context, statements dbplugin.State
}

if len(databaseUser.Roles) == 0 {
return "", "", fmt.Errorf("roles array is required in creation statement")
return newdbplugin.NewUserResponse{}, fmt.Errorf("roles array is required in creation statement")
}

databaseUserRequest := &mongodbatlas.DatabaseUser{
Expand All @@ -110,63 +144,53 @@ func (m *MongoDBAtlas) CreateUser(ctx context.Context, statements dbplugin.State

_, _, err = client.DatabaseUsers.Create(ctx, m.ProjectID, databaseUserRequest)
if err != nil {
return "", "", err
return newdbplugin.NewUserResponse{}, err
}
return username, password, nil
}

// RenewUser is not supported on MongoDB, so this is a no-op.
func (m *MongoDBAtlas) RenewUser(ctx context.Context, statements dbplugin.Statements, username string, expiration time.Time) error {
// NOOP
return nil
resp := newdbplugin.NewUserResponse{
Username: username,
}

return resp, nil
}

// RevokeUser drops the specified user from the authentication database. If none is provided
// in the revocation statement, the default "admin" authentication database will be assumed.
func (m *MongoDBAtlas) RevokeUser(ctx context.Context, statements dbplugin.Statements, username string) error {
func (m *MongoDBAtlas) UpdateUser(ctx context.Context, req newdbplugin.UpdateUserRequest) (newdbplugin.UpdateUserResponse, error) {
if req.Password == nil {
calvn marked this conversation as resolved.
Show resolved Hide resolved
return newdbplugin.UpdateUserResponse{}, nil
}

m.Lock()
defer m.Unlock()

statements = dbutil.StatementCompatibilityHelper(statements)

client, err := m.getConnection(ctx)
if err != nil {
return err
return newdbplugin.UpdateUserResponse{}, err
}

_, err = client.DatabaseUsers.Delete(ctx, m.ProjectID, username)
return err
}

// SetCredentials uses provided information to set/create a user in the
// database. Unlike CreateUser, this method requires a username be provided and
// uses the name given, instead of generating a name. This is used for creating
// and setting the password of static accounts, as well as rolling back
// passwords in the database in the event an updated database fails to save in
// Vault's storage.
func (m *MongoDBAtlas) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
// Grab the lock
m.Lock()
defer m.Unlock()
databaseUserRequest := &mongodbatlas.DatabaseUser{
Password: req.Password.NewPassword,
}

client, err := m.getConnection(ctx)
_, _, err = client.DatabaseUsers.Update(context.Background(), m.ProjectID, req.Username, databaseUserRequest)
if err != nil {
return "", "", err
return newdbplugin.UpdateUserResponse{}, err
}

username = staticUser.Username
password = staticUser.Password
return newdbplugin.UpdateUserResponse{}, nil

databaseUserRequest := &mongodbatlas.DatabaseUser{
Password: password,
}
}

_, _, err = client.DatabaseUsers.Update(context.Background(), m.ProjectID, username, databaseUserRequest)
func (m *MongoDBAtlas) DeleteUser(ctx context.Context, req newdbplugin.DeleteUserRequest) (newdbplugin.DeleteUserResponse, error) {
m.Lock()
defer m.Unlock()

client, err := m.getConnection(ctx)
if err != nil {
return "", "", err
return newdbplugin.DeleteUserResponse{}, err
}

return username, password, nil
_, err = client.DatabaseUsers.Delete(ctx, m.ProjectID, req.Username)
return newdbplugin.DeleteUserResponse{}, err
}

func (m *MongoDBAtlas) getConnection(ctx context.Context) (*mongodbatlas.Client, error) {
Expand Down