feat(mongo): Add MongoDB connection support for Boundary CLI PORT (#6069)

* feat(mongo): add mongo for boundary cli

* Fix: remove changes added by mistake during testing

* feat(enos): install MongoDB client (mongosh) in e2e Docker test runtime

* Remove explicit Boundary DB/server startup from target_tcp_connect_mongo_test.go

* Add a full stop and trigger CI

* feat: add mongosh support to boundary connect mongo

* feat: add mongosh support to boundary connect mongo

* feat: add auth-source flag for MongoDB connect command

- Add -auth-source flag with default value 'admin' for root users
- Add BOUNDARY_CONNECT_MONGO_AUTH_SOURCE environment variable support
- Update documentation with new flag and correct mongosh default
- Improve MongoDB test with explicit auth-source parameter
- Fix Docker configuration formatting for better readability

This ensures MongoDB root users authenticate against the admin database
by default while allowing flexibility for custom authentication databases.

* test(e2e): Fixed false positive in Mongo connect test; use UriNetwork and container hostname

* make gen

* fix: Removed unnecessary variable containerName from the MongoDB E2E test

* feat(connect/mongo): Build URI compatible with mongosh and set default authSource

* feat(connect/mongo): use mongosh flags following PostgreSQL pattern

* remove 'connection string' from auth-source usage text

* Update internal/cmd/commands/connect/mongo.go

Co-authored-by: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com>

* remove 'connection string' from auth-source usage text

* refacotr: replace authSource flag with authenticationDatabase

---------

Co-authored-by: Enbiya <100806254+enbiyagoral@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
pull/6136/head
Bharath Gajjala 6 months ago committed by GitHub
parent b006c47887
commit 4078838f56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,6 +15,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.

@ -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() {
@ -118,6 +119,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

@ -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...),

@ -59,15 +59,16 @@ var (
type Command struct {
*base.Command
flagAuthzToken string
flagListenAddr string
flagListenPort int64
flagTargetId string
flagTargetName string
flagHostId string
flagExec string
flagUsername string
flagDbname string
flagAuthzToken string
flagListenAddr string
flagListenPort int64
flagTargetId string
flagTargetName string
flagHostId string
flagExec string
flagUsername string
flagDbname string
flagMongoDbAuthenticationDatabase string
// HTTP
httpFlags
@ -81,6 +82,9 @@ type Command struct {
// MySQL
mysqlFlags
// MongoDB
mongoFlags
// Cassandra
cassandraFlags
@ -115,6 +119,8 @@ func (c *Command) Synopsis() string {
return postgresSynopsis
case "mysql":
return mysqlSynopsis
case "mongo":
return mongoSynopsis
case "cassandra":
return cassandraSynopsis
case "redis":
@ -241,6 +247,9 @@ func (c *Command) Flags() *base.FlagSets {
case "mysql":
mysqlOptions(c, set)
case "mongo":
mongoOptions(c, set)
case "cassandra":
cassandraOptions(c, set)
@ -336,6 +345,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 "redis":
@ -714,6 +725,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 {

@ -0,0 +1,110 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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: "authentication-database",
Target: &c.flagMongoDbAuthenticationDatabase,
EnvVar: "BOUNDARY_CONNECT_MONGO_AUTHENTICATION_DATABASE",
Completion: complete.PredictNothing,
Default: "",
Usage: `Specifies the authentication database for MongoDB. If omitted, mongosh defaults authSource to the database name (dbname); if none is specified, it defaults to "admin".`,
})
}
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":
if port != "" {
args = append(args, "--port", port)
}
args = append(args, "--host", ip)
if c.flagDbname != "" {
args = append(args, c.flagDbname)
}
switch {
case username != "":
args = append(args, "-u", username)
case c.flagUsername != "":
args = append(args, "-u", c.flagUsername)
}
if password != "" {
args = append(args, "-p", password)
if c.flagDbname == "" {
c.UI.Warn("Credentials are being brokered but no -dbname parameter provided. mongosh will default the database to 'test'.")
}
}
if c.flagMongoDbAuthenticationDatabase != "" {
args = append(args, "--authenticationDatabase", c.flagMongoDbAuthenticationDatabase)
}
default:
return nil, nil, proxy.Credentials{}, fmt.Errorf("unsupported MongoDB style: %s", m.flagMongoStyle)
}
return args, envs, retCreds, retErr
}

@ -360,6 +360,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 {

@ -0,0 +1,134 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package base_test
import (
"bytes"
"context"
"io"
"net/url"
"os/exec"
"testing"
"github.com/creack/pty"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/testing/internal/e2e"
"github.com/hashicorp/boundary/testing/internal/e2e/boundary"
"github.com/hashicorp/boundary/testing/internal/e2e/infra"
"github.com/ory/dockertest/v3"
"github.com/stretchr/testify/require"
)
// TestCliTcpTargetConnectMongo uses the boundary cli to connect to a
// target using `connect mongo`
func TestCliTcpTargetConnectMongo(t *testing.T) {
e2e.MaybeSkipTest(t)
ctx := context.Background()
pool, err := dockertest.NewPool("")
require.NoError(t, err)
// e2e_cluster network is created by the e2e infra setup
network, err := pool.NetworksByName("e2e_cluster")
require.NoError(t, err, "Failed to get e2e_cluster network")
c := infra.StartMongo(t, pool, &network[0], "mongo", "7.0")
require.NotNil(t, c, "MongoDB container should not be nil")
t.Cleanup(func() {
if err := pool.Purge(c.Resource); err != nil {
t.Logf("Failed to purge MongoDB container: %v", err)
}
})
u, err := url.Parse(c.UriNetwork)
require.NoError(t, err, "Failed to parse MongoDB URL")
user, hostname, port, db := u.User.Username(), u.Hostname(), u.Port(), u.Path[1:]
pw, pwSet := u.User.Password()
t.Logf("MongoDB info: user=%s, db=%s, host=%s, port=%s, password-set:%t",
user, db, hostname, port, pwSet)
err = pool.Retry(func() error {
cmd := exec.CommandContext(ctx, "docker", "exec", hostname,
"mongosh", "-u", user, "-p", pw, "--authenticationDatabase", "admin",
"--eval", "db.runCommand('ping')")
return cmd.Run()
})
require.NoError(t, err, "MongoDB container failed to start")
boundary.AuthenticateAdminCli(t, ctx)
orgId, err := boundary.CreateOrgCli(t, ctx)
require.NoError(t, err)
t.Cleanup(func() {
ctx := context.Background()
boundary.AuthenticateAdminCli(t, ctx)
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId))
require.NoError(t, output.Err, string(output.Stderr))
})
projectId, err := boundary.CreateProjectCli(t, ctx, orgId)
require.NoError(t, err)
targetId, err := boundary.CreateTargetCli(
t,
ctx,
projectId,
port,
target.WithAddress(hostname),
)
require.NoError(t, err)
storeId, err := boundary.CreateCredentialStoreStaticCli(t, ctx, projectId)
require.NoError(t, err)
credentialId, err := boundary.CreateStaticCredentialPasswordCli(
t,
ctx,
storeId,
user,
pw,
)
require.NoError(t, err)
err = boundary.AddBrokeredCredentialSourceToTargetCli(t, ctx, targetId, credentialId)
require.NoError(t, err)
cmd := exec.CommandContext(ctx,
"boundary",
"connect", "mongo",
"-target-id", targetId,
"-dbname", db,
"-authentication-database", "admin",
)
f, err := pty.Start(cmd)
require.NoError(t, err)
t.Cleanup(func() {
err := f.Close()
require.NoError(t, err)
})
_, err = f.Write([]byte("db.runCommand('ping')\n"))
require.NoError(t, err)
_, err = f.Write([]byte("db.getName()\n"))
require.NoError(t, err)
_, err = f.Write([]byte("exit\n"))
require.NoError(t, err)
_, err = f.Write([]byte{4})
require.NoError(t, err)
var buf bytes.Buffer
_, _ = io.Copy(&buf, f)
output := buf.String()
t.Logf("MongoDB session output: %s", output)
require.Contains(t, output, `ok:`, "MongoDB ping command should succeed")
require.Contains(t, output, `1`, "MongoDB ping should return ok: 1")
require.Contains(t, output, db, "Database name should appear in the session")
require.NotContains(t, output, "MongoServerSelectionError", "Should not have connection errors")
t.Log("Successfully connected to MongoDB target")
}

@ -43,6 +43,7 @@ Subcommands:
kube Authorize a session against a target and invoke a Kubernetes client to connect
cassandra Authorize a session against a target and invoke a Cassandra client to connect
mysql Authorize a session against a target and invoke a MySQL client to connect
mongo Authorize a session against a target and invoke a MongoDB client to connect
postgres Authorize a session against a target and invoke a Postgres client to connect
rdp Authorize a session against a target and invoke an RDP client to connect
redis Authorize a session against a target and invoke a redis client to connect
@ -58,6 +59,7 @@ of the subcommand in the sidebar or one of the links below:
- [kube](/boundary/docs/commands/connect/kube)
- [cassandra](/boundary/docs/commands/connect/cassandra)
- [mysql](/boundary/docs/commands/connect/mysql)
- [mongo](/boundary/docs/commands/connect/mongo)
- [postgres](/boundary/docs/commands/connect/postgres)
- [rdp](/boundary/docs/commands/connect/rdp)
- [redis] (/boundary/docs/commands/connect/redis)

@ -0,0 +1,85 @@
---
layout: docs
page_title: connect mongo - Command
description: >-
The "connect mongo" command performs a target authorization or consumes an existing authorization token, and then launches a proxied MongoDB connection.
---
# connect mongo
Command: `boundary connect mongo`
The `connect mongo` command authorizes a session against a target and invokes a MongoDB client for the connection.
The command fills in the local address and port.
@include 'cmd-connect-env-vars.mdx'
## Examples
The following example shows how to connect to a target with the ID `ttcp_eTcMueUYv` using a MongoDB helper:
```shell-session
$ boundary connect mongo -target-id=ttcp_eTcZMueUYv \
-dbname=northwind \
-username=superuser \
-authentication-database=admin \
-format=table
```
When prompted, you must enter the password for the user, "superuser":
<CodeBlockConfig hideClipboard>
```plaintext
Enter password:
Current Mongosh Log ID: 64a1b2c3d4e5f6789012345
Connecting to: mongodb://127.0.0.1:12345/northwind?authSource=admin
Using MongoDB: 7.0.0
Using Mongosh: 2.0.0
northwind> show collections
orders
products
customers
northwind> db.products.count()
77
northwind>
```
</CodeBlockConfig>
## Usage
<CodeBlockConfig hideClipboard>
```shell-session
$ boundary connect mongo [options] [args]
```
</CodeBlockConfig>
@include 'cmd-connect-command-options.mdx'
### MongoDB options:
- `-dbname` `(string: "")` - The database name you want to pass through to the client.
You can also specify the database name using the **BOUNDARY_CONNECT_DBNAME** environment variable.
- `-style` `(string: "mongosh")` - How the CLI attempts to invoke a MongoDB client.
This value also sets a suitable default for `-exec`, if you did not specify a value.
The default and currently-understood value is `mongosh`.
You can also specify how the CLI attempts to invoke a MongoDB client using the **BOUNDARY_CONNECT_MONGO_STYLE** environment variable.
- `-username` `(string: "")` - The username you want to pass through to the client.
This value may be overridden by credentials sourced from a credential store.
You can also specify a username using the **BOUNDARY_CONNECT_USERNAME** environment variable.
- `-authentication-database` `(string: "")` - The authentication database for MongoDB.
If omitted, mongosh defaults `authSource` to the database name (`-dbname`). If no database is specified, it defaults to `admin`.
You can also specify the authentication database using the **BOUNDARY_CONNECT_MONGO_AUTHENTICATION_DATABASE** environment variable.
@include 'cmd-option-note.mdx'

@ -1246,6 +1246,10 @@
"title": "mysql",
"path": "commands/connect/mysql"
},
{
"title": "mongo",
"path": "commands/connect/mongo"
},
{
"title": "postgres",
"path": "commands/connect/postgres"

Loading…
Cancel
Save