From 37f66c8391019c5583e3314ee37cc031b715de4f Mon Sep 17 00:00:00 2001 From: Randy <857271370@qq.com> Date: Sat, 7 Nov 2026 07:05:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20allow=20MongoDB=20SCRAM?= =?UTF-8?q?=20over=20an=20explicit=20tls=3Dfalse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many in-cluster MongoDB deployments, such as operator-managed replica sets, authenticate with SCRAM but have no TLS, and Weir refused them. Authentication now requires an explicit tls option instead of tls=true. tls=false keeps SCRAM-SHA-256, which never sends the password, and is documented for isolated networks only. --- config/weir.reference.yaml | 6 +- docs/configuration.md | 14 ++-- internal/app/credentials_test.go | 4 +- .../mongodb/secure_integration_test.go | 69 +++++++++++++++++++ internal/backend/mongodb/uri.go | 13 ++-- internal/backend/mongodb/uri_test.go | 15 +++- internal/testutil/testmongo/secure.go | 18 ++++- 7 files changed, 123 insertions(+), 16 deletions(-) diff --git a/config/weir.reference.yaml b/config/weir.reference.yaml index f28ffb60..7636ff0a 100644 --- a/config/weir.reference.yaml +++ b/config/weir.reference.yaml @@ -30,9 +30,11 @@ stores: # Direct mode isolates one member and provides no failover. # No userinfo, database path, fragments, SRV, mongos or load balancing. # Accepted URI options: replicaSet, directConnection, authSource, - # authMechanism=SCRAM-SHA-256, tls=true. Any other option fails. + # authMechanism=SCRAM-SHA-256, tls. Any other option fails. # Authentication requires the credentials below plus explicit authSource, - # SCRAM and TLS. Member certificates verify each discovered hostname/IP. + # SCRAM and tls. Prefer tls=true; tls=false sends data unencrypted and + # suits only an isolated network. Member certificates verify each + # discovered hostname/IP. # Example: # mongodb://mongo-a:27017,mongo-b:27017/?replicaSet=orders&authSource=admin&authMechanism=SCRAM-SHA-256&tls=true # CA files belong in tls.ca_file; URI tlsCAFile is rejected. diff --git a/docs/configuration.md b/docs/configuration.md index e83861b7..0a24bee9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -85,9 +85,14 @@ A seed without a port uses 27017. IPv6 seeds use brackets, such as URI userinfo, database paths, fragments, SRV, mongos/sharding, load balancing, secondary routing and other options are unsupported. A query requires the `/` separator before `?`. Accepted options are `replicaSet`, `directConnection`, -`authSource`, `authMechanism=SCRAM-SHA-256` and `tls=true`. Authentication requires the -`authentication` block, explicit SCRAM-SHA-256, `authSource` and TLS; TLS options -require credentials. Custom CAs belong in `tls.ca_file`, not in the URI. +`authSource`, `authMechanism=SCRAM-SHA-256` and `tls`. Authentication requires the +`authentication` block, explicit SCRAM-SHA-256, `authSource` and an explicit +`tls`; `tls` requires credentials. Use `tls=true` unless the backend has no TLS: +`tls=false` still authenticates with SCRAM-SHA-256, which never sends the +password, but sends all data unencrypted, so use it only on a network that only +the backend and its clients can reach, such as a cluster namespace guarded by a +NetworkPolicy. Custom CAs belong in `tls.ca_file`, not in the URI, and require +`tls=true`. Verification uses each actual member's hostname/IP, not the seed's name. OCSP follows the MongoDB driver: a stapled or responder answer of revoked fails the connection, while an unreachable responder does not. All @@ -116,7 +121,8 @@ confirmation of the same commit separately. Each username and password comes from exactly one explicit value or file; URI userinfo is rejected. Files contain UTF-8 text and allow one final LF/CRLF. -Authentication requires TLS to the backend. A custom `tls.ca_file` replaces the +Search authentication requires HTTPS, and MongoDB authentication an explicit +`tls` option (see [MongoDB](#mongodb)). A custom `tls.ca_file` replaces the system roots while retaining hostname verification. Backend identity and configuration stay local to each process. The application diff --git a/internal/app/credentials_test.go b/internal/app/credentials_test.go index 02858f16..44b020d6 100644 --- a/internal/app/credentials_test.go +++ b/internal/app/credentials_test.go @@ -273,9 +273,9 @@ func TestMongoCredentialMappingAndProfile(t *testing.T) { if err := cfg.Validate(); err == nil || strings.Contains(err.Error(), "sentinel") { t.Fatal("URI userinfo must be rejected without credential leakage", err) } - m.URI = "mongodb://unresolved.invalid:27017/?authSource=admin&authMechanism=SCRAM-SHA-256&tls=false" + m.URI = "mongodb://unresolved.invalid:27017/?authSource=admin&authMechanism=SCRAM-SHA-256" if err := cfg.Validate(); err == nil || strings.Contains(err.Error(), "sentinel") { - t.Fatal("separate credentials must still enforce the audited SCRAM/TLS profile", err) + t.Fatal("separate credentials must still enforce the audited SCRAM profile with explicit tls", err) } } diff --git a/internal/backend/mongodb/secure_integration_test.go b/internal/backend/mongodb/secure_integration_test.go index 6938a28e..ffae753b 100644 --- a/internal/backend/mongodb/secure_integration_test.go +++ b/internal/backend/mongodb/secure_integration_test.go @@ -82,6 +82,75 @@ func TestMongoSCRAMTLSProductionOpen(t *testing.T) { } } +// plaintextConfig turns a fixture configuration into SCRAM over tls=false. +func plaintextConfig(t *testing.T, cfg Config) Config { + t.Helper() + parsed, err := url.Parse(cfg.URI) + if err != nil { + t.Fatal("invalid owned MongoDB fixture URI") + } + query := parsed.Query() + query.Set("tls", "false") + parsed.RawQuery = query.Encode() + cfg.URI = parsed.String() + cfg.CAFile = "" + return cfg +} + +// With an explicit tls=false, SCRAM-SHA-256 authenticates over plaintext: +// writes and reads work, a wrong password still fails, and a server that +// requires TLS refuses the connection. +func TestMongoSCRAMWithoutTLSOpen(t *testing.T) { + fixture := testmongo.OpenSecureAllowingPlaintext(t) + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) + defer cancel() + cfg := Config{URI: fixture.URI, Store: "mongo"} + cfg = plaintextConfig(t, mongoFixtureConfig(t, cfg)) + adapter, err := Open(ctx, cfg) + if err != nil { + t.Fatal(err) + } + defer adapter.Close(context.Background()) + doc := bson.D{{Key: "_id", Value: "plaintext"}, {Key: "n", Value: int64(7)}} + raw, _ := bson.Marshal(doc) + document := &pb.Document{ContentType: "application/bson", Data: raw} + put := &pb.MutateRequest_Put{Put: document} + request := &pb.MutateRequest{Resource: fixture.DB + "/records/s:plaintext", Action: put} + operation := &pb.Command_Mutate{Mutate: request} + command := &pb.Command{Operation: operation} + op := &pb.ExecuteRequest{Index: 1, Command: command} + plan, failure := prepareTestRecord(adapter, op) + if failure != nil { + t.Fatal(failure) + } + result := adapter.ExecuteRecords(ctx, []*execution.Plan{plan}) + if result[0].GetMutationResult().GetOutcome() != pb.MutationOutcome_APPLIED { + t.Fatal(result) + } + read := prepareCounter(t, adapter, fixture.DB+"/records/s:plaintext") + result = adapter.ExecuteRecords(ctx, []*execution.Plan{read}) + if result[0].GetReadResult().GetDocument() == nil { + t.Fatal(result) + } + badConfig := Config{URI: fixture.BadPassURI, Store: "mongo"} + badConfig = plaintextConfig(t, mongoFixtureConfig(t, badConfig)) + badContext, badCancel := context.WithTimeout(t.Context(), 5*time.Second) + defer badCancel() + if bad, err := Open(badContext, badConfig); err == nil { + bad.Close(context.Background()) + t.Fatal("wrong password authenticated over plaintext") + } + strict := testmongo.OpenSecure(t) + strictConfig := Config{URI: strict.URI, Store: "mongo"} + strictConfig = plaintextConfig(t, mongoFixtureConfig(t, strictConfig)) + strictContext, strictCancel := context.WithTimeout(t.Context(), 5*time.Second) + defer strictCancel() + if connected, err := Open(strictContext, strictConfig); err == nil { + connected.Close(context.Background()) + t.Fatal("tls=false connected to a server that requires TLS") + } +} + func TestMongoSCRAMTLSRepeatedFailureAndClose(t *testing.T) { fixture := testmongo.OpenSecure(t) baseline := connectionCount(t, fixture.Admin) diff --git a/internal/backend/mongodb/uri.go b/internal/backend/mongodb/uri.go index 918802cb..4f19e41c 100644 --- a/internal/backend/mongodb/uri.go +++ b/internal/backend/mongodb/uri.go @@ -22,9 +22,9 @@ func parseConfig(cfg Config) (*connstring.ConnString, error) { if err != nil { return nil, err } - // parseURI allows TLS only with SCRAM, so this covers both. - if (cfg.Username != "") != uri.SSL { - return nil, errors.New("MongoDB credentials and SCRAM with TLS require each other") + // parseURI allows tls only with SCRAM, so this covers both. + if (cfg.Username != "") != (uri.AuthMechanism != "") { + return nil, errors.New("MongoDB credentials and SCRAM require each other") } if cfg.CAFile != "" && !uri.SSL { return nil, errors.New("MongoDB CA file requires TLS") @@ -62,9 +62,12 @@ func parseURI(raw string) (*connstring.ConnString, error) { } else if len(uri.Hosts) != 1 || uri.DirectConnectionSet && !uri.DirectConnection { return nil, errors.New("MongoDB discovery requires an explicit replica-set name") } + // Authentication must state tls. tls=false sends SCRAM-SHA-256, which never + // reveals the password, and all data unencrypted, so it belongs only on a + // network that only the backend and its clients can reach. if uri.AuthMechanism != "" || uri.AuthSourceSet || uri.SSLSet { - if uri.AuthMechanism != "SCRAM-SHA-256" || !uri.AuthSourceSet || !validDatabaseName(uri.AuthSource) || !uri.SSL { - return nil, errors.New("MongoDB URI authentication requires explicit SCRAM-SHA-256, authSource, and TLS") + if uri.AuthMechanism != "SCRAM-SHA-256" || !uri.AuthSourceSet || !validDatabaseName(uri.AuthSource) || !uri.SSLSet { + return nil, errors.New("MongoDB URI authentication requires explicit SCRAM-SHA-256, authSource, and tls") } } return uri, nil diff --git a/internal/backend/mongodb/uri_test.go b/internal/backend/mongodb/uri_test.go index 69133449..553a49e1 100644 --- a/internal/backend/mongodb/uri_test.go +++ b/internal/backend/mongodb/uri_test.go @@ -33,6 +33,12 @@ func TestValidateMongoConfigProfiles(t *testing.T) { t.Fatal("supported credential values rejected or static validation accessed CA/DNS", err) } } + + // SCRAM over an isolated plaintext network must say so with tls=false. + plaintext := Config{URI: "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false", Username: "user", Password: "password"} + if err := ValidateConfig(plaintext); err != nil { + t.Fatal("explicit tls=false SCRAM rejected", err) + } } func TestValidateMongoConfigRejectsOutsideProfile(t *testing.T) { @@ -78,7 +84,6 @@ func TestValidateMongoConfigRejectsOutsideProfile(t *testing.T) { "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&tls=true", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=invalid.name&tls=true", - "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tlsInsecure=true", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tlsCAFile=", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tlsCAFile=private-sentinel%0Apath", @@ -88,6 +93,14 @@ func TestValidateMongoConfigRejectsOutsideProfile(t *testing.T) { invalid.URI = uri cases = append(cases, invalid) } + // A CA file cannot apply without TLS, and tls=false needs credentials. + plaintext := secure + plaintext.URI = "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false" + plaintext.CAFile = "/missing/ca-private-sentinel.pem" + cases = append(cases, plaintext) + anonymous := base + anonymous.URI = "mongodb://127.0.0.1:27028/?tls=false" + cases = append(cases, anonymous) for _, pair := range []struct{ username, password string }{ {"", "password-private-sentinel"}, {"user-private-sentinel", ""}, diff --git a/internal/testutil/testmongo/secure.go b/internal/testutil/testmongo/secure.go index f55cf7f6..53eebafa 100644 --- a/internal/testutil/testmongo/secure.go +++ b/internal/testutil/testmongo/secure.go @@ -38,6 +38,7 @@ type SecureFixture struct { Client *mongo.Client startup context.Context + tlsMode string owned bool root string process *exec.Cmd @@ -55,11 +56,24 @@ type SecureFixture struct { var secureFixtureSequence atomic.Uint64 +// OpenSecure starts a mongod that requires TLS and SCRAM. func OpenSecure(t *testing.T) *SecureFixture { + t.Helper() + return openSecure(t, "requireTLS") +} + +// OpenSecureAllowingPlaintext starts a mongod that requires SCRAM and accepts +// both TLS and plaintext connections. +func OpenSecureAllowingPlaintext(t *testing.T) *SecureFixture { + t.Helper() + return openSecure(t, "preferTLS") +} + +func openSecure(t *testing.T, tlsMode string) *SecureFixture { t.Helper() startup, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() - fixture := &SecureFixture{startup: startup} + fixture := &SecureFixture{startup: startup, tlsMode: tlsMode} t.Cleanup(func() { fixture.cleanup(t) }) fixture.prepareRoot(t) fixture.startMongo(t) @@ -263,7 +277,7 @@ func (fixture *SecureFixture) startMongo(t *testing.T) { "--replSet", "weir_mongo_secure", "--auth", "--keyFile", filepath.Join(fixture.root, "keyfile"), - "--tlsMode", "requireTLS", + "--tlsMode", fixture.tlsMode, "--tlsCertificateKeyFile", filepath.Join(fixture.root, "server.pem"), "--tlsCAFile", fixture.caFile, "--tlsAllowConnectionsWithoutCertificates", From c87a200b9e5e560f05094d455d96dbf074754fed Mon Sep 17 00:00:00 2001 From: Randy <857271370@qq.com> Date: Sat, 7 Nov 2026 07:26:04 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20test:=20prove=20tls=3Dfalse=20i?= =?UTF-8?q?s=20plaintext=20and=20state=20its=20risks=20plainly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - serverStatus shows Weir's connections skip TLS, replacing the 5s requireTLS refusal - the wrong password must fail with AuthenticationFailed - unit tests pin the tls=false driver options and URI edge cases - docs name the offline-guessing and hijack risks of plaintext SCRAM --- config/weir.reference.yaml | 7 +- docs/configuration.md | 16 +++-- .../mongodb/secure_integration_test.go | 69 ++++++++++++++++--- internal/backend/mongodb/uri.go | 7 +- internal/backend/mongodb/uri_test.go | 41 ++++++++++- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/config/weir.reference.yaml b/config/weir.reference.yaml index 7636ff0a..a3783a3a 100644 --- a/config/weir.reference.yaml +++ b/config/weir.reference.yaml @@ -32,9 +32,10 @@ stores: # Accepted URI options: replicaSet, directConnection, authSource, # authMechanism=SCRAM-SHA-256, tls. Any other option fails. # Authentication requires the credentials below plus explicit authSource, - # SCRAM and tls. Prefer tls=true; tls=false sends data unencrypted and - # suits only an isolated network. Member certificates verify each - # discovered hostname/IP. + # SCRAM and tls. Prefer tls=true. tls=false sends the SCRAM handshake and + # every command in plaintext, so anyone on the path can read or change + # them; use it only on a trusted network path to every member. With + # tls=true, member certificates verify each discovered hostname/IP. # Example: # mongodb://mongo-a:27017,mongo-b:27017/?replicaSet=orders&authSource=admin&authMechanism=SCRAM-SHA-256&tls=true # CA files belong in tls.ca_file; URI tlsCAFile is rejected. diff --git a/docs/configuration.md b/docs/configuration.md index 0a24bee9..20c777fb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -87,12 +87,16 @@ secondary routing and other options are unsupported. A query requires the `/` separator before `?`. Accepted options are `replicaSet`, `directConnection`, `authSource`, `authMechanism=SCRAM-SHA-256` and `tls`. Authentication requires the `authentication` block, explicit SCRAM-SHA-256, `authSource` and an explicit -`tls`; `tls` requires credentials. Use `tls=true` unless the backend has no TLS: -`tls=false` still authenticates with SCRAM-SHA-256, which never sends the -password, but sends all data unencrypted, so use it only on a network that only -the backend and its clients can reach, such as a cluster namespace guarded by a -NetworkPolicy. Custom CAs belong in `tls.ca_file`, not in the URI, and require -`tls=true`. +`tls`; `tls` requires credentials. Use `tls=true` unless the backend has no TLS. +With `tls=false` Weir still authenticates with SCRAM-SHA-256, but the handshake +and every command travel in plaintext with nothing binding them together. +Anyone who can see the traffic can read and change commands, guess the password +offline from a captured handshake, or relay the handshake and then use the +authenticated connection with Weir's privileges. Use it only when the network +path to every replica-set member, including members Weir discovers, is trusted, +and give the user a long random password. A NetworkPolicy limits which Pods can +connect, but does not hide traffic from anyone with node or host-network access. +Custom CAs belong in `tls.ca_file`, not in the URI, and require `tls=true`. Verification uses each actual member's hostname/IP, not the seed's name. OCSP follows the MongoDB driver: a stapled or responder answer of revoked fails the connection, while an unreachable responder does not. All diff --git a/internal/backend/mongodb/secure_integration_test.go b/internal/backend/mongodb/secure_integration_test.go index ffae753b..97bb797f 100644 --- a/internal/backend/mongodb/secure_integration_test.go +++ b/internal/backend/mongodb/secure_integration_test.go @@ -14,6 +14,7 @@ import ( "github.com/batchstream/weir/internal/execution" "github.com/batchstream/weir/internal/testutil/testmongo" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" ) func TestMongoSCRAMTLSProductionOpen(t *testing.T) { @@ -98,14 +99,15 @@ func plaintextConfig(t *testing.T, cfg Config) Config { } // With an explicit tls=false, SCRAM-SHA-256 authenticates over plaintext: -// writes and reads work, a wrong password still fails, and a server that -// requires TLS refuses the connection. +// writes and reads work without a TLS handshake, and a wrong password still +// fails authentication. func TestMongoSCRAMWithoutTLSOpen(t *testing.T) { fixture := testmongo.OpenSecureAllowingPlaintext(t) ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) defer cancel() cfg := Config{URI: fixture.URI, Store: "mongo"} cfg = plaintextConfig(t, mongoFixtureConfig(t, cfg)) + before := settledTransportCounts(t, fixture.Admin) adapter, err := Open(ctx, cfg) if err != nil { t.Fatal(err) @@ -132,23 +134,68 @@ func TestMongoSCRAMWithoutTLSOpen(t *testing.T) { if result[0].GetReadResult().GetDocument() == nil { t.Fatal(result) } + after := transportCounts(t, fixture.Admin) + if after.connections <= before.connections || after.handshakes != before.handshakes { + t.Fatalf("tls=false must connect without TLS: connections %d->%d, TLS handshakes %d->%d", + before.connections, after.connections, before.handshakes, after.handshakes) + } badConfig := Config{URI: fixture.BadPassURI, Store: "mongo"} badConfig = plaintextConfig(t, mongoFixtureConfig(t, badConfig)) badContext, badCancel := context.WithTimeout(t.Context(), 5*time.Second) defer badCancel() - if bad, err := Open(badContext, badConfig); err == nil { + bad, err := Open(badContext, badConfig) + if err == nil { bad.Close(context.Background()) t.Fatal("wrong password authenticated over plaintext") } - strict := testmongo.OpenSecure(t) - strictConfig := Config{URI: strict.URI, Store: "mongo"} - strictConfig = plaintextConfig(t, mongoFixtureConfig(t, strictConfig)) - strictContext, strictCancel := context.WithTimeout(t.Context(), 5*time.Second) - defer strictCancel() - if connected, err := Open(strictContext, strictConfig); err == nil { - connected.Close(context.Background()) - t.Fatal("tls=false connected to a server that requires TLS") + if !strings.Contains(err.Error(), "MongoDB code 18") { + t.Fatal("wrong password over plaintext did not fail authentication", err) + } +} + +type transportCount struct { + connections int64 + handshakes int64 +} + +// settledTransportCounts waits until the fixture's own clients stop opening +// monitoring connections, so later changes belong to the code under test. +func settledTransportCounts(t *testing.T, client *mongo.Client) transportCount { + t.Helper() + previous := transportCounts(t, client) + for range 20 { + time.Sleep(250 * time.Millisecond) + current := transportCounts(t, client) + if current == previous { + return current + } + previous = current + } + t.Fatal("fixture connections did not settle") + return previous +} + +// transportCounts reads how many connections the server has accepted and how +// many of them negotiated TLS. +func transportCounts(t *testing.T, client *mongo.Client) transportCount { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + command := bson.D{{Key: "serverStatus", Value: 1}} + var status struct { + Connections struct { + TotalCreated int64 `bson:"totalCreated"` + } `bson:"connections"` + TransportSecurity map[string]int64 `bson:"transportSecurity"` + } + if err := client.Database("admin").RunCommand(ctx, command).Decode(&status); err != nil { + t.Fatal(err) + } + count := transportCount{connections: status.Connections.TotalCreated} + for _, handshakes := range status.TransportSecurity { + count.handshakes += handshakes } + return count } func TestMongoSCRAMTLSRepeatedFailureAndClose(t *testing.T) { diff --git a/internal/backend/mongodb/uri.go b/internal/backend/mongodb/uri.go index 4f19e41c..6311b249 100644 --- a/internal/backend/mongodb/uri.go +++ b/internal/backend/mongodb/uri.go @@ -62,9 +62,10 @@ func parseURI(raw string) (*connstring.ConnString, error) { } else if len(uri.Hosts) != 1 || uri.DirectConnectionSet && !uri.DirectConnection { return nil, errors.New("MongoDB discovery requires an explicit replica-set name") } - // Authentication must state tls. tls=false sends SCRAM-SHA-256, which never - // reveals the password, and all data unencrypted, so it belongs only on a - // network that only the backend and its clients can reach. + // Authentication must state tls. With tls=false the SCRAM handshake and all + // commands are plaintext: anyone on the path can read and change them, guess + // the password offline from the handshake, or take over the authenticated + // connection. It suits only a trusted network path to every member. if uri.AuthMechanism != "" || uri.AuthSourceSet || uri.SSLSet { if uri.AuthMechanism != "SCRAM-SHA-256" || !uri.AuthSourceSet || !validDatabaseName(uri.AuthSource) || !uri.SSLSet { return nil, errors.New("MongoDB URI authentication requires explicit SCRAM-SHA-256, authSource, and tls") diff --git a/internal/backend/mongodb/uri_test.go b/internal/backend/mongodb/uri_test.go index 553a49e1..06cb6d0e 100644 --- a/internal/backend/mongodb/uri_test.go +++ b/internal/backend/mongodb/uri_test.go @@ -35,9 +35,15 @@ func TestValidateMongoConfigProfiles(t *testing.T) { } // SCRAM over an isolated plaintext network must say so with tls=false. - plaintext := Config{URI: "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false", Username: "user", Password: "password"} - if err := ValidateConfig(plaintext); err != nil { - t.Fatal("explicit tls=false SCRAM rejected", err) + // Option names are case-insensitive, as elsewhere in the URI. + for _, uri := range []string{ + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&TLS=false", + } { + plaintext := Config{URI: uri, Username: "user", Password: "password"} + if err := ValidateConfig(plaintext); err != nil { + t.Fatal("explicit tls=false SCRAM rejected", uri, err) + } } } @@ -85,6 +91,12 @@ func TestValidateMongoConfigRejectsOutsideProfile(t *testing.T) { "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&tls=true", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=invalid.name&tls=true", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tlsInsecure=true", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=FALSE", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=0", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tls=false", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false&tls=true", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&ssl=false", + "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false&tlsInsecure=true", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tlsCAFile=", "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=true&tlsCAFile=private-sentinel%0Apath", secure.URI + "&tlsCAFile=" + strings.Repeat("a", 2049), @@ -156,6 +168,29 @@ func TestMongoDriverAuthenticationOptions(t *testing.T) { } } +// tls=false keeps SCRAM authentication and configures no TLS at all. +func TestMongoDriverPlaintextAuthenticationOptions(t *testing.T) { + cfg := Config{ + URI: "mongodb://unresolved.invalid:27028/?authMechanism=SCRAM-SHA-256&authSource=admin&tls=false&directConnection=true", + Username: "user", + Password: "password", + } + uri, err := parseConfig(cfg) + if err != nil { + t.Fatal(err) + } + opts, err := connectionOptions(cfg, uri) + if err != nil { + t.Fatal(err) + } + if opts.Auth == nil || opts.Auth.AuthMechanism != "SCRAM-SHA-256" || opts.Auth.AuthSource != "admin" || !opts.Auth.PasswordSet { + t.Fatal("tls=false must keep SCRAM authentication") + } + if opts.TLSConfig != nil { + t.Fatal("tls=false must not configure TLS") + } +} + func TestMongoDriverUnauthenticatedOptions(t *testing.T) { cfg := Config{URI: "mongodb://127.0.0.1:27028/"} if err := ValidateConfig(cfg); err != nil {