open-vault/plugins/database/mongodb/mongodb_test.go
Tom Proctor a52fd805dd
Pin MongoDB test container images pre-v6 (#16880)
v6 was released in the last 24h, and our tests fail to connect to the db when v6 is used.
Using v6 needs investigating, but for now I'm pinning to the last known good version.
2022-08-25 08:14:37 -07:00

470 lines
12 KiB
Go

package mongodb
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"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, "5.0.10")
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, "5.0.10")
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, "5.0.10")
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, "5.0.10")
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, "5.0.10")
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, "5.0.10")
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{
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
}