Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advertise SCRAM / SASL support in addition to PLAIN #4113

Merged
merged 34 commits into from Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f7b34b5
Unmark showCredentials as ignored property of usersInfo
henvic Feb 22, 2024
477ab17
wip
henvic Feb 22, 2024
2208a62
failure example
henvic Feb 23, 2024
3dfe891
Merge branch 'main' into feat/sasl-adv
henvic Feb 26, 2024
b35ac42
wip
henvic Feb 26, 2024
136ecf9
wip
henvic Feb 26, 2024
1963561
wip
henvic Feb 26, 2024
5e3e377
wip
henvic Feb 26, 2024
46bcdad
wip
henvic Feb 26, 2024
ad289de
Merge branch 'main' into feat/sasl-adv
henvic Feb 26, 2024
a0e504f
wip
henvic Feb 26, 2024
01282a9
wip
henvic Feb 26, 2024
2c834b4
wip
henvic Feb 26, 2024
78b41d7
wip
henvic Feb 27, 2024
5a77a3d
wip
henvic Feb 27, 2024
76efeee
wip
henvic Feb 27, 2024
3cc579a
Merge branch 'main' into feat/sasl-adv
henvic Feb 27, 2024
830386d
wip
henvic Feb 28, 2024
2736c4a
wip
henvic Feb 28, 2024
e769c90
wip
henvic Feb 28, 2024
87bcf66
wip
henvic Feb 28, 2024
cb336a1
Merge branch 'main' into feat/sasl-adv
henvic Mar 1, 2024
92ebe16
wip
henvic Mar 3, 2024
530e14e
Merge branch 'main' into feat/sasl-adv
henvic Mar 4, 2024
0173295
Fix `saslContinue` crashing due to not found authentication conversat…
henvic Mar 5, 2024
efd071b
code review remarks
henvic Mar 5, 2024
fae330e
wip
henvic Mar 5, 2024
22a2be4
wip
henvic Mar 5, 2024
b930a1b
update test
chilagrow Mar 6, 2024
444a39d
flatten if statements
chilagrow Mar 6, 2024
d44e886
update test
chilagrow Mar 6, 2024
76990ef
update test to not panic for failForMongoDB
chilagrow Mar 6, 2024
586cb05
Merge branch 'main' into feat/sasl-adv
chilagrow Mar 6, 2024
ceadf72
Merge branch 'main' into feat/sasl-adv
AlekSi Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/commands_administration_test.go
Expand Up @@ -858,7 +858,7 @@ func TestGetParameterCommandAuthenticationMechanisms(t *testing.T) {
require.NoError(t, err)

expected := bson.D{
{"authenticationMechanisms", bson.A{"PLAIN"}},
{"authenticationMechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256", "PLAIN"}},
{"ok", float64(1)},
}
require.Equal(t, expected, res)
Expand Down
179 changes: 179 additions & 0 deletions integration/hello_command_test.go
@@ -0,0 +1,179 @@
// Copyright 2021 FerretDB 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 (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"

"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/must"
)

func TestHello(t *testing.T) {
t.Parallel()

ctx, collection := setup.Setup(t, shareddata.Scalars, shareddata.Composites)
db := collection.Database()

var res bson.D

require.NoError(t, db.RunCommand(ctx, bson.D{
{"hello", "1"},
}).Decode(&res))

actual := ConvertDocument(t, res)

assert.Equal(t, must.NotFail(actual.Get("isWritablePrimary")), true)
assert.Equal(t, must.NotFail(actual.Get("maxBsonObjectSize")), int32(16777216))
assert.Equal(t, must.NotFail(actual.Get("maxMessageSizeBytes")), int32(48000000))
assert.Equal(t, must.NotFail(actual.Get("maxMessageSizeBytes")), int32(48000000))
assert.Equal(t, must.NotFail(actual.Get("maxWriteBatchSize")), int32(100000))
assert.IsType(t, must.NotFail(actual.Get("localTime")), time.Time{})
assert.IsType(t, must.NotFail(actual.Get("connectionId")), int32(1))
assert.Equal(t, must.NotFail(actual.Get("minWireVersion")), int32(0))
assert.Equal(t, must.NotFail(actual.Get("maxWireVersion")), int32(21))
assert.Equal(t, must.NotFail(actual.Get("readOnly")), false)
assert.Equal(t, must.NotFail(actual.Get("ok")), float64(1))
}

func TestHelloWithSupportedMechs(t *testing.T) {
t.Parallel()

ctx, collection := setup.Setup(t, shareddata.Scalars, shareddata.Composites)
db := collection.Database()

usersPayload := []bson.D{
{
{"createUser", "hello_user"},
{"roles", bson.A{}},
{"pwd", "hello_password"},
},
{
{"createUser", "hello_user_scram1"},
{"roles", bson.A{}},
{"pwd", "hello_password"},
{"mechanisms", bson.A{"SCRAM-SHA-1"}},
},
{
{"createUser", "hello_user_scram256"},
{"roles", bson.A{}},
{"pwd", "hello_password"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
}

if !setup.IsMongoDB(t) {
usersPayload = append(usersPayload, primitive.D{
{"createUser", "hello_user_plain"},
{"roles", bson.A{}},
{"pwd", "hello_password"},
{"mechanisms", bson.A{"PLAIN"}},
})
}

for _, u := range usersPayload {
require.NoError(t, db.RunCommand(ctx, u).Err())
}

testCases := map[string]struct { //nolint:vet // used for test only
user string
mechs *types.Array
err string
}{
"NotFound": {
user: db.Name() + ".not_found",
},
"AnotherDB": {
user: db.Name() + "_not_found.another_db",
},
"HelloUser": {
user: db.Name() + ".hello_user",
mechs: must.NotFail(types.NewArray("SCRAM-SHA-1", "SCRAM-SHA-256")),
},
"HelloUserPlain": {
user: db.Name() + ".hello_user_plain",
mechs: must.NotFail(types.NewArray("PLAIN")),
},
"HelloUserSCRAM1": {
user: db.Name() + ".hello_user_scram1",
mechs: must.NotFail(types.NewArray("SCRAM-SHA-1")),
},
"HelloUserSCRAM256": {
user: db.Name() + ".hello_user_scram256",
mechs: must.NotFail(types.NewArray("SCRAM-SHA-256")),
},
"EmptyUsername": {
user: db.Name() + ".",
mechs: nil,
},
"MissingSeparator": {
user: db.Name(),
err: "UserName must contain a '.' separated database.user pair",
},
}

for name, tc := range testCases {
tc, name := tc, name
AlekSi marked this conversation as resolved.
Show resolved Hide resolved

t.Run(name, func(t *testing.T) {
t.Parallel()

var res bson.D

if tc.mechs != nil && tc.mechs.Contains("PLAIN") {
setup.SkipForMongoDB(t, "PLAIN authentication mechanism is not support by MongoDB")
AlekSi marked this conversation as resolved.
Show resolved Hide resolved
}

err := db.RunCommand(ctx, bson.D{
{"hello", "1"},
{"saslSupportedMechs", tc.user},
}).Decode(&res)

if tc.err != "" {
require.ErrorContains(t, err, tc.err)
chilagrow marked this conversation as resolved.
Show resolved Hide resolved
return
}

actual := ConvertDocument(t, res)

if tc.mechs != nil {
mechanisms := must.NotFail(actual.Get("saslSupportedMechs"))
assert.True(t, mechanisms.(*types.Array).ContainsAll(tc.mechs))
} else {
assert.False(t, actual.Has("saslSupportedMechs"))
}
AlekSi marked this conversation as resolved.
Show resolved Hide resolved

assert.Equal(t, must.NotFail(actual.Get("isWritablePrimary")), true)
assert.Equal(t, must.NotFail(actual.Get("maxBsonObjectSize")), int32(16777216))
assert.Equal(t, must.NotFail(actual.Get("maxMessageSizeBytes")), int32(48000000))
assert.Equal(t, must.NotFail(actual.Get("maxMessageSizeBytes")), int32(48000000))
assert.Equal(t, must.NotFail(actual.Get("maxWriteBatchSize")), int32(100000))
assert.IsType(t, must.NotFail(actual.Get("localTime")), time.Time{})
assert.IsType(t, must.NotFail(actual.Get("connectionId")), int32(1))
assert.Equal(t, must.NotFail(actual.Get("minWireVersion")), int32(0))
assert.Equal(t, must.NotFail(actual.Get("maxWireVersion")), int32(21))
assert.Equal(t, must.NotFail(actual.Get("readOnly")), false)
assert.Equal(t, must.NotFail(actual.Get("ok")), float64(1))
})
}
}
129 changes: 100 additions & 29 deletions integration/users/usersinfo_test.go
Expand Up @@ -87,6 +87,28 @@ func TestUsersinfo(t *testing.T) {
},
},
},
{
dbSuffix: "allbackends",
payloads: []bson.D{
{
{"createUser", "WithSCRAMSHA1"},
{"roles", bson.A{}},
{"pwd", "pwd1"},
{"mechanisms", bson.A{"SCRAM-SHA-1"}},
},
},
},
{
dbSuffix: "allbackends",
payloads: []bson.D{
{
{"createUser", "WithSCRAMSHA256"},
{"roles", bson.A{}},
{"pwd", "pwd1"},
{"mechanisms", bson.A{"SCRAM-SHA-256"}},
},
},
},
}

dbPrefix := testutil.DatabaseName(t)
Expand Down Expand Up @@ -117,13 +139,15 @@ func TestUsersinfo(t *testing.T) {
}

testCases := map[string]struct { //nolint:vet // for readability
dbSuffix string
payload bson.D
err *mongo.CommandError
altMessage string
expected bson.D
hasUser map[string]struct{}
skipForMongoDB string // optional, skip test for MongoDB backend with a specific reason
dbSuffix string
payload bson.D
err *mongo.CommandError
altMessage string
expected bson.D
hasUser map[string]struct{}

showCredentials []string // showCredentials list the credentials types expected to be returned
skipForMongoDB string // optional, skip test for MongoDB backend with a specific reason
chilagrow marked this conversation as resolved.
Show resolved Hide resolved
}{
"NoUserFound": {
dbSuffix: "no_users",
Expand Down Expand Up @@ -182,6 +206,7 @@ func TestUsersinfo(t *testing.T) {
{"usersInfo", "WithPLAIN"},
{"showCredentials", true},
},
showCredentials: []string{"PLAIN"},
expected: bson.D{
{"users", bson.A{
bson.D{
Expand All @@ -195,6 +220,44 @@ func TestUsersinfo(t *testing.T) {
},
skipForMongoDB: "Only MongoDB Enterprise offers PLAIN",
},
"WithSCRAMSHA1": {
dbSuffix: "allbackends",
payload: bson.D{
{"usersInfo", "WithSCRAMSHA1"},
{"showCredentials", true},
},
showCredentials: []string{"SCRAM-SHA-1"},
expected: bson.D{
{"users", bson.A{
bson.D{
{"_id", "TestUsersinfo.WithSCRAMSHA1"},
{"user", "scramsha1"},
{"db", "TestUsersinfo"},
{"roles", bson.A{}},
},
}},
{"ok", float64(1)},
},
},
"WithSCRAMSHA256": {
dbSuffix: "allbackends",
payload: bson.D{
{"usersInfo", "WithSCRAMSHA256"},
{"showCredentials", true},
},
showCredentials: []string{"SCRAM-SHA-256"},
expected: bson.D{
{"users", bson.A{
bson.D{
{"_id", "TestUsersinfo.WithSCRAMSHA256"},
{"user", "scramsha256"},
{"db", "TestUsersinfo"},
{"roles", bson.A{}},
},
}},
{"ok", float64(1)},
},
},
"FromSameDatabase": {
dbSuffix: "_example",
payload: bson.D{{
Expand Down Expand Up @@ -513,35 +576,27 @@ func TestUsersinfo(t *testing.T) {
require.True(t, (tc.hasUser == nil) != (tc.expected == nil))

payload := integration.ConvertDocument(t, tc.payload)
var showCredentials bool

if payload.Has("showCredentials") {
showCredentials = must.NotFail(payload.Get("showCredentials")).(bool)
}
if showCredentials {
if !setup.IsMongoDB(t) {
cred, ok := actualUser.Get("credentials")
assert.Nil(t, ok, "credentials not found")
assertPlainCredentials(t, "PLAIN", cred.(*types.Document))
if tc.showCredentials != nil {
cred, ok := actualUser.Get("credentials")
assert.Nil(t, ok, "credentials not found")
AlekSi marked this conversation as resolved.
Show resolved Hide resolved

for _, typ := range tc.showCredentials {
switch typ {
case "PLAIN":
assertPlainCredentials(t, "PLAIN", cred.(*types.Document))
case "SCRAM-SHA-1":
assertSCRAMSHA1Credentials(t, "SCRAM-SHA-1", cred.(*types.Document))
case "SCRAM-SHA-256":
assertSCRAMSHA256Credentials(t, "SCRAM-SHA-256", cred.(*types.Document))
}
}
} else {
AlekSi marked this conversation as resolved.
Show resolved Hide resolved
assert.False(t, actualUser.Has("credentials"))
}

if payload.Has("mechanisms") {
payloadMechanisms := must.NotFail(payload.Get("mechanisms")).(*types.Array)

if payloadMechanisms.Contains("PLAIN") {
assertPlainCredentials(t, "PLAIN", must.NotFail(actualUser.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-1") {
assertSCRAMSHA1Credentials(t, "SCRAM-SHA-1", must.NotFail(actualUser.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-256") {
assertSCRAMSHA256Credentials(t, "SCRAM-SHA-256", must.NotFail(actualUser.Get("credentials")).(*types.Document))
}
assertUsersinfoMechanisms(t, payload, actualUser)
}

foundUsers[must.NotFail(actualUser.Get("_id")).(string)] = struct{}{}
Expand All @@ -568,3 +623,19 @@ func TestUsersinfo(t *testing.T) {
})
}
}

func assertUsersinfoMechanisms(t *testing.T, payload, user *types.Document) {
AlekSi marked this conversation as resolved.
Show resolved Hide resolved
payloadMechanisms := must.NotFail(payload.Get("mechanisms")).(*types.Array)

if payloadMechanisms.Contains("PLAIN") {
assertPlainCredentials(t, "PLAIN", must.NotFail(user.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-1") {
assertSCRAMSHA1Credentials(t, "SCRAM-SHA-1", must.NotFail(user.Get("credentials")).(*types.Document))
}

if payloadMechanisms.Contains("SCRAM-SHA-256") {
assertSCRAMSHA256Credentials(t, "SCRAM-SHA-256", must.NotFail(user.Get("credentials")).(*types.Document))
}
}
2 changes: 1 addition & 1 deletion internal/handler/msg_getparameter.go
Expand Up @@ -52,7 +52,7 @@ func (h *Handler) MsgGetParameter(ctx context.Context, msg *wire.OpMsg) (*wire.O
// "settableAtStartup", <bool>,
//)),
"authenticationMechanisms", must.NotFail(types.NewDocument(
"value", must.NotFail(types.NewArray("PLAIN")),
"value", must.NotFail(types.NewArray("SCRAM-SHA-1", "SCRAM-SHA-256", "PLAIN")),
"settableAtRuntime", false,
"settableAtStartup", true,
)),
Expand Down