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 14 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
203 changes: 203 additions & 0 deletions integration/hello_command_test.go
@@ -0,0 +1,203 @@
// 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"

"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)

keys := []string{
"isWritablePrimary",
"maxBsonObjectSize",
"maxMessageSizeBytes",
"maxWriteBatchSize",
"localTime",
"connectionId",
"minWireVersion",
"maxWireVersion",
"readOnly",
"ok",
}

if !setup.IsMongoDB(t) {
assert.Equal(t, actual.Keys(), keys)
} else {
// MongoDB have additional keys,
// so just check if the keys exists on it.
for _, wantKey := range keys {
assert.Contains(t, actual.Keys(), wantKey)
}
}
}
henvic marked this conversation as resolved.
Show resolved Hide resolved

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 := []struct { //nolint:vet // used for test only
username string
db string
mechs *types.Array
err bool
}{
{
username: "not_found",
db: db.Name(),
},
{
username: "another_db",
db: db.Name() + "_not_found",
},
{
username: "hello_user",
db: db.Name(),
mechs: must.NotFail(types.NewArray("SCRAM-SHA-1", "SCRAM-SHA-256")),
},
{
username: "hello_user_plain",
db: db.Name(),
mechs: must.NotFail(types.NewArray("PLAIN")),
},
{
username: "hello_user_scram1",
db: db.Name(),
mechs: must.NotFail(types.NewArray("SCRAM-SHA-1")),
},
{
username: "hello_user_scram256",
db: db.Name(),
mechs: must.NotFail(types.NewArray("SCRAM-SHA-256")),
},
}

for _, tc := range testCases {
tc := tc

t.Run(tc.username, 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.db + "." + tc.username},
}).Decode(&res)

if tc.err {
require.Error(t, err)
}

actual := ConvertDocument(t, res)

keys := []string{
"isWritablePrimary",
"maxBsonObjectSize",
"maxMessageSizeBytes",
"maxWriteBatchSize",
"localTime",
"connectionId",
"minWireVersion",
"maxWireVersion",
"readOnly",
}

if tc.mechs != nil {
keys = append(keys, "saslSupportedMechs")
mechanisms := must.NotFail(actual.Get("saslSupportedMechs"))

if !setup.IsMongoDB(t) {
chilagrow marked this conversation as resolved.
Show resolved Hide resolved
assert.Equal(t, tc.mechs, mechanisms)
}
} else {
assert.False(t, actual.Has("saslSupportedMechs"))
}

keys = append(keys, "ok")

if !setup.IsMongoDB(t) {
assert.Equal(t, actual.Keys(), keys)
} else {
// MongoDB have additional keys,
// so just check if the keys exists on it.
for _, wantKey := range keys {
assert.Contains(t, actual.Keys(), wantKey)
}
}
})
}
}
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
126 changes: 112 additions & 14 deletions internal/handler/msg_hello.go
Expand Up @@ -16,10 +16,14 @@

import (
"context"
"errors"
"strings"
"time"

"github.com/FerretDB/FerretDB/internal/handler/common"
"github.com/FerretDB/FerretDB/internal/handler/handlererrors"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/iterator"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/wire"
Expand All @@ -36,21 +40,115 @@
return nil, lazyerrors.Error(err)
}

saslSupportedMechs, err := common.GetOptionalParam(doc, "saslSupportedMechs", "")
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 45 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L45

Added line #L45 was not covered by tests
}

var saslSupportedMechsResp *types.Array

if saslSupportedMechs != "" {
chilagrow marked this conversation as resolved.
Show resolved Hide resolved
db, username, ok := strings.Cut(saslSupportedMechs, ".")
if !ok {
return nil, handlererrors.NewCommandErrorMsg(
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
handlererrors.ErrBadValue,
"UserName must contain a '.' separated database.user pair",
)

Check warning on line 56 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L53-L56

Added lines #L53 - L56 were not covered by tests
}
// If username is empty, MongoDB doesn't send back a saslSupportedMechs property but
henvic marked this conversation as resolved.
Show resolved Hide resolved
// still sends the response normally.
if username != "" {
mechs, err := h.getUserSupportedMechs(ctx, db, username)
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 63 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L63

Added line #L63 was not covered by tests
}

if len(mechs) > 0 {
saslSupportedMechsResp = must.NotFail(types.NewArray())
for _, k := range mechs {
saslSupportedMechsResp.Append(k)
}
}
}
}

resp := must.NotFail(types.NewDocument(
"isWritablePrimary", true,
"maxBsonObjectSize", int32(types.MaxDocumentLen),
"maxMessageSizeBytes", int32(wire.MaxMsgLen),
"maxWriteBatchSize", int32(100000),
"localTime", time.Now(),
"connectionId", int32(42),
"minWireVersion", common.MinWireVersion,
"maxWireVersion", common.MaxWireVersion,
"readOnly", false,
))

if saslSupportedMechsResp != nil {
resp.Set("saslSupportedMechs", saslSupportedMechsResp)
}

resp.Set("ok", float64(1))

var reply wire.OpMsg
must.NoError(reply.SetSections(wire.MakeOpMsgSection(
must.NotFail(types.NewDocument(
"isWritablePrimary", true,
"maxBsonObjectSize", int32(types.MaxDocumentLen),
"maxMessageSizeBytes", int32(wire.MaxMsgLen),
"maxWriteBatchSize", int32(100000),
"localTime", time.Now(),
"connectionId", int32(42),
"minWireVersion", common.MinWireVersion,
"maxWireVersion", common.MaxWireVersion,
"readOnly", false,
"ok", float64(1),
)),
)))
must.NoError(reply.SetSections(wire.MakeOpMsgSection(resp)))

return &reply, nil
}

// getUserSupportedMechs for a given user.
func (h *Handler) getUserSupportedMechs(ctx context.Context, db, username string) ([]string, error) {
adminDB, err := h.b.Database("admin")
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 103 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L103

Added line #L103 was not covered by tests
}

usersCol, err := adminDB.Collection("system.users")
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 108 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L108

Added line #L108 was not covered by tests
}

filter, err := usersInfoFilter(false, false, db, []usersInfoPair{
{username: username, db: db},
})
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 115 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L115

Added line #L115 was not covered by tests
}

// Filter isn't being passed to the query as we are filtering after retrieving all data
// from the database due to limitations of the internal/backends filters.
henvic marked this conversation as resolved.
Show resolved Hide resolved
qr, err := usersCol.Query(ctx, nil)
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 122 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L122

Added line #L122 was not covered by tests
}

defer qr.Iter.Close()

for {
_, v, err := qr.Iter.Next()

if errors.Is(err, iterator.ErrIteratorDone) {
break
}

if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 135 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L135

Added line #L135 was not covered by tests
}

matches, err := common.FilterDocument(v, filter)
if err != nil {
return nil, lazyerrors.Error(err)

Check warning on line 140 in internal/handler/msg_hello.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_hello.go#L140

Added line #L140 was not covered by tests
}

if !matches {
continue
}

if v.Has("credentials") {
credentials := must.NotFail(v.Get("credentials")).(*types.Document)
return credentials.Keys(), nil
}
}

return nil, nil
}
2 changes: 1 addition & 1 deletion internal/handler/msg_usersinfo.go
Expand Up @@ -59,7 +59,7 @@ func (h *Handler) MsgUsersInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMs

common.Ignored(
document, h.L,
"showCredentials", "showCustomData", "showPrivileges",
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
"showCustomData", "showPrivileges",
"showAuthenticationRestrictions", "comment", "filter",
)

Expand Down