mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-11-04 04:28:08 +00:00 
			
		
		
		
	* upgrade mongo driver to 1.11 * add changelog * fix failing test comparison * ignore http.Transport
		
			
				
	
	
		
			475 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			475 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) HashiCorp, Inc.
 | 
						|
// SPDX-License-Identifier: MPL-2.0
 | 
						|
 | 
						|
package mongodb
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"crypto/x509"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"reflect"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/google/go-cmp/cmp"
 | 
						|
	"github.com/google/go-cmp/cmp/cmpopts"
 | 
						|
	"github.com/hashicorp/vault/helper/testhelpers/certhelpers"
 | 
						|
	"github.com/hashicorp/vault/helper/testhelpers/mongodb"
 | 
						|
	dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
 | 
						|
	dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
 | 
						|
	"github.com/stretchr/testify/require"
 | 
						|
	"go.mongodb.org/mongo-driver/mongo"
 | 
						|
	"go.mongodb.org/mongo-driver/mongo/options"
 | 
						|
	"go.mongodb.org/mongo-driver/mongo/readpref"
 | 
						|
)
 | 
						|
 | 
						|
const mongoAdminRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }`
 | 
						|
 | 
						|
func TestMongoDB_Initialize(t *testing.T) {
 | 
						|
	cleanup, connURL := mongodb.PrepareTestContainer(t, "latest")
 | 
						|
	defer cleanup()
 | 
						|
 | 
						|
	db := new()
 | 
						|
	defer dbtesting.AssertClose(t, db)
 | 
						|
 | 
						|
	config := map[string]interface{}{
 | 
						|
		"connection_url": connURL,
 | 
						|
	}
 | 
						|
 | 
						|
	// Make a copy since the original map could be modified by the Initialize call
 | 
						|
	expectedConfig := copyConfig(config)
 | 
						|
 | 
						|
	req := dbplugin.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")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestNewUser_usernameTemplate(t *testing.T) {
 | 
						|
	type testCase struct {
 | 
						|
		usernameTemplate string
 | 
						|
 | 
						|
		newUserReq            dbplugin.NewUserRequest
 | 
						|
		expectedUsernameRegex string
 | 
						|
	}
 | 
						|
 | 
						|
	tests := map[string]testCase{
 | 
						|
		"default username template": {
 | 
						|
			usernameTemplate: "",
 | 
						|
 | 
						|
			newUserReq: dbplugin.NewUserRequest{
 | 
						|
				UsernameConfig: dbplugin.UsernameMetadata{
 | 
						|
					DisplayName: "token",
 | 
						|
					RoleName:    "testrolenamewithmanycharacters",
 | 
						|
				},
 | 
						|
				Statements: dbplugin.Statements{
 | 
						|
					Commands: []string{mongoAdminRole},
 | 
						|
				},
 | 
						|
				Password:   "98yq3thgnakjsfhjkl",
 | 
						|
				Expiration: time.Now().Add(time.Minute),
 | 
						|
			},
 | 
						|
 | 
						|
			expectedUsernameRegex: "^v-token-testrolenamewit-[a-zA-Z0-9]{20}-[0-9]{10}$",
 | 
						|
		},
 | 
						|
		"default username template with invalid chars": {
 | 
						|
			usernameTemplate: "",
 | 
						|
 | 
						|
			newUserReq: dbplugin.NewUserRequest{
 | 
						|
				UsernameConfig: dbplugin.UsernameMetadata{
 | 
						|
					DisplayName: "a.bad.account",
 | 
						|
					RoleName:    "a.bad.role",
 | 
						|
				},
 | 
						|
				Statements: dbplugin.Statements{
 | 
						|
					Commands: []string{mongoAdminRole},
 | 
						|
				},
 | 
						|
				Password:   "98yq3thgnakjsfhjkl",
 | 
						|
				Expiration: time.Now().Add(time.Minute),
 | 
						|
			},
 | 
						|
 | 
						|
			expectedUsernameRegex: "^v-a-bad-account-a-bad-role-[a-zA-Z0-9]{20}-[0-9]{10}$",
 | 
						|
		},
 | 
						|
		"custom username template": {
 | 
						|
			usernameTemplate: "{{random 2 | uppercase}}_{{unix_time}}_{{.RoleName | uppercase}}_{{.DisplayName | uppercase}}",
 | 
						|
 | 
						|
			newUserReq: dbplugin.NewUserRequest{
 | 
						|
				UsernameConfig: dbplugin.UsernameMetadata{
 | 
						|
					DisplayName: "token",
 | 
						|
					RoleName:    "testrolenamewithmanycharacters",
 | 
						|
				},
 | 
						|
				Statements: dbplugin.Statements{
 | 
						|
					Commands: []string{mongoAdminRole},
 | 
						|
				},
 | 
						|
				Password:   "98yq3thgnakjsfhjkl",
 | 
						|
				Expiration: time.Now().Add(time.Minute),
 | 
						|
			},
 | 
						|
 | 
						|
			expectedUsernameRegex: "^[A-Z0-9]{2}_[0-9]{10}_TESTROLENAMEWITHMANYCHARACTERS_TOKEN$",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for name, test := range tests {
 | 
						|
		t.Run(name, func(t *testing.T) {
 | 
						|
			cleanup, connURL := mongodb.PrepareTestContainer(t, "latest")
 | 
						|
			defer cleanup()
 | 
						|
 | 
						|
			db := new()
 | 
						|
			defer dbtesting.AssertClose(t, db)
 | 
						|
 | 
						|
			initReq := dbplugin.InitializeRequest{
 | 
						|
				Config: map[string]interface{}{
 | 
						|
					"connection_url":    connURL,
 | 
						|
					"username_template": test.usernameTemplate,
 | 
						|
				},
 | 
						|
				VerifyConnection: true,
 | 
						|
			}
 | 
						|
			dbtesting.AssertInitialize(t, db, initReq)
 | 
						|
 | 
						|
			ctx := context.Background()
 | 
						|
			newUserResp, err := db.NewUser(ctx, test.newUserReq)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Regexp(t, test.expectedUsernameRegex, newUserResp.Username)
 | 
						|
 | 
						|
			assertCredsExist(t, newUserResp.Username, test.newUserReq.Password, connURL)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestMongoDB_CreateUser(t *testing.T) {
 | 
						|
	cleanup, connURL := mongodb.PrepareTestContainer(t, "latest")
 | 
						|
	defer cleanup()
 | 
						|
 | 
						|
	db := new()
 | 
						|
	defer dbtesting.AssertClose(t, db)
 | 
						|
 | 
						|
	initReq := dbplugin.InitializeRequest{
 | 
						|
		Config: map[string]interface{}{
 | 
						|
			"connection_url": connURL,
 | 
						|
		},
 | 
						|
		VerifyConnection: true,
 | 
						|
	}
 | 
						|
	dbtesting.AssertInitialize(t, db, initReq)
 | 
						|
 | 
						|
	password := "myreallysecurepassword"
 | 
						|
	createReq := dbplugin.NewUserRequest{
 | 
						|
		UsernameConfig: dbplugin.UsernameMetadata{
 | 
						|
			DisplayName: "test",
 | 
						|
			RoleName:    "test",
 | 
						|
		},
 | 
						|
		Statements: dbplugin.Statements{
 | 
						|
			Commands: []string{mongoAdminRole},
 | 
						|
		},
 | 
						|
		Password:   password,
 | 
						|
		Expiration: time.Now().Add(time.Minute),
 | 
						|
	}
 | 
						|
	createResp := dbtesting.AssertNewUser(t, db, createReq)
 | 
						|
 | 
						|
	assertCredsExist(t, createResp.Username, password, connURL)
 | 
						|
}
 | 
						|
 | 
						|
func TestMongoDB_CreateUser_writeConcern(t *testing.T) {
 | 
						|
	cleanup, connURL := mongodb.PrepareTestContainer(t, "latest")
 | 
						|
	defer cleanup()
 | 
						|
 | 
						|
	initReq := dbplugin.InitializeRequest{
 | 
						|
		Config: map[string]interface{}{
 | 
						|
			"connection_url": connURL,
 | 
						|
			"write_concern":  `{ "wmode": "majority", "wtimeout": 5000 }`,
 | 
						|
		},
 | 
						|
		VerifyConnection: true,
 | 
						|
	}
 | 
						|
 | 
						|
	db := new()
 | 
						|
	defer dbtesting.AssertClose(t, db)
 | 
						|
 | 
						|
	dbtesting.AssertInitialize(t, db, initReq)
 | 
						|
 | 
						|
	password := "myreallysecurepassword"
 | 
						|
	createReq := dbplugin.NewUserRequest{
 | 
						|
		UsernameConfig: dbplugin.UsernameMetadata{
 | 
						|
			DisplayName: "test",
 | 
						|
			RoleName:    "test",
 | 
						|
		},
 | 
						|
		Statements: dbplugin.Statements{
 | 
						|
			Commands: []string{mongoAdminRole},
 | 
						|
		},
 | 
						|
		Password:   password,
 | 
						|
		Expiration: time.Now().Add(time.Minute),
 | 
						|
	}
 | 
						|
	createResp := dbtesting.AssertNewUser(t, db, createReq)
 | 
						|
 | 
						|
	assertCredsExist(t, createResp.Username, password, connURL)
 | 
						|
}
 | 
						|
 | 
						|
func TestMongoDB_DeleteUser(t *testing.T) {
 | 
						|
	cleanup, connURL := mongodb.PrepareTestContainer(t, "latest")
 | 
						|
	defer cleanup()
 | 
						|
 | 
						|
	db := new()
 | 
						|
	defer dbtesting.AssertClose(t, db)
 | 
						|
 | 
						|
	initReq := dbplugin.InitializeRequest{
 | 
						|
		Config: map[string]interface{}{
 | 
						|
			"connection_url": connURL,
 | 
						|
		},
 | 
						|
		VerifyConnection: true,
 | 
						|
	}
 | 
						|
	dbtesting.AssertInitialize(t, db, initReq)
 | 
						|
 | 
						|
	password := "myreallysecurepassword"
 | 
						|
	createReq := dbplugin.NewUserRequest{
 | 
						|
		UsernameConfig: dbplugin.UsernameMetadata{
 | 
						|
			DisplayName: "test",
 | 
						|
			RoleName:    "test",
 | 
						|
		},
 | 
						|
		Statements: dbplugin.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
 | 
						|
	delReq := dbplugin.DeleteUserRequest{
 | 
						|
		Username: createResp.Username,
 | 
						|
	}
 | 
						|
 | 
						|
	dbtesting.AssertDeleteUser(t, db, delReq)
 | 
						|
 | 
						|
	assertCredsDoNotExist(t, createResp.Username, password, connURL)
 | 
						|
}
 | 
						|
 | 
						|
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"
 | 
						|
	db := new()
 | 
						|
	defer dbtesting.AssertClose(t, db)
 | 
						|
 | 
						|
	initReq := dbplugin.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"
 | 
						|
	createDBUser(t, connURL, "test", dbUser, startingPassword)
 | 
						|
 | 
						|
	newPassword := "myreallysecurecredentials"
 | 
						|
 | 
						|
	updateReq := dbplugin.UpdateUserRequest{
 | 
						|
		Username: dbUser,
 | 
						|
		Password: &dbplugin.ChangePassword{
 | 
						|
			NewPassword: newPassword,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	dbtesting.AssertUpdateUser(t, db, updateReq)
 | 
						|
 | 
						|
	assertCredsExist(t, dbUser, newPassword, connURL)
 | 
						|
}
 | 
						|
 | 
						|
func TestGetTLSAuth(t *testing.T) {
 | 
						|
	ca := certhelpers.NewCert(t,
 | 
						|
		certhelpers.CommonName("certificate authority"),
 | 
						|
		certhelpers.IsCA(true),
 | 
						|
		certhelpers.SelfSign(),
 | 
						|
	)
 | 
						|
	cert := certhelpers.NewCert(t,
 | 
						|
		certhelpers.CommonName("test cert"),
 | 
						|
		certhelpers.Parent(ca),
 | 
						|
	)
 | 
						|
 | 
						|
	type testCase struct {
 | 
						|
		username   string
 | 
						|
		tlsCAData  []byte
 | 
						|
		tlsKeyData []byte
 | 
						|
 | 
						|
		expectOpts *options.ClientOptions
 | 
						|
		expectErr  bool
 | 
						|
	}
 | 
						|
 | 
						|
	tests := map[string]testCase{
 | 
						|
		"no TLS data set": {
 | 
						|
			expectOpts: nil,
 | 
						|
			expectErr:  false,
 | 
						|
		},
 | 
						|
		"bad CA": {
 | 
						|
			tlsCAData: []byte("foobar"),
 | 
						|
 | 
						|
			expectOpts: nil,
 | 
						|
			expectErr:  true,
 | 
						|
		},
 | 
						|
		"bad key": {
 | 
						|
			tlsKeyData: []byte("foobar"),
 | 
						|
 | 
						|
			expectOpts: nil,
 | 
						|
			expectErr:  true,
 | 
						|
		},
 | 
						|
		"good ca": {
 | 
						|
			tlsCAData: cert.Pem,
 | 
						|
 | 
						|
			expectOpts: options.Client().
 | 
						|
				SetTLSConfig(
 | 
						|
					&tls.Config{
 | 
						|
						RootCAs: appendToCertPool(t, x509.NewCertPool(), cert.Pem),
 | 
						|
					},
 | 
						|
				),
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		"good key": {
 | 
						|
			username:   "unittest",
 | 
						|
			tlsKeyData: cert.CombinedPEM(),
 | 
						|
 | 
						|
			expectOpts: options.Client().
 | 
						|
				SetTLSConfig(
 | 
						|
					&tls.Config{
 | 
						|
						Certificates: []tls.Certificate{cert.TLSCert},
 | 
						|
					},
 | 
						|
				).
 | 
						|
				SetAuth(options.Credential{
 | 
						|
					AuthMechanism: "MONGODB-X509",
 | 
						|
					Username:      "unittest",
 | 
						|
				}),
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for name, test := range tests {
 | 
						|
		t.Run(name, func(t *testing.T) {
 | 
						|
			c := new()
 | 
						|
			c.Username = test.username
 | 
						|
			c.TLSCAData = test.tlsCAData
 | 
						|
			c.TLSCertificateKeyData = test.tlsKeyData
 | 
						|
 | 
						|
			actual, err := c.getTLSAuth()
 | 
						|
			if test.expectErr && err == nil {
 | 
						|
				t.Fatalf("err expected, got nil")
 | 
						|
			}
 | 
						|
			if !test.expectErr && err != nil {
 | 
						|
				t.Fatalf("no error expected, got: %s", err)
 | 
						|
			}
 | 
						|
			assertDeepEqual(t, test.expectOpts, actual)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func appendToCertPool(t *testing.T, pool *x509.CertPool, caPem []byte) *x509.CertPool {
 | 
						|
	t.Helper()
 | 
						|
 | 
						|
	ok := pool.AppendCertsFromPEM(caPem)
 | 
						|
	if !ok {
 | 
						|
		t.Fatalf("Unable to append cert to cert pool")
 | 
						|
	}
 | 
						|
	return pool
 | 
						|
}
 | 
						|
 | 
						|
var cmpClientOptionsOpts = cmp.Options{
 | 
						|
	cmpopts.IgnoreTypes(http.Transport{}),
 | 
						|
 | 
						|
	cmp.AllowUnexported(options.ClientOptions{}),
 | 
						|
 | 
						|
	cmp.AllowUnexported(tls.Config{}),
 | 
						|
	cmpopts.IgnoreTypes(sync.Mutex{}, sync.RWMutex{}),
 | 
						|
 | 
						|
	// 'lazyCerts' has a func field which can't be compared.
 | 
						|
	cmpopts.IgnoreFields(x509.CertPool{}, "lazyCerts"),
 | 
						|
	cmp.AllowUnexported(x509.CertPool{}),
 | 
						|
}
 | 
						|
 | 
						|
// Need a special comparison for ClientOptions because reflect.DeepEquals won't work in Go 1.16.
 | 
						|
// See: https://github.com/golang/go/issues/45891
 | 
						|
func assertDeepEqual(t *testing.T, a, b *options.ClientOptions) {
 | 
						|
	t.Helper()
 | 
						|
 | 
						|
	if diff := cmp.Diff(a, b, cmpClientOptionsOpts); diff != "" {
 | 
						|
		t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func createDBUser(t testing.TB, connURL, db, username, password string) {
 | 
						|
	t.Helper()
 | 
						|
 | 
						|
	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.Fatalf("failed to create user in mongodb: %s", result.Err())
 | 
						|
	}
 | 
						|
 | 
						|
	assertCredsExist(t, username, password, connURL)
 | 
						|
}
 | 
						|
 | 
						|
func assertCredsExist(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 {
 | 
						|
		t.Fatalf("Failed to connect to mongo: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
}
 |