diff --git a/plugins/database/mongodb/connection_producer.go b/plugins/database/mongodb/connection_producer.go index 5915177020..59047e0042 100644 --- a/plugins/database/mongodb/connection_producer.go +++ b/plugins/database/mongodb/connection_producer.go @@ -10,10 +10,10 @@ import ( "sync" "time" - "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/sdk/database/helper/connutil" "github.com/hashicorp/vault/sdk/database/helper/dbutil" - "github.com/mitchellh/mapstructure" + + "github.com/hashicorp/errwrap" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" @@ -49,56 +49,6 @@ type writeConcern struct { J bool // Sync via the journal if present } -func (c *mongoDBConnectionProducer) 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 *mongoDBConnectionProducer) Init(ctx context.Context, conf map[string]interface{}, verifyConnection bool) (map[string]interface{}, error) { - c.Lock() - defer c.Unlock() - - c.RawConfig = conf - - err := mapstructure.WeakDecode(conf, c) - if err != nil { - return nil, err - } - - if len(c.ConnectionURL) == 0 { - return nil, fmt.Errorf("connection_url cannot be empty") - } - - writeOpts, err := c.getWriteConcern() - if err != nil { - return nil, err - } - - authOpts, err := c.getTLSAuth() - if err != nil { - return nil, err - } - - c.clientOptions = options.MergeClientOptions(writeOpts, authOpts) - - // 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 - - if verifyConnection { - if _, err := c.Connection(ctx); err != nil { - return nil, errwrap.Wrapf("error verifying connection: {{err}}", err) - } - - if err := c.client.Ping(ctx, readpref.Primary()); err != nil { - return nil, errwrap.Wrapf("error verifying connection: {{err}}", err) - } - } - - return conf, nil -} - // Connection creates or returns an existing a database connection. If the session fails // on a ping check, the session will be closed and then re-created. // This method does not lock the mutex and it is intended that this is the callers @@ -157,8 +107,8 @@ func (c *mongoDBConnectionProducer) Close() error { return nil } -func (c *mongoDBConnectionProducer) secretValues() map[string]interface{} { - return map[string]interface{}{ +func (c *mongoDBConnectionProducer) secretValues() map[string]string { + return map[string]string{ c.Password: "[password]", } } diff --git a/plugins/database/mongodb/connection_producer_test.go b/plugins/database/mongodb/connection_producer_test.go index 0ee104c144..19b6e94ebf 100644 --- a/plugins/database/mongodb/connection_producer_test.go +++ b/plugins/database/mongodb/connection_producer_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/vault/helper/testhelpers/certhelpers" "github.com/hashicorp/vault/helper/testhelpers/mongodb" + "github.com/hashicorp/vault/sdk/database/newdbplugin" "github.com/ory/dockertest" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -78,17 +79,20 @@ net: // Test mongo := new() - conf := map[string]interface{}{ - "connection_url": retURL, - "allowed_roles": "*", - "tls_certificate_key": clientCert.CombinedPEM(), - "tls_ca": caCert.Pem, + initReq := newdbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "connection_url": retURL, + "allowed_roles": "*", + "tls_certificate_key": clientCert.CombinedPEM(), + "tls_ca": caCert.Pem, + }, + VerifyConnection: true, } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _, err := mongo.Init(ctx, conf, true) + _, err := mongo.Initialize(ctx, initReq) if err != nil { t.Fatalf("Unable to initialize mongo engine: %s", err) } diff --git a/plugins/database/mongodb/mongodb.go b/plugins/database/mongodb/mongodb.go index 8af06839bb..a3029292a4 100644 --- a/plugins/database/mongodb/mongodb.go +++ b/plugins/database/mongodb/mongodb.go @@ -6,13 +6,15 @@ import ( "fmt" "io" "strings" - "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/database/dbplugin" "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/mitchellh/mapstructure" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/writeconcern" "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" ) @@ -22,32 +24,24 @@ const mongoDBTypeName = "mongodb" // MongoDB is an implementation of Database interface type MongoDB struct { *mongoDBConnectionProducer - credsutil.CredentialsProducer } -var _ dbplugin.Database = &MongoDB{} +var _ newdbplugin.Database = &MongoDB{} // New returns a new MongoDB instance func New() (interface{}, error) { db := new() - dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues) + dbType := newdbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues) return dbType, nil } func new() *MongoDB { - connProducer := &mongoDBConnectionProducer{} - connProducer.Type = mongoDBTypeName - - credsProducer := &credsutil.SQLCredentialsProducer{ - DisplayNameLen: 15, - RoleNameLen: 15, - UsernameLen: 100, - Separator: "-", + connProducer := &mongoDBConnectionProducer{ + Type: mongoDBTypeName, } return &MongoDB{ mongoDBConnectionProducer: connProducer, - CredentialsProducer: credsProducer, } } @@ -58,7 +52,7 @@ func Run(apiTLSConfig *api.TLSConfig) error { return err } - dbplugin.Serve(dbType.(dbplugin.Database), api.VaultPluginTLSProvider(apiTLSConfig)) + newdbplugin.Serve(dbType.(newdbplugin.Database), api.VaultPluginTLSProvider(apiTLSConfig)) return nil } @@ -77,40 +71,79 @@ func (m *MongoDB) getConnection(ctx context.Context) (*mongo.Client, error) { return client.(*mongo.Client), 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(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { +func (m *MongoDB) Initialize(ctx context.Context, req newdbplugin.InitializeRequest) (newdbplugin.InitializeResponse, error) { + m.Lock() + defer m.Unlock() + + m.RawConfig = req.Config + + err := mapstructure.WeakDecode(req.Config, m.mongoDBConnectionProducer) + if err != nil { + return newdbplugin.InitializeResponse{}, err + } + + if len(m.ConnectionURL) == 0 { + return newdbplugin.InitializeResponse{}, fmt.Errorf("connection_url cannot be empty-mongo fail") + } + + writeOpts, err := m.getWriteConcern() + if err != nil { + return newdbplugin.InitializeResponse{}, err + } + + authOpts, err := m.getTLSAuth() + if err != nil { + return newdbplugin.InitializeResponse{}, err + } + + m.clientOptions = options.MergeClientOptions(writeOpts, authOpts) + + // 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 req.VerifyConnection { + _, err := m.Connection(ctx) + if err != nil { + return newdbplugin.InitializeResponse{}, fmt.Errorf("failed to verify connection: %w", err) + } + + err = m.client.Ping(ctx, readpref.Primary()) + if err != nil { + return newdbplugin.InitializeResponse{}, fmt.Errorf("failed to verify connection: %w", err) + } + } + + resp := newdbplugin.InitializeResponse{ + Config: req.Config, + } + return resp, nil +} + +func (m *MongoDB) NewUser(ctx context.Context, req newdbplugin.NewUserRequest) (newdbplugin.NewUserResponse, error) { // Grab the lock m.Lock() defer m.Unlock() - statements = dbutil.StatementCompatibilityHelper(statements) - - if len(statements.Creation) == 0 { - return "", "", dbutil.ErrEmptyCreationStatement + if len(req.Statements.Commands) == 0 { + return newdbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement } - username, err = m.GenerateUsername(usernameConfig) + username, err := credsutil.GenerateUsername( + credsutil.DisplayName(req.UsernameConfig.DisplayName, 15), + credsutil.RoleName(req.UsernameConfig.RoleName, 15), + credsutil.MaxLength(100), + credsutil.Separator("-"), + ) if err != nil { - return "", "", err - } - - password, err = m.GeneratePassword() - if err != nil { - return "", "", err + return newdbplugin.NewUserResponse{}, err } // Unmarshal statements.CreationStatements into mongodbRoles var mongoCS mongoDBStatement - err = json.Unmarshal([]byte(statements.Creation[0]), &mongoCS) + err = json.Unmarshal([]byte(req.Statements.Commands[0]), &mongoCS) if err != nil { - return "", "", err + return newdbplugin.NewUserResponse{}, err } // Default to "admin" if no db provided @@ -119,83 +152,82 @@ func (m *MongoDB) CreateUser(ctx context.Context, statements dbplugin.Statements } if len(mongoCS.Roles) == 0 { - return "", "", fmt.Errorf("roles array is required in creation statement") + return newdbplugin.NewUserResponse{}, fmt.Errorf("roles array is required in creation statement") } createUserCmd := createUserCommand{ Username: username, - Password: password, + Password: req.Password, Roles: mongoCS.Roles.toStandardRolesArray(), } if err := m.runCommandWithRetry(ctx, mongoCS.DB, createUserCmd); err != nil { - return "", "", err + return newdbplugin.NewUserResponse{}, err } - return username, password, nil + resp := newdbplugin.NewUserResponse{ + Username: username, + } + return resp, nil } -// 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 *MongoDB) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { - // Grab the lock +func (m *MongoDB) UpdateUser(ctx context.Context, req newdbplugin.UpdateUserRequest) (newdbplugin.UpdateUserResponse, error) { + if req.Password != nil { + err := m.changeUserPassword(ctx, req.Username, req.Password.NewPassword) + return newdbplugin.UpdateUserResponse{}, err + } + return newdbplugin.UpdateUserResponse{}, nil +} + +func (m *MongoDB) changeUserPassword(ctx context.Context, username, password string) error { m.Lock() defer m.Unlock() - username = staticUser.Username - password = staticUser.Password + connURL := m.getConnectionURL() + cs, err := connstring.Parse(connURL) + if err != nil { + return err + } + // Currently doesn't support custom statements for changing the user's password changeUserCmd := &updateUserCommand{ Username: username, Password: password, } - connURL := m.getConnectionURL() - cs, err := connstring.Parse(connURL) + database := cs.Database + if username == m.Username || database == "" { + database = "admin" + } + + err = m.runCommandWithRetry(ctx, database, changeUserCmd) if err != nil { - return "", "", err - } - if err := m.runCommandWithRetry(ctx, cs.Database, changeUserCmd); err != nil { - return "", "", err + return err } - return username, password, nil -} - -// RenewUser is not supported on MongoDB, so this is a no-op. -func (m *MongoDB) RenewUser(ctx context.Context, statements dbplugin.Statements, username string, expiration time.Time) error { - // NOOP return 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 *MongoDB) RevokeUser(ctx context.Context, statements dbplugin.Statements, username string) error { +func (m *MongoDB) DeleteUser(ctx context.Context, req newdbplugin.DeleteUserRequest) (newdbplugin.DeleteUserResponse, error) { m.Lock() defer m.Unlock() - statements = dbutil.StatementCompatibilityHelper(statements) - // If no revocation statements provided, pass in empty JSON var revocationStatement string - switch len(statements.Revocation) { + switch len(req.Statements.Commands) { case 0: revocationStatement = `{}` case 1: - revocationStatement = statements.Revocation[0] + revocationStatement = req.Statements.Commands[0] default: - return fmt.Errorf("expected 0 or 1 revocation statements, got %d", len(statements.Revocation)) + return newdbplugin.DeleteUserResponse{}, fmt.Errorf("expected 0 or 1 revocation statements, got %d", len(req.Statements.Commands)) } // Unmarshal revocation statements into mongodbRoles var mongoCS mongoDBStatement err := json.Unmarshal([]byte(revocationStatement), &mongoCS) if err != nil { - return err + return newdbplugin.DeleteUserResponse{}, err } db := mongoCS.DB @@ -205,40 +237,12 @@ func (m *MongoDB) RevokeUser(ctx context.Context, statements dbplugin.Statements } dropUserCmd := &dropUserCommand{ - Username: username, + Username: req.Username, WriteConcern: writeconcern.New(writeconcern.WMajority()), } - return m.runCommandWithRetry(ctx, db, dropUserCmd) -} - -// RotateRootCredentials in MongoDB -func (m *MongoDB) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) { - // Grab the lock - m.Lock() - defer m.Unlock() - - if m.Username == "" { - return m.RawConfig, fmt.Errorf("username not specified for root credentials") - } - - password, err := m.GeneratePassword() - if err != nil { - return nil, err - } - - changeUserCmd := &updateUserCommand{ - Username: m.Username, - Password: password, - } - - if err := m.runCommandWithRetry(ctx, "admin", changeUserCmd); err != nil { - return nil, err - } - - m.RawConfig["password"] = password - m.Password = password - return m.RawConfig, nil + err = m.runCommandWithRetry(ctx, db, dropUserCmd) + return newdbplugin.DeleteUserResponse{}, err } // runCommandWithRetry runs a command and retries once more if there's a failure diff --git a/plugins/database/mongodb/mongodb_test.go b/plugins/database/mongodb/mongodb_test.go index 15ff767535..9bc9f8eca4 100644 --- a/plugins/database/mongodb/mongodb_test.go +++ b/plugins/database/mongodb/mongodb_test.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "net/url" "reflect" "strings" "testing" @@ -13,230 +12,185 @@ import ( "github.com/hashicorp/vault/helper/testhelpers/certhelpers" "github.com/hashicorp/vault/helper/testhelpers/mongodb" - "github.com/hashicorp/vault/sdk/database/dbplugin" + "github.com/hashicorp/vault/sdk/database/newdbplugin" + dbtesting "github.com/hashicorp/vault/sdk/database/newdbplugin/testing" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" ) -const testMongoDBRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }` - -const testMongoDBWriteConcern = `{ "wmode": "majority", "wtimeout": 5000 }` +const mongoAdminRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }` func TestMongoDB_Initialize(t *testing.T) { cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") defer cleanup() - connectionDetails := map[string]interface{}{ + db := new() + defer dbtesting.AssertClose(t, db) + + config := map[string]interface{}{ "connection_url": connURL, } - db := new() - _, err := db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) + // Make a copy since the original map could be modified by the Initialize call + expectedConfig := copyConfig(config) + + req := newdbplugin.InitializeRequest{ + Config: config, + VerifyConnection: true, + } + + resp := dbtesting.AssertInitialize(t, db, req) + + if !reflect.DeepEqual(resp.Config, expectedConfig) { + t.Fatalf("Actual config: %#v\nExpected config: %#v", resp.Config, expectedConfig) } if !db.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, connURL := mongodb.PrepareTestContainer(t, "latest") defer cleanup() - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - db := new() - _, err := db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } + defer dbtesting.AssertClose(t, db) - statements := dbplugin.Statements{ - Creation: []string{testMongoDBRole}, + initReq := newdbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "connection_url": connURL, + }, + VerifyConnection: true, } + dbtesting.AssertInitialize(t, db, initReq) - usernameConfig := dbplugin.UsernameConfig{ - DisplayName: "test", - RoleName: "test", + password := "myreallysecurepassword" + createReq := newdbplugin.NewUserRequest{ + UsernameConfig: newdbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + }, + Statements: newdbplugin.Statements{ + Commands: []string{mongoAdminRole}, + }, + Password: password, + Expiration: time.Now().Add(time.Minute), } + createResp := dbtesting.AssertNewUser(t, db, createReq) - username, password, err := db.CreateUser(context.Background(), 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) - } + assertCredsExist(t, createResp.Username, password, connURL) } func TestMongoDB_CreateUser_writeConcern(t *testing.T) { cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") defer cleanup() - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - "write_concern": testMongoDBWriteConcern, + initReq := newdbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "connection_url": connURL, + "write_concern": `{ "wmode": "majority", "wtimeout": 5000 }`, + }, + VerifyConnection: true, } db := new() - _, err := db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } + defer dbtesting.AssertClose(t, db) - statements := dbplugin.Statements{ - Creation: []string{testMongoDBRole}, - } + dbtesting.AssertInitialize(t, db, initReq) - usernameConfig := dbplugin.UsernameConfig{ - DisplayName: "test", - RoleName: "test", + password := "myreallysecurepassword" + createReq := newdbplugin.NewUserRequest{ + UsernameConfig: newdbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + }, + Statements: newdbplugin.Statements{ + Commands: []string{mongoAdminRole}, + }, + Password: password, + Expiration: time.Now().Add(time.Minute), } + createResp := dbtesting.AssertNewUser(t, db, createReq) - username, password, err := db.CreateUser(context.Background(), 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) - } + assertCredsExist(t, createResp.Username, password, connURL) } -func TestMongoDB_RevokeUser(t *testing.T) { +func TestMongoDB_DeleteUser(t *testing.T) { cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") defer cleanup() - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - db := new() - _, err := db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } + defer dbtesting.AssertClose(t, db) - statements := dbplugin.Statements{ - Creation: []string{testMongoDBRole}, + initReq := newdbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "connection_url": connURL, + }, + VerifyConnection: true, } + dbtesting.AssertInitialize(t, db, initReq) - usernameConfig := dbplugin.UsernameConfig{ - DisplayName: "test", - RoleName: "test", - } - - username, password, err := db.CreateUser(context.Background(), 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) + password := "myreallysecurepassword" + createReq := newdbplugin.NewUserRequest{ + UsernameConfig: newdbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + }, + Statements: newdbplugin.Statements{ + Commands: []string{mongoAdminRole}, + }, + Password: password, + Expiration: time.Now().Add(time.Minute), } + createResp := dbtesting.AssertNewUser(t, db, createReq) + assertCredsExist(t, createResp.Username, password, connURL) // Test default revocation statement - err = db.RevokeUser(context.Background(), statements, username) - if err != nil { - t.Fatalf("err: %s", err) + delReq := newdbplugin.DeleteUserRequest{ + Username: createResp.Username, } - if err = testCredsExist(t, connURL, username, password); err == nil { - t.Fatal("Credentials were not revoked") - } + dbtesting.AssertDeleteUser(t, db, delReq) + + assertCredsDoNotExist(t, createResp.Username, password, connURL) } -func testCredsExist(t testing.TB, connURL, username, password string) error { - connURL = strings.Replace(connURL, "mongodb://", fmt.Sprintf("mongodb://%s:%s@", username, password), 1) - - ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) - client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL)) - if err != nil { - return err - } - return client.Ping(ctx, readpref.Primary()) -} - -func TestMongoDB_SetCredentials(t *testing.T) { +func TestMongoDB_UpdateUser_Password(t *testing.T) { cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") defer cleanup() // The docker test method PrepareTestContainer defaults to a database "test" // if none is provided connURL = connURL + "/test" - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - db := new() - _, err := db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) + defer dbtesting.AssertClose(t, db) + + initReq := newdbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "connection_url": connURL, + }, + VerifyConnection: true, } + dbtesting.AssertInitialize(t, db, initReq) // create the database user in advance, and test the connection dbUser := "testmongouser" startingPassword := "password" - testCreateDBUser(t, connURL, "test", dbUser, startingPassword) - if err := testCredsExist(t, connURL, dbUser, startingPassword); err != nil { - t.Fatalf("Could not connect with new credentials: %s", err) - } + createDBUser(t, connURL, "test", dbUser, startingPassword) - newPassword, err := db.GenerateCredentials(context.Background()) - if err != nil { - t.Fatal(err) - } + newPassword := "myreallysecurecredentials" - usernameConfig := dbplugin.StaticUserConfig{ + updateReq := newdbplugin.UpdateUserRequest{ Username: dbUser, - Password: newPassword, + Password: &newdbplugin.ChangePassword{ + NewPassword: newPassword, + }, } + dbtesting.AssertUpdateUser(t, db, updateReq) - username, password, err := db.SetCredentials(context.Background(), dbplugin.Statements{}, usernameConfig) - 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) - } - // confirm the original creds used to set still work (should be the same) - if err := testCredsExist(t, connURL, dbUser, newPassword); err != nil { - t.Fatalf("Could not connect with new credentials: %s", err) - } - - if (dbUser != username) || (newPassword != password) { - t.Fatalf("username/password mismatch: (%s)/(%s) vs (%s)/(%s)", dbUser, username, newPassword, password) - } -} - -func testCreateDBUser(t testing.TB, connURL, db, username, password string) { - ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) - client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL)) - if err != nil { - t.Fatal(err) - } - - createUserCmd := &createUserCommand{ - Username: username, - Password: password, - Roles: []interface{}{}, - } - result := client.Database(db).RunCommand(ctx, createUserCmd, nil) - if result.Err() != nil { - t.Fatal(result.Err()) - } + assertCredsExist(t, dbUser, newPassword, connURL) } func TestGetTLSAuth(t *testing.T) { @@ -336,129 +290,67 @@ func appendToCertPool(t *testing.T, pool *x509.CertPool, caPem []byte) *x509.Cer return pool } -func TestMongoDB_RotateRootCredentials(t *testing.T) { - cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") - defer cleanup() - - // Test to ensure that we can't rotate the root creds if no username has been specified - testCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - db := new() - connDetailsWithoutUsername := map[string]interface{}{ - "connection_url": connURL, - } - _, err := db.Init(testCtx, connDetailsWithoutUsername, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Rotate credentials should fail because no username is specified - cfg, err := db.RotateRootCredentials(testCtx, nil) - if err == nil { - t.Fatalf("successfully rotated root credentials when no username was present") - } - if !reflect.DeepEqual(cfg, connDetailsWithoutUsername) { - t.Fatalf("expected connection details: %#v but were %#v", connDetailsWithoutUsername, cfg) - } - - db.Close() - - // Reset the database object with new connection details - username := "vault-test-admin" - initialPassword := "myreallysecurepassword" - - db = new() - connDetailsWithUsername := map[string]interface{}{ - "connection_url": connURL, - "username": username, - "password": initialPassword, - } - _, err = db.Init(testCtx, connDetailsWithUsername, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Create root user - createUser(t, connURL, username, initialPassword) - initialURL := setUserPassOnURL(t, connURL, username, initialPassword) - - // Ensure the initial root user can connect - err = assertConnection(testCtx, initialURL) - if err != nil { - t.Fatalf("%s", err) - } - - // Rotate credentials - newCfg, err := db.RotateRootCredentials(testCtx, nil) - if err != nil { - t.Fatalf("unexpected err rotating root credentials: %s", err) - } - - // Ensure the initial root user can no longer connect - err = assertConnection(testCtx, initialURL) - if err == nil { - t.Fatalf("connection with initial credentials succeeded when it shouldn't have") - } - - // Ensure the new password can connect - newURL := setUserPassOnURL(t, connURL, username, newCfg["password"].(string)) - err = assertConnection(testCtx, newURL) - if err != nil { - t.Fatalf("unexpected error pinging client with new credentials: %s", err) - } -} - -func createUser(t *testing.T, connURL, username, password string) { +func createDBUser(t testing.TB, connURL, db, username, password string) { t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - client, err := createClient(ctx, connURL, nil) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL)) if err != nil { - t.Fatalf("Unable to make initial connection: %s", err) + t.Fatal(err) } - createUserCmd := createUserCommand{ + createUserCmd := &createUserCommand{ Username: username, Password: password, - Roles: []interface{}{ - "userAdminAnyDatabase", - "dbAdminAnyDatabase", - "readWriteAnyDatabase", - }, + Roles: []interface{}{}, + } + result := client.Database(db).RunCommand(ctx, createUserCmd, nil) + if result.Err() != nil { + t.Fatalf("failed to create user in mongodb: %s", result.Err()) } - result := client.Database("admin").RunCommand(ctx, createUserCmd, nil) - err = result.Err() - if err != nil { - t.Fatalf("Unable to create admin user: %s", err) - } + assertCredsExist(t, username, password, connURL) } -func assertConnection(testCtx context.Context, connURL string) error { - // Connect as initial root user and ensure the connection is successful - client, err := createClient(testCtx, connURL, nil) - if err != nil { - return fmt.Errorf("unable to create client connection with initial root user: %w", err) - } - - err = client.Ping(testCtx, nil) - if err != nil { - return fmt.Errorf("failed to ping server with initial root user: %w", err) - } - client.Disconnect(testCtx) - return nil -} - -func setUserPassOnURL(t *testing.T, connURL, username, password string) string { +func assertCredsExist(t testing.TB, username, password, connURL string) { t.Helper() - uri, err := url.Parse(connURL) + + connURL = strings.Replace(connURL, "mongodb://", fmt.Sprintf("mongodb://%s:%s@", username, password), 1) + + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL)) if err != nil { - t.Fatalf("unable to parse connection URL: %s", err) + t.Fatalf("Failed to connect to mongo: %s", err) } - uri.User = url.UserPassword(username, password) - return uri.String() + err = client.Ping(ctx, readpref.Primary()) + if err != nil { + t.Fatalf("Failed to ping mongo with user %q: %s", username, err) + } +} + +func assertCredsDoNotExist(t testing.TB, username, password, connURL string) { + t.Helper() + + connURL = strings.Replace(connURL, "mongodb://", fmt.Sprintf("mongodb://%s:%s@", username, password), 1) + + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL)) + if err != nil { + return // Creds don't exist as expected + } + + err = client.Ping(ctx, readpref.Primary()) + if err != nil { + return // Creds don't exist as expected + } + t.Fatalf("User %q exists and was able to authenticate", username) +} + +func copyConfig(config map[string]interface{}) map[string]interface{} { + newConfig := map[string]interface{}{} + for k, v := range config { + newConfig[k] = v + } + return newConfig }