// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package mongodb import ( "context" "fmt" "io/ioutil" "net/url" "os" paths "path" "path/filepath" "reflect" "sort" "testing" "time" "github.com/hashicorp/vault/helper/testhelpers/certhelpers" dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/ory/dockertest" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" ) func TestInit_clientTLS(t *testing.T) { t.Skip("Skipping this test because CircleCI can't mount the files we need without further investigation: " + "https://support.circleci.com/hc/en-us/articles/360007324514-How-can-I-mount-volumes-to-docker-containers-") // Set up temp directory so we can mount it to the docker container confDir := makeTempDir(t) defer os.RemoveAll(confDir) // Create certificates for Mongo authentication caCert := certhelpers.NewCert(t, certhelpers.CommonName("test certificate authority"), certhelpers.IsCA(true), certhelpers.SelfSign(), ) serverCert := certhelpers.NewCert(t, certhelpers.CommonName("server"), certhelpers.DNS("localhost"), certhelpers.Parent(caCert), ) clientCert := certhelpers.NewCert(t, certhelpers.CommonName("client"), certhelpers.DNS("client"), certhelpers.Parent(caCert), ) writeFile(t, paths.Join(confDir, "ca.pem"), caCert.CombinedPEM(), 0o644) writeFile(t, paths.Join(confDir, "server.pem"), serverCert.CombinedPEM(), 0o644) writeFile(t, paths.Join(confDir, "client.pem"), clientCert.CombinedPEM(), 0o644) // ////////////////////////////////////////////////////// // Set up Mongo config file rawConf := ` net: tls: mode: preferTLS certificateKeyFile: /etc/mongo/server.pem CAFile: /etc/mongo/ca.pem allowInvalidHostnames: true` writeFile(t, paths.Join(confDir, "mongod.conf"), []byte(rawConf), 0o644) // ////////////////////////////////////////////////////// // Start Mongo container retURL, cleanup := startMongoWithTLS(t, "latest", confDir) defer cleanup() // ////////////////////////////////////////////////////// // Set up x509 user mClient := connect(t, retURL) setUpX509User(t, mClient, clientCert) // ////////////////////////////////////////////////////// // Test mongo := new() initReq := dbplugin.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.Initialize(ctx, initReq) if err != nil { t.Fatalf("Unable to initialize mongo engine: %s", err) } // Initialization complete. The connection was established, but we need to ensure // that we're connected as the right user whoamiCmd := map[string]interface{}{ "connectionStatus": 1, } client, err := mongo.Connection(ctx) if err != nil { t.Fatalf("Unable to make connection to Mongo: %s", err) } result := client.Database("test").RunCommand(ctx, whoamiCmd) if result.Err() != nil { t.Fatalf("Unable to connect to Mongo: %s", err) } expected := connStatus{ AuthInfo: authInfo{ AuthenticatedUsers: []user{ { User: fmt.Sprintf("CN=%s", clientCert.Template.Subject.CommonName), DB: "$external", }, }, AuthenticatedUserRoles: []role{ { Role: "readWrite", DB: "test", }, { Role: "userAdminAnyDatabase", DB: "admin", }, }, }, Ok: 1, } // Sort the AuthenticatedUserRoles because Mongo doesn't return them in the same order every time // Thanks Mongo! /tableflip sort.Sort(expected.AuthInfo.AuthenticatedUserRoles) actual := connStatus{} err = result.Decode(&actual) if err != nil { t.Fatalf("Unable to decode connection status: %s", err) } sort.Sort(actual.AuthInfo.AuthenticatedUserRoles) if !reflect.DeepEqual(actual, expected) { t.Fatalf("Actual:%#v\nExpected:\n%#v", actual, expected) } } func makeTempDir(t *testing.T) (confDir string) { confDir, err := ioutil.TempDir(".", "mongodb-test-data") if err != nil { t.Fatalf("Unable to make temp directory: %s", err) } // Convert the directory to an absolute path because docker needs it when mounting confDir, err = filepath.Abs(filepath.Clean(confDir)) if err != nil { t.Fatalf("Unable to determine where temp directory is on absolute path: %s", err) } return confDir } func startMongoWithTLS(t *testing.T, version string, confDir string) (retURL string, cleanup func()) { if os.Getenv("MONGODB_URL") != "" { return os.Getenv("MONGODB_URL"), func() {} } pool, err := dockertest.NewPool("") if err != nil { t.Fatalf("Failed to connect to docker: %s", err) } pool.MaxWait = 30 * time.Second containerName := "mongo-unit-test" // Remove previously running container if it is still running because cleanup failed err = pool.RemoveContainerByName(containerName) if err != nil { t.Fatalf("Unable to remove old running containers: %s", err) } runOpts := &dockertest.RunOptions{ Name: containerName, Repository: "mongo", Tag: version, Cmd: []string{"mongod", "--config", "/etc/mongo/mongod.conf"}, // Mount the directory from local filesystem into the container Mounts: []string{ fmt.Sprintf("%s:/etc/mongo", confDir), }, } resource, err := pool.RunWithOptions(runOpts) if err != nil { t.Fatalf("Could not start local mongo docker container: %s", err) } resource.Expire(30) cleanup = func() { err := pool.Purge(resource) if err != nil { t.Fatalf("Failed to cleanup local container: %s", err) } } uri := url.URL{ Scheme: "mongodb", Host: fmt.Sprintf("localhost:%s", resource.GetPort("27017/tcp")), } retURL = uri.String() // exponential backoff-retry err = pool.Retry(func() error { var err error ctx, _ := context.WithTimeout(context.Background(), 1*time.Minute) client, err := mongo.Connect(ctx, options.Client().ApplyURI(retURL)) if err = client.Disconnect(ctx); err != nil { t.Fatal() } return client.Ping(ctx, readpref.Primary()) }) if err != nil { cleanup() t.Fatalf("Could not connect to mongo docker container: %s", err) } return retURL, cleanup } func connect(t *testing.T, uri string) (client *mongo.Client) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) if err != nil { t.Fatalf("Unable to make connection to Mongo: %s", err) } err = client.Ping(ctx, readpref.Primary()) if err != nil { t.Fatalf("Failed to ping Mongo server: %s", err) } return client } func setUpX509User(t *testing.T, client *mongo.Client, cert certhelpers.Certificate) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() username := fmt.Sprintf("CN=%s", cert.Template.Subject.CommonName) cmd := &createUserCommand{ Username: username, Roles: []interface{}{ mongodbRole{ Role: "readWrite", DB: "test", }, mongodbRole{ Role: "userAdminAnyDatabase", DB: "admin", }, }, } result := client.Database("$external").RunCommand(ctx, cmd) err := result.Err() if err != nil { t.Fatalf("Failed to create x509 user in database: %s", err) } } type connStatus struct { AuthInfo authInfo `bson:"authInfo"` Ok int `bson:"ok"` } type authInfo struct { AuthenticatedUsers []user `bson:"authenticatedUsers"` AuthenticatedUserRoles roles `bson:"authenticatedUserRoles"` } type user struct { User string `bson:"user"` DB string `bson:"db"` } type role struct { Role string `bson:"role"` DB string `bson:"db"` } type roles []role func (r roles) Len() int { return len(r) } func (r roles) Less(i, j int) bool { return r[i].Role < r[j].Role } func (r roles) Swap(i, j int) { r[i], r[j] = r[j], r[i] } // //////////////////////////////////////////////////////////////////////////// // Writing to file // //////////////////////////////////////////////////////////////////////////// func writeFile(t *testing.T, filename string, data []byte, perms os.FileMode) { t.Helper() err := ioutil.WriteFile(filename, data, perms) if err != nil { t.Fatalf("Unable to write to file [%s]: %s", filename, err) } }