Files
zeekayandHanzo Dev 06d5eca873 Declare operations on the group that serves them
zip's registration is a verb method on the group, so an operation reads as the
group answering it rather than a function taking the group, and a route that is
not a typed operation says which method it answers with. The package-level form
and the Router interface it existed to accept any target through are both gone.

The toolchain and the pin move together, deliberately. Below zip v1.36.50 the
JSON encoder was selected on a build tag, so a repo whose go directive rises
first writes a wire that is neither the old release's nor the new one's until the
pin catches up.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 06:11:42 -07:00

1311 lines
33 KiB
Go

// Copyright 2021 DocDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package integration
import (
"net/url"
"testing"
"time"
"github.com/FerretDB/wire"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/wire/wireclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
xdgscram "github.com/xdg-go/scram"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/hanzoai/docdb/internal/util/must"
"github.com/hanzoai/docdb/internal/util/testutil"
"github.com/hanzoai/docdb/integration/setup"
)
func TestAuth(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, nil)
ctx := s.Ctx
collection := s.Collection
db := collection.Database()
testCases := map[string]struct { //nolint:vet // for readability
username string
password string
updatePassword string // if set, the password will be updated to this one after the user is created.
mechanisms bson.A // mechanisms to use for creating user authentication
authMechanism string
userNotFound bool
wrongPassword bool
pingErr string
}{
"ScramSHA256": {
username: "scramsha256",
password: "password",
mechanisms: bson.A{"SCRAM-SHA-256"},
authMechanism: "SCRAM-SHA-256",
},
"MultipleScramSHA256": {
username: "scramsha256multi",
password: "password",
mechanisms: bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"},
authMechanism: "SCRAM-SHA-256",
},
"ScramSHA256Updated": {
username: "scramsha256updated",
password: "pass123",
updatePassword: "anotherpassword",
mechanisms: bson.A{"SCRAM-SHA-256"},
authMechanism: "SCRAM-SHA-256",
},
"NotFoundUser": {
username: "notfound",
password: "something",
mechanisms: bson.A{"SCRAM-SHA-256"},
authMechanism: "SCRAM-SHA-256",
userNotFound: true,
pingErr: "Authentication failed.",
},
"BadPassword": {
username: "baduser",
password: "something",
mechanisms: bson.A{"SCRAM-SHA-256"},
authMechanism: "SCRAM-SHA-256",
wrongPassword: true,
pingErr: "Authentication failed.",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
if !tc.userNotFound {
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/864
_ = db.RunCommand(ctx, bson.D{{"dropUser", tc.username}})
// root role is only available in admin database, a role with sufficient privilege is used
roles := bson.A{"readWrite"}
if !setup.IsMongoDB(t) {
// TODO https://github.com/hanzoai/docdb/issues/3974
roles = bson.A{}
}
createPayload := bson.D{
{"createUser", tc.username},
{"roles", roles},
{"pwd", tc.password},
{"mechanisms", tc.mechanisms},
}
err := db.RunCommand(ctx, createPayload).Err()
require.NoError(t, err, "cannot create user")
}
if tc.updatePassword != "" {
updatePayload := bson.D{
{"updateUser", tc.username},
{"pwd", tc.updatePassword},
}
err := db.RunCommand(ctx, updatePayload).Err()
require.NoError(t, err, "cannot update user")
}
password := tc.password
if tc.updatePassword != "" {
password = tc.updatePassword
}
if tc.wrongPassword {
password = "wrongpassword"
}
credential := options.Credential{
AuthMechanism: tc.authMechanism,
AuthSource: db.Name(),
Username: tc.username,
Password: password,
}
opts := options.Client().ApplyURI(s.MongoDBURI).SetAuth(credential)
client, err := mongo.Connect(ctx, opts)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, client.Disconnect(ctx))
})
// Ping to force connection to be established and tested.
err = client.Ping(ctx, nil)
if tc.pingErr != "" {
require.ErrorContains(t, err, tc.pingErr)
return
}
connCollection := client.Database(db.Name()).Collection(collection.Name())
require.NotNil(t, connCollection, "cannot get collection")
r, err := connCollection.InsertOne(ctx, bson.D{{"ping", "pong"}})
require.NoError(t, err, "cannot insert document")
id := r.InsertedID.(primitive.ObjectID)
require.NotEmpty(t, id)
var result bson.D
err = connCollection.FindOne(ctx, bson.D{{"_id", id}}).Decode(&result)
require.NoError(t, err, "cannot find document")
require.Equal(t, bson.D{{"_id", id}, {"ping", "pong"}}, result)
})
}
}
func TestAuthAlreadyAuthenticated(tt *testing.T) {
t := setup.FailsForDocDB(tt, "https://github.com/hanzoai/docdb-DocumentDB/issues/953")
tt.Parallel()
s := setup.SetupWithOpts(tt, nil)
ctx, db := s.Ctx, s.Collection.Database()
username, password, mechanism := "testuser", "testpass", "SCRAM-SHA-256"
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/864
_ = db.RunCommand(ctx, bson.D{{"dropUser", username}})
err := db.RunCommand(ctx, bson.D{
{"createUser", username},
{"roles", bson.A{}},
{"pwd", password},
{"mechanisms", bson.A{mechanism}},
}).Err()
require.NoError(t, err, "cannot create user")
credential := options.Credential{
AuthMechanism: mechanism,
AuthSource: db.Name(),
Username: username,
Password: password,
}
opts := options.Client().ApplyURI(s.MongoDBURI).SetAuth(credential)
client, err := mongo.Connect(ctx, opts)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, client.Disconnect(ctx))
})
db = client.Database(db.Name())
var res bson.D
err = db.RunCommand(ctx, bson.D{{"connectionStatus", 1}}).Decode(&res)
require.NoError(t, err)
expected := bson.D{
{"authInfo", bson.D{
{"authenticatedUsers", bson.A{
bson.D{
{"user", username},
{"db", db.Name()},
},
}},
{"authenticatedUserRoles", bson.A{}},
}},
{"ok", float64(1)},
}
AssertEqualDocuments(t, expected, res)
saslStart := bson.D{
{"saslStart", 1},
{"mechanism", mechanism},
{"payload", []byte("n,,n=testuser,r=Y0iJqJu58tGDrUdtqS7+m0oMe4sau3f6")},
{"autoAuthorize", 1},
{"options", bson.D{{"skipEmptyExchange", true}}},
}
err = db.RunCommand(ctx, saslStart).Decode(&res)
require.NoError(t, err)
err = db.RunCommand(ctx, bson.D{{"connectionStatus", 1}}).Decode(&res)
require.NoError(t, err)
AssertEqualDocuments(t, expected, res)
err = db.RunCommand(ctx, saslStart).Decode(&res)
require.NoError(t, err)
}
func TestSASLContinueErrors(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
t.Run("HandshakeFails", func(t *testing.T) {
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
msg := wire.MustOpMsg(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"$db", "admin",
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
p, ok := res.Get("payload").(wirebson.Binary)
require.True(t, ok)
require.NotEmpty(t, p.B)
err = res.Replace("payload", wirebson.Binary{})
require.NoError(t, err)
FixCluster(t, res)
expected := wirebson.MustDocument(
"conversationId", int32(1),
"done", false,
"payload", wirebson.Binary{},
"ok", float64(1),
)
testutil.AssertEqual(t, expected, res)
payload = "invalid"
msg = wire.MustOpMsg(
"saslContinue", int32(1),
"conversationId", int32(1),
"payload", wirebson.Binary{B: []byte(payload)},
"$db", "admin",
)
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
res, err = resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
FixCluster(t, res)
expected = wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Authentication failed.",
"code", int32(18),
"codeName", "AuthenticationFailed",
)
testutil.AssertEqual(t, expected, res)
msg = wire.MustOpMsg(
"saslContinue", int32(1),
"conversationId", int32(1),
"payload", wirebson.Binary{},
"$db", "admin",
)
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
res, err = resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
FixCluster(t, res)
expected = wirebson.MustDocument(
"ok", float64(0),
"errmsg", "No SASL session state found",
"code", int32(17),
"codeName", "ProtocolError",
)
testutil.AssertEqual(t, expected, res)
})
t.Run("NoAuthenticatedUser", func(t *testing.T) {
msg := wire.MustOpMsg(
"connectionStatus", int64(1),
"$db", testutil.DatabaseName(t),
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
FixCluster(t, res)
expected := wirebson.MustDocument(
"authInfo", wirebson.MustDocument(
"authenticatedUsers", wirebson.MustArray(),
"authenticatedUserRoles", wirebson.MustArray(),
),
"ok", float64(1),
)
testutil.AssertEqual(t, expected, res)
})
t.Run("FindFails", func(t *testing.T) {
msg := wire.MustOpMsg(
"find", "values",
"filter", wirebson.MustDocument(),
"$db", testutil.DatabaseName(t),
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
FixCluster(t, res)
expected := wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Command find requires authentication",
"code", int32(13),
"codeName", "Unauthorized",
)
testutil.AssertEqual(t, expected, res)
})
}
func TestSASLContinueNoConversation(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
msg := wire.MustOpMsg(
"saslContinue", int32(1),
"conversationId", int32(1),
"payload", wirebson.Binary{},
"$db", "admin",
)
_, resBody, err := conn.Request(ctx, msg)
require.NoError(t, err)
res, err := resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
FixCluster(t, res)
expected := wirebson.MustDocument(
"ok", float64(0),
"errmsg", "No SASL session state found",
"code", int32(17),
"codeName", "ProtocolError",
)
testutil.AssertEqual(t, expected, res)
}
func TestHelloIsMasterOpQuerySpeculative(t *testing.T) {
t.Parallel()
for name, tc := range map[string]struct {
command string
}{
"Hello": {command: "hello"},
"IsMaster": {command: "isMaster"},
// avoid panic: Directory name "testopqueryspeculativeauthenticate_ismaster" already used by
// another test "TestOpQuerySpeculativeAuthenticate/Ismaster".
"IsmasterLower": {command: "ismaster"},
} {
t.Run(name, func(t *testing.T) {
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
t.Run("SpeculativeAuthenticate", func(t *testing.T) {
// as is request sent by C# driver
q := wire.MustOpQuery(
tc.command, int32(1),
"helloOk", true,
"client", wirebson.MustDocument(
"driver", wirebson.MustDocument(
"name", "mongo-go-driver",
"version", "2.28.0",
),
"os", wirebson.MustDocument(
"type", "Linux",
"name", "Ubuntu 22.04.5 LTS",
"architecture", "amd64",
"version", "22.04.5",
),
"platform", ".NET 8.0.8",
),
"compression", wirebson.MustArray(),
"speculativeAuthenticate", wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"options", wirebson.MustDocument("skipEmptyExchange", true),
"db", "admin",
),
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpReply).Document()
require.NoError(t, err)
ok := res.Get("ok")
require.Equal(t, float64(1), ok)
speculativeAuthenticateV := res.Get("speculativeAuthenticate")
require.NotNil(t, speculativeAuthenticateV)
var speculativeAuthenticate *wirebson.Document
speculativeAuthenticate, err = speculativeAuthenticateV.(wirebson.AnyDocument).Decode()
require.NoError(t, err)
done := speculativeAuthenticate.Get("done")
require.Equal(t, false, done)
payload, err = conv.Step(string(speculativeAuthenticate.Get("payload").(wirebson.Binary).B))
require.NoError(t, err)
})
t.Run("SASLContinue", func(t *testing.T) {
q := wire.MustOpQuery(
"saslContinue", int32(1),
"conversationId", int32(1),
"payload", wirebson.Binary{B: []byte(payload)},
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpReply).Document()
require.NoError(t, err)
serverPayload, ok := res.Get("payload").(wirebson.Binary)
require.True(t, ok)
FixCluster(t, res)
expectedComparable := wirebson.MustDocument(
"conversationId", int32(1),
"done", true,
"payload", serverPayload,
"ok", float64(1),
)
testutil.AssertEqual(t, expectedComparable, res)
_, err = conv.Step(string(serverPayload.B))
require.NoError(t, err)
require.True(t, conv.Valid())
})
t.Run("SASLContinueTooMany", func(t *testing.T) {
q := wire.MustOpQuery(
"saslContinue", int32(1),
"payload", wirebson.Binary{},
"conversationId", int32(1),
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpReply).Document()
require.NoError(t, err)
FixCluster(t, res)
expected := wirebson.MustDocument(
"ok", float64(0),
"errmsg", "No SASL session state found",
"code", int32(17),
"codeName", "ProtocolError",
)
testutil.AssertEqual(t, expected, res)
})
})
t.Run(name+"NotSuccess", func(t *testing.T) {
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
client := must.NotFail(xdgscram.SHA256.NewClient("user", "wrong", ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
q := wire.MustOpQuery(
tc.command, int32(1),
"speculativeAuthenticate", wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"db", "admin",
),
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
_, resBody, err := conn.Request(ctx, q)
require.NoError(t, err)
res, err := resBody.(*wire.OpReply).Document()
require.NoError(t, err)
ok := res.Get("ok")
require.Equal(t, float64(1), ok)
speculativeAuthenticate := res.Get("speculativeAuthenticate")
require.Nil(t, speculativeAuthenticate)
})
t.Run(name+"TypeMismatch", func(t *testing.T) {
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
q := wire.MustOpQuery(
tc.command, int32(1),
"speculativeAuthenticate", int32(1),
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
_, resBody, err := conn.Request(ctx, q)
require.NoError(t, err)
resMsg, err := resBody.(*wire.OpReply).Document()
require.NoError(t, err)
ok := resMsg.Get("ok")
assert.Equal(t, float64(0), ok)
code := resMsg.Get("code")
assert.Equal(t, int32(14), code)
codeName := resMsg.Get("codeName")
assert.Equal(t, "TypeMismatch", codeName)
})
t.Run(name+"NoDB", func(t *testing.T) {
mechanism := "SCRAM-SHA-256"
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
q := wire.MustOpQuery(
tc.command, int32(1),
"speculativeAuthenticate", wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", mechanism,
"payload", wirebson.Binary{B: []byte(payload)},
),
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
_, resBody, err := conn.Request(ctx, q)
require.NoError(t, err)
res, err := resBody.(*wire.OpReply).Document()
require.NoError(t, err)
ok := res.Get("ok")
assert.Equal(t, float64(1), ok)
speculativeAuthenticate := res.Get("speculativeAuthenticate")
require.Nil(t, speculativeAuthenticate)
})
}
}
func TestHelloSpeculative(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
clientPayload := must.NotFail(conv.Step(""))
t.Run("SpeculativeAuthenticate", func(t *testing.T) {
// as is request sent by legacy Mongo shell
msg := wire.MustOpMsg(
"hello", int32(1),
"speculativeAuthenticate", wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(clientPayload)},
"db", "admin",
),
"saslSupportedMechs", "admin."+username,
"client", wirebson.MustDocument(
"application", wirebson.MustDocument(
"name", "MongoDB Shell",
),
"driver", wirebson.MustDocument(
"name", "MongoDB Internal Client",
"version", "7.0.8",
),
"os", wirebson.MustDocument(
"type", "Linux",
"name", "Ubuntu",
"architecture", "x86_64",
"version", "22.04",
),
),
"$db", "admin",
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpMsg).Document()
require.NoError(t, err)
speculativeAuthenticateV, ok := res.Get("speculativeAuthenticate").(wirebson.AnyDocument)
require.True(t, ok)
var speculativeAuthenticate *wirebson.Document
speculativeAuthenticate, err = speculativeAuthenticateV.Decode()
require.NoError(t, err)
serverPayload, ok := speculativeAuthenticate.Get("payload").(wirebson.Binary)
require.True(t, ok)
connectionID := res.Get("connectionId")
assert.IsType(t, int32(0), connectionID)
localTime := res.Get("localTime")
assert.IsType(t, time.Time{}, localTime)
saslSupportedMechs := res.Get("saslSupportedMechs")
require.NotNil(t, saslSupportedMechs)
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/566
res.Remove("hosts")
res.Remove("setName")
res.Remove("topologyVersion")
res.Remove("setVersion")
res.Remove("secondary")
res.Remove("primary")
res.Remove("me")
res.Remove("electionId")
res.Remove("lastWrite")
FixCluster(t, res)
expectedComparable := wirebson.MustDocument(
"isWritablePrimary", true,
"maxBsonObjectSize", int32(16777216),
"maxMessageSizeBytes", int32(48000000),
"maxWriteBatchSize", int32(100000),
"localTime", localTime,
"logicalSessionTimeoutMinutes", int32(30),
"connectionId", connectionID,
"minWireVersion", int32(0),
"maxWireVersion", int32(21),
"readOnly", false,
"saslSupportedMechs", saslSupportedMechs,
"speculativeAuthenticate", wirebson.MustDocument(
"conversationId", int32(1),
"done", false,
"payload", serverPayload,
),
"ok", float64(1),
)
testutil.AssertEqual(t, expectedComparable, res)
clientPayload, err = conv.Step(string(serverPayload.B))
require.NoError(t, err)
})
t.Run("SASLContinue", func(t *testing.T) {
msg := wire.MustOpMsg(
"saslContinue", int32(1),
"payload", wirebson.Binary{B: []byte(clientPayload)},
"conversationId", int32(1),
"$db", "admin",
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = must.NotFail(resBody.(*wire.OpMsg).DocumentRaw()).Decode()
require.NoError(t, err)
serverPayload, ok := res.Get("payload").(wirebson.Binary)
require.True(t, ok, "got: "+res.LogMessageIndent())
FixCluster(t, res)
expectedComparable := wirebson.MustDocument(
"conversationId", int32(1),
"done", false,
"payload", serverPayload,
"ok", float64(1),
)
testutil.AssertEqual(t, expectedComparable, res)
_, err = conv.Step(string(serverPayload.B))
require.NoError(t, err)
require.True(t, conv.Valid())
})
t.Run("SASLContinueEmpty", func(t *testing.T) {
msg := wire.MustOpMsg(
"saslContinue", int32(1),
"payload", wirebson.Binary{}, // client sends empty payload
"conversationId", int32(1),
"$db", "admin",
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = must.NotFail(resBody.(*wire.OpMsg).DocumentRaw()).Decode()
require.NoError(t, err)
serverPayload, ok := res.Get("payload").(wirebson.Binary)
require.True(t, ok, "got: "+res.LogMessageIndent())
FixCluster(t, res)
expectedComparable := wirebson.MustDocument(
"conversationId", int32(1),
"done", true,
"payload", serverPayload,
"ok", float64(1),
)
testutil.AssertEqual(t, expectedComparable, res)
})
t.Run("SASLContinueTooMany", func(t *testing.T) {
msg := wire.MustOpMsg(
"saslContinue", int32(1),
"payload", wirebson.Binary{},
"conversationId", int32(1),
"$db", "admin",
)
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, msg)
require.NoError(t, err)
var res *wirebson.Document
res, err = must.NotFail(resBody.(*wire.OpMsg).DocumentRaw()).Decode()
require.NoError(t, err)
FixCluster(t, res)
expected := wirebson.MustDocument(
"ok", float64(0),
"errmsg", "No SASL session state found",
"code", int32(17),
"codeName", "ProtocolError",
)
testutil.AssertEqual(t, expected, res)
})
}
func TestHelloOpQuerySASLSupportedMechs(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
q := wire.MustOpQuery(
"hello", int32(1),
"saslSupportedMechs", "admin.username",
"speculativeAuthenticate", wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"db", "admin",
),
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpReply).DocumentDeep()
require.NoError(t, err)
ok := res.Get("ok")
require.Equal(t, float64(1), ok)
speculativeAuthenticateV := res.Get("speculativeAuthenticate")
require.NotNil(t, speculativeAuthenticateV)
payloadAny := speculativeAuthenticateV.(*wirebson.Document).Get("payload")
connectionID := res.Get("connectionId")
assert.IsType(t, int32(0), connectionID)
res.Remove("connectionId")
localTime := res.Get("localTime")
assert.IsType(t, time.Time{}, localTime)
res.Remove("localTime")
saslSupportedMechs := res.Get("saslSupportedMechs")
require.NotNil(t, saslSupportedMechs)
mechs := saslSupportedMechs.(*wirebson.Array)
var found bool
for v := range mechs.Values() {
if v == "SCRAM-SHA-256" {
found = true
break
}
}
assert.True(t, found, "expected SCRAM-SHA-256")
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/566
res.Remove("hosts")
res.Remove("setName")
res.Remove("topologyVersion")
res.Remove("setVersion")
res.Remove("secondary")
res.Remove("primary")
res.Remove("me")
res.Remove("electionId")
res.Remove("lastWrite")
FixCluster(t, res)
expectedComparable := wirebson.MustDocument(
"isWritablePrimary", true,
"maxBsonObjectSize", int32(16777216),
"maxMessageSizeBytes", int32(48000000),
"maxWriteBatchSize", int32(100000),
"logicalSessionTimeoutMinutes", int32(30),
"minWireVersion", int32(0),
"maxWireVersion", int32(21),
"readOnly", false,
"saslSupportedMechs", saslSupportedMechs,
"speculativeAuthenticate", wirebson.MustDocument(
"conversationId", int32(1),
"done", false,
"payload", payloadAny,
),
"ok", float64(1),
)
testutil.AssertEqual(t, expectedComparable, res)
}
func TestSASLStartOpQueryErrors(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
for name, tc := range map[string]struct { //nolint:vet // test only
query *wirebson.Document
fullCollectionName string
reply *wirebson.Document
failsForDocDB string
}{
"WrongAuthDB": {
query: wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
),
fullCollectionName: "wrong-auth-db.$cmd",
reply: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Authentication failed.",
"code", int32(18),
"codeName", "AuthenticationFailed",
),
failsForDocDB: "https://github.com/hanzoai/docdb-DocumentDB/issues/932",
},
"NotAllowed$db": {
query: wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"$db", "admin",
),
fullCollectionName: "admin.$cmd",
reply: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "$db is not allowed in OP_QUERY requests",
"code", int32(40621),
"codeName", "Location40621",
),
},
"InvalidPayload": {
query: wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte("invalid")},
),
fullCollectionName: "admin.$cmd",
reply: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Authentication failed.",
"code", int32(18),
"codeName", "AuthenticationFailed",
),
},
"UnsupportedMechanism": {
query: wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "UNKNOWN-MECHANISM",
"payload", wirebson.Binary{B: []byte(payload)},
),
fullCollectionName: "admin.$cmd",
reply: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Received authentication for mechanism UNKNOWN-MECHANISM which is not enabled",
"code", int32(334),
"codeName", "MechanismUnavailable",
),
},
} {
t.Run(name, func(tt *testing.T) {
var t testing.TB = tt
if tc.failsForDocDB != "" {
t = setup.FailsForDocDB(t, tc.failsForDocDB)
}
q := must.NotFail(wire.NewOpQuery(tc.query))
q.FullCollectionName = tc.fullCollectionName
q.NumberToReturn = -1
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
var res *wirebson.Document
res, err = resBody.(*wire.OpReply).Document()
require.NoError(t, err)
FixCluster(t, res)
testutil.AssertEqual(t, tc.reply, res)
})
}
}
func TestSASLStartErrors(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, &setup.SetupOpts{
WireConn: setup.WireConnNoAuth,
})
ctx, conn := s.Ctx, s.WireConn
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
for name, tc := range map[string]struct {
msg *wirebson.Document
res *wirebson.Document
}{
"InvalidOptions": {
msg: wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"options", "invalid",
"$db", "admin",
),
res: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "BSON field 'saslStart.options' is the wrong type 'string', expected type 'object'",
"code", int32(14),
"codeName", "TypeMismatch",
),
},
"InvalidOptionsSkipEmptyExchange": {
msg: wirebson.MustDocument(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
"options", wirebson.MustDocument("skipEmptyExchange", "invalid"),
"$db", "admin",
),
res: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Authentication failed.",
"code", int32(18),
"codeName", "AuthenticationFailed",
),
},
} {
t.Run(name, func(t *testing.T) {
q := must.NotFail(wire.NewOpMsg(tc.msg))
var resBody wire.MsgBody
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
var res *wirebson.Document
res, err = must.NotFail(resBody.(*wire.OpMsg).DocumentRaw()).Decode()
require.NoError(t, err)
FixCluster(t, res)
testutil.AssertEqual(t, tc.res, res)
})
}
}
func TestSASLContinueOpQueryErrors(t *testing.T) {
t.Parallel()
s := setup.SetupWithOpts(t, nil)
ctx := s.Ctx
for name, tc := range map[string]struct { //nolint:vet // test only
query *wirebson.Document
fullCollectionName string
reply *wirebson.Document
}{
"AuthenticationError": {
query: wirebson.MustDocument(
"saslContinue", int32(1),
"conversationId", int32(1),
"payload", wirebson.Binary{},
),
fullCollectionName: "wrong-auth-db.$cmd",
reply: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "Authentication failed.",
"code", int32(18),
"codeName", "AuthenticationFailed",
),
},
"NotAllowed$db": {
query: wirebson.MustDocument(
"saslContinue", int32(1),
"conversationId", int32(1),
"payload", wirebson.Binary{},
"$db", "admin",
),
fullCollectionName: "admin.$cmd",
reply: wirebson.MustDocument(
"ok", float64(0),
"errmsg", "$db is not allowed in OP_QUERY requests",
"code", int32(40621),
"codeName", "Location40621",
),
},
} {
t.Run(name, func(t *testing.T) {
u, err := url.Parse(s.MongoDBURI)
require.NoError(t, err)
username := u.User.Username()
password, _ := u.User.Password()
client := must.NotFail(xdgscram.SHA256.NewClient(username, password, ""))
conv := client.NewConversation()
payload := must.NotFail(conv.Step(""))
q := wire.MustOpQuery(
"saslStart", int32(1),
"mechanism", "SCRAM-SHA-256",
"payload", wirebson.Binary{B: []byte(payload)},
)
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
cleanUri, _, _, _, err := wireclient.Credentials(s.MongoDBURI)
require.NoError(t, err)
conn, err := wireclient.Connect(ctx, cleanUri, testutil.Logger(t))
require.NoError(t, err)
_, resBody, err := conn.Request(ctx, q)
require.NoError(t, err)
res, err := resBody.(*wire.OpReply).Document()
require.NoError(t, err)
FixCluster(t, res)
err = res.Replace("payload", wirebson.Binary{})
require.NoError(t, err)
expectedComparable := wirebson.MustDocument(
"conversationId", int32(1),
"done", false,
"payload", wirebson.Binary{},
"ok", float64(1),
)
testutil.AssertEqual(t, expectedComparable, res)
q = must.NotFail(wire.NewOpQuery(tc.query))
q.FullCollectionName = tc.fullCollectionName
q.NumberToReturn = -1
_, resBody, err = conn.Request(ctx, q)
require.NoError(t, err)
res, err = resBody.(*wire.OpReply).Document()
require.NoError(t, err)
FixCluster(t, res)
testutil.AssertEqual(t, tc.reply, res)
})
}
}