Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
This new helper command allows users to authorize sessions against MySQL
targets and automatically invoke a MySQL client with the appropriate
connection parameters and credentials.
* cli: Added `boundary connect mongo` command for connecting to MongoDB targets.
This new helper command allows users to authorize sessions against MongoDB
targets and automatically invoke a MongoDB client with the appropriate
connection parameters and credentials.
* Adds support to parse User-Agent headers and emit them in telemetry events
([PR](https://github.com/hashicorp/boundary/pull/5645)).
* cli: Added `boundary connect cassandra` command for connecting to Cassandra targets.
Expand Down
10 changes: 9 additions & 1 deletion enos/modules/test_e2e_docker/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ apt update
# default-mysql-client is used for mysql tests
# wget is used for downloading external dependencies and repository keys
# apt-transport-https enables HTTPS transport for APT repositories
apt install unzip pass lsb-release postgresql-client default-mysql-client wget apt-transport-https -y
# curl and ca-certificates are required for some repository setups (e.g., MongoDB).
apt install unzip pass lsb-release postgresql-client default-mysql-client wget apt-transport-https curl ca-certificates -y

# Function to install Cassandra
install_cassandra() {
Expand Down Expand Up @@ -116,6 +117,13 @@ echo \
apt update
apt install docker-ce-cli -y

# Install MongoDB client (mongosh)
# Reference: https://www.mongodb.com/docs/mongodb-shell/install/#debian
curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/7.0 main" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list
apt update
apt install -y mongodb-mongosh

# Run Tests
unzip /boundary.zip -d /usr/local/bin/
cd /src/boundary
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Func: "mysql",
}
}),
"connect mongo": wrapper.Wrap(func() wrapper.WrappableCommand {
return &connect.Command{
Command: base.NewCommand(ui, opts...),
Func: "mongo",
}
}),
"connect cassandra": wrapper.Wrap(func() wrapper.WrappableCommand {
return &connect.Command{
Command: base.NewCommand(ui, opts...),
Expand Down
21 changes: 21 additions & 0 deletions internal/cmd/commands/connect/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Command struct {
flagExec string
flagUsername string
flagDbname string
flagAuthSource string

// HTTP
httpFlags
Expand All @@ -80,6 +81,9 @@ type Command struct {
// MySQL
mysqlFlags

// MongoDB
mongoFlags

// Cassandra
cassandraFlags

Expand Down Expand Up @@ -111,6 +115,8 @@ func (c *Command) Synopsis() string {
return postgresSynopsis
case "mysql":
return mysqlSynopsis
case "mongo":
return mongoSynopsis
case "cassandra":
return cassandraSynopsis
case "rdp":
Expand Down Expand Up @@ -235,6 +241,9 @@ func (c *Command) Flags() *base.FlagSets {
case "mysql":
mysqlOptions(c, set)

case "mongo":
mongoOptions(c, set)

case "cassandra":
cassandraOptions(c, set)

Expand Down Expand Up @@ -327,6 +336,8 @@ func (c *Command) Run(args []string) (retCode int) {
c.flagExec = c.postgresFlags.defaultExec()
case "mysql":
c.flagExec = c.mysqlFlags.defaultExec()
case "mongo":
c.flagExec = c.mongoFlags.defaultExec()
case "cassandra":
c.flagExec = c.cassandraFlags.defaultExec()
case "rdp":
Expand Down Expand Up @@ -683,6 +694,16 @@ func (c *Command) handleExec(clientProxy *apiproxy.ClientProxy, passthroughArgs
envs = append(envs, mysqlEnvs...)
creds = mysqlCreds

case "mongo":
mongoArgs, mongoEnvs, mongoCreds, mongoErr := c.mongoFlags.buildArgs(c, port, host, addr, creds)
if mongoErr != nil {
argsErr = mongoErr
break
}
args = append(args, mongoArgs...)
envs = append(envs, mongoEnvs...)
creds = mongoCreds

case "cassandra":
cassandraArgs, cassandraEnvs, cassandraCreds, cassandraErr := c.cassandraFlags.buildArgs(c, port, host, addr, creds)
if cassandraErr != nil {
Expand Down
120 changes: 120 additions & 0 deletions internal/cmd/commands/connect/mongo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package connect

import (
"fmt"
"strings"

"github.com/hashicorp/boundary/api/proxy"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/posener/complete"
)

const (
mongoSynopsis = "Authorize a session against a target and invoke a MongoDB client to connect"
)

func mongoOptions(c *Command, set *base.FlagSets) {
f := set.NewFlagSet("MongoDB Options")

f.StringVar(&base.StringVar{
Name: "style",
Target: &c.flagMongoStyle,
EnvVar: "BOUNDARY_CONNECT_MONGO_STYLE",
Completion: complete.PredictSet("mongosh"),
Default: "mongosh",
Usage: `Specifies how the CLI will attempt to invoke a MongoDB client. Currently only "mongosh" is supported.`,
})

f.StringVar(&base.StringVar{
Name: "username",
Target: &c.flagUsername,
EnvVar: "BOUNDARY_CONNECT_USERNAME",
Completion: complete.PredictNothing,
Usage: `Specifies the username to pass through to the client. May be overridden by credentials sourced from a credential store.`,
})

f.StringVar(&base.StringVar{
Name: "dbname",
Target: &c.flagDbname,
EnvVar: "BOUNDARY_CONNECT_DBNAME",
Completion: complete.PredictNothing,
Usage: `Specifies the database name to pass through to the client.`,
})

f.StringVar(&base.StringVar{
Name: "auth-source",
Target: &c.flagAuthSource,
EnvVar: "BOUNDARY_CONNECT_MONGO_AUTH_SOURCE",
Completion: complete.PredictNothing,
Default: "admin",
Usage: `Specifies the authentication database for MongoDB. Defaults to "admin" for root-style users.`,
})
}

type mongoFlags struct {
flagMongoStyle string
}

func (m *mongoFlags) defaultExec() string {
return strings.ToLower(m.flagMongoStyle)
}

func (m *mongoFlags) buildArgs(c *Command, port, ip, _ string, creds proxy.Credentials) (args, envs []string, retCreds proxy.Credentials, retErr error) {
var username, password string

retCreds = creds
if len(retCreds.UsernamePassword) > 0 {
// Mark credential as consumed so it is not printed to user
retCreds.UsernamePassword[0].Consumed = true

// For now just grab the first username password credential brokered
username = retCreds.UsernamePassword[0].Username
password = retCreds.UsernamePassword[0].Password
}

switch m.flagMongoStyle {
case "mongosh":
// Build MongoDB connection string for mongosh
connectionString := "mongodb://"

// Add username and password to connection string
if username != "" {
connectionString += username
if password != "" {
connectionString += ":" + password
}
connectionString += "@"
} else if c.flagUsername != "" {
connectionString += c.flagUsername
if password != "" {
connectionString += ":" + password
}
connectionString += "@"
}

// Add host and port
connectionString += ip
if port != "" {
connectionString += ":" + port
}

// Add database name
if c.flagDbname != "" {
connectionString += "/" + c.flagDbname
}

// Add authSource parameter if not already present
authSource := c.flagAuthSource

if !strings.Contains(connectionString, "?") {
connectionString += "?authSource=" + authSource
} else if !strings.Contains(strings.ToLower(connectionString), "authsource=") {
connectionString += "&authSource=" + authSource
}

args = append(args, connectionString)
default:
return nil, nil, proxy.Credentials{}, fmt.Errorf("unsupported MongoDB style: %s", m.flagMongoStyle)
}
return
}
51 changes: 51 additions & 0 deletions testing/internal/e2e/infra/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,57 @@ func StartMysql(t testing.TB, pool *dockertest.Pool, network *dockertest.Network
}
}

// StartMongo starts a MongoDB database in a docker container.
// Returns information about the container
func StartMongo(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container {
t.Log("Starting MongoDB database...")
c, err := LoadConfig()
require.NoError(t, err)

err = pool.Client.PullImage(docker.PullImageOptions{
Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository),
Tag: tag,
}, docker.AuthConfiguration{})
require.NoError(t, err)

networkAlias := "e2emongo"
mongoDb := "e2eboundarydb"
mongoUser := "e2eboundary"
mongoPassword := "e2eboundary"

resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository),
Tag: tag,
Env: []string{
"MONGO_INITDB_ROOT_USERNAME=" + mongoUser,
"MONGO_INITDB_ROOT_PASSWORD=" + mongoPassword,
"MONGO_INITDB_DATABASE=" + mongoDb,
},
ExposedPorts: []string{"27017/tcp"},
Name: networkAlias,
Networks: []*dockertest.Network{network},
})
require.NoError(t, err)

return &Container{
Resource: resource,
UriLocalhost: fmt.Sprintf(
"mongodb://%s:%s@%s/%s?authSource=admin",
mongoUser,
mongoPassword,
resource.GetHostPort("27017/tcp"),
mongoDb,
),
UriNetwork: fmt.Sprintf(
"mongodb://%s:%s@%s:27017/%s?authSource=admin",
mongoUser,
mongoPassword,
networkAlias,
mongoDb,
),
}
}

// StartCassandra starts a Cassandra database in a docker container.
// Returns information about the container
func StartCassandra(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container {
Expand Down
Loading
Loading