Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,40 @@ func main() {
}
```

### Query Operations
### TLS / SSL Connections (e.g. AWS DocumentDB)

MongoDB-compatible services such as AWS DocumentDB can enforce in-transit
encryption. Enable TLS through the builder and, when the service uses a private
certificate authority, point it at the CA bundle (for DocumentDB this is the
global RDS CA bundle, `global-bundle.pem`):

```go
opts := database.NewMongoOptions().
SetHost("docdb-cluster.cluster-xxxx.eu-west-1.docdb.amazonaws.com:27017").
SetAuthSource("admin").
SetUsername("user").
SetPassword("password").
SetTimeout(30).
SetRetryWrites(false). // DocumentDB does not support retryable writes
SetTLS(true). // enforce TLS
SetTLSCAFile("/etc/ssl/global-bundle.pem"). // trust the DocumentDB CA bundle (implicitly enables TLS)
Build()

db, err := database.New(opts)
if err != nil {
log.Fatal(err)
}
```

TLS builder options:

| Method | Description |
|--------|-------------|
| `SetTLS(bool)` | Enables/disables a TLS connection to the server. |
| `SetTLSCAFile(string)` | Path to a PEM file with the trusted certificate authorities. Setting a non-empty path implicitly enables TLS. |
| `SetTLSInsecureSkipVerify(bool)` | Disables certificate/hostname verification. Insecure — local testing only. |



#### FindOne with Fluent API

Expand Down
79 changes: 79 additions & 0 deletions pkg/database/mongodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package database

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"time"

"github.com/go-playground/validator/v10"
Expand All @@ -23,6 +26,18 @@ type MongoOptions struct {
AuthMechanism string
ReplicaSet string
RetryWrites bool

// TLS enables a TLS/SSL connection to the server. This is required for
// MongoDB-compatible services that enforce in-transit encryption, such as
// AWS DocumentDB with TLS turned on.
TLS bool
// TLSCAFile is the path to a PEM file containing one or more certificate
// authorities to trust when making a TLS connection. For AWS DocumentDB this
// is the global RDS CA bundle (e.g. global-bundle.pem).
TLSCAFile string
// TLSInsecureSkipVerify disables certificate and hostname verification.
// This is insecure and should only be used for local testing.
TLSInsecureSkipVerify bool
}

// Validate validates the MongoOptions configuration
Expand Down Expand Up @@ -99,6 +114,32 @@ func (b *MongoOptionsBuilder) SetRetryWrites(retryWrites bool) *MongoOptionsBuil
return b
}

// SetTLS enables or disables a TLS/SSL connection to the server.
// Enable this for MongoDB-compatible services that enforce in-transit
// encryption, such as AWS DocumentDB with TLS turned on.
func (b *MongoOptionsBuilder) SetTLS(tls bool) *MongoOptionsBuilder {
b.options.TLS = tls
return b
}

// SetTLSCAFile sets the path to a PEM file containing one or more certificate
// authorities to trust when establishing a TLS connection. Setting this also
// implicitly enables TLS. For AWS DocumentDB use the global RDS CA bundle.
func (b *MongoOptionsBuilder) SetTLSCAFile(caFile string) *MongoOptionsBuilder {
b.options.TLSCAFile = caFile
if caFile != "" {
b.options.TLS = true
}
return b
}

// SetTLSInsecureSkipVerify disables certificate and hostname verification.
// This is insecure and should only be used for local testing.
func (b *MongoOptionsBuilder) SetTLSInsecureSkipVerify(skip bool) *MongoOptionsBuilder {
b.options.TLSInsecureSkipVerify = skip
return b
}

// Build builds the Mongo options
func (b *MongoOptionsBuilder) Build() *MongoOptions {
return b.options
Expand Down Expand Up @@ -161,6 +202,10 @@ func newMongoClientFromURI(ctx context.Context, options *MongoOptions) (Database
SetRetryWrites(options.RetryWrites).
SetMonitor(otelmongo.NewMonitor(otelmongo.WithCommandAttributeDisabled(false)))

if err := applyTLSConfig(opts, options); err != nil {
return nil, err
}

client, err := mongo.Connect(ctx, opts)
return &MongoClient{
Client: client,
Expand Down Expand Up @@ -202,13 +247,47 @@ func newMongoClientFromComponents(ctx context.Context, options *MongoOptions) (D
clientOpts.SetServerAPIOptions(serverAPI)
}

if err := applyTLSConfig(clientOpts, options); err != nil {
return nil, err
}

client, err := mongo.Connect(ctx, clientOpts)
return &MongoClient{
Client: client,
Options: options,
}, err
}

// applyTLSConfig configures TLS on the provided client options when TLS is
// enabled. It optionally loads a custom CA bundle (required by managed services
// such as AWS DocumentDB) and supports skipping verification for local testing.
func applyTLSConfig(clientOpts *moptions.ClientOptions, options *MongoOptions) error {
if !options.TLS && options.TLSCAFile == "" {
return nil
}

tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: options.TLSInsecureSkipVerify,
}

if options.TLSCAFile != "" {
caBytes, err := os.ReadFile(options.TLSCAFile)
if err != nil {
return fmt.Errorf("failed to read TLS CA file %q: %w", options.TLSCAFile, err)
}

caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caBytes) {
return fmt.Errorf("failed to parse any certificates from TLS CA file %q", options.TLSCAFile)
}
tlsConfig.RootCAs = caPool
}

clientOpts.SetTLSConfig(tlsConfig)
return nil
}

// Ping pings the MongoDB server to check connectivity
func (m *MongoClient) Ping(ctx context.Context) error {
err := m.Client.Ping(ctx, nil)
Expand Down
43 changes: 43 additions & 0 deletions pkg/database/mongodb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,49 @@ func TestMongoOptionsBuilder(t *testing.T) {
}
})

t.Run("TLSSetters", func(t *testing.T) {
opts := NewMongoOptions().
SetUri("mongodb://localhost").
SetTimeout(5000).
SetTLS(true).
SetTLSInsecureSkipVerify(true).
Build()

if !opts.TLS {
t.Error("expected TLS to be true")
}
if !opts.TLSInsecureSkipVerify {
t.Error("expected TLSInsecureSkipVerify to be true")
}
})

t.Run("TLSCAFileImplicitlyEnablesTLS", func(t *testing.T) {
opts := NewMongoOptions().
SetUri("mongodb://localhost").
SetTimeout(5000).
SetTLSCAFile("/etc/ssl/global-bundle.pem").
Build()

if opts.TLSCAFile != "/etc/ssl/global-bundle.pem" {
t.Errorf("expected TLSCAFile to be '/etc/ssl/global-bundle.pem', got '%s'", opts.TLSCAFile)
}
if !opts.TLS {
t.Error("expected TLS to be implicitly enabled when a CA file is set")
}
})

t.Run("EmptyTLSCAFileDoesNotEnableTLS", func(t *testing.T) {
opts := NewMongoOptions().
SetUri("mongodb://localhost").
SetTimeout(5000).
SetTLSCAFile("").
Build()

if opts.TLS {
t.Error("expected TLS to remain disabled when CA file is empty")
}
})

t.Run("PartialBuilder", func(t *testing.T) {
opts := NewMongoOptions().
SetUri("mongodb://localhost").
Expand Down
Loading