vault/plugins/database/mongodb/connection_producer_test.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

315 lines
8.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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)
}
}