diff --git a/Taskfile.yml b/Taskfile.yml index 6956e909b522..660bc4778754 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -317,7 +317,7 @@ tasks: -coverpkg=../... -coverprofile=integration-mongodb.txt ./... - -target-url='mongodb://127.0.0.1:47017/' + -target-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=../build/certs/client.pem&tlsCaFile=../build/certs/rootCA-cert.pem&replicaSet=rs0' -target-backend=mongodb bench-unit: diff --git a/integration/cursors/getmore_test.go b/integration/cursors/getmore_test.go index 1f1cf14c6407..b48e79638062 100644 --- a/integration/cursors/getmore_test.go +++ b/integration/cursors/getmore_test.go @@ -606,17 +606,6 @@ func TestCursorsGetMoreCommandConnection(t *testing.T) { t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/153") // do not run subtest in parallel to avoid breaking another parallel subtest - - u, err := url.Parse(s.MongoDBURI) - require.NoError(t, err) - - client2, err := mongo.Connect(ctx, options.Client().ApplyURI(u.String())) - require.NoError(t, err) - - defer client2.Disconnect(ctx) - - collection2 := client2.Database(databaseName).Collection(collectionName) - var res bson.D err = collection1.Database().RunCommand( ctx, @@ -638,15 +627,22 @@ func TestCursorsGetMoreCommandConnection(t *testing.T) { cursorID, _ := cursor.Get("id") assert.NotNil(t, cursorID) - err = collection2.Database().RunCommand( + client2, err := mongo.Connect(ctx, options.Client().ApplyURI(s.MongoDBURI)) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, client2.Disconnect(ctx)) + }) + + err = client2.Database(databaseName).RunCommand( ctx, bson.D{ {"getMore", cursorID}, - {"collection", collection2.Name()}, + {"collection", client2.Database(databaseName).Collection(collectionName).Name()}, }, ).Decode(&res) - integration.AssertMatchesCommandError(t, mongo.CommandError{Code: 50738, Name: "Location50738"}, err) + integration.AssertMatchesCommandError(t, mongo.CommandError{Code: 13, Name: "Unauthorized"}, err) }) } diff --git a/integration/cursors/tailable_sessions_test.go b/integration/cursors/tailable_sessions_test.go index 6e66862815e2..40ea4ed74a8c 100644 --- a/integration/cursors/tailable_sessions_test.go +++ b/integration/cursors/tailable_sessions_test.go @@ -15,7 +15,6 @@ package cursors import ( - "errors" "sync/atomic" "testing" @@ -88,11 +87,7 @@ func TestTailableCursorsBetweenSessions(tt *testing.T) { for i := 1; i < 50; i++ { err = db.RunCommand(ctx, getMoreCmd).Err() if err != nil { - var ce mongo.CommandError - - require.True(t, errors.As(err, &ce)) - require.Equal(t, int32(50738), ce.Code) - require.Equal(t, "Location50738", ce.Name) + integration.AssertMatchesCommandError(t, mongo.CommandError{Code: 13, Name: "Unauthorized"}, err) passed.Store(true) return diff --git a/integration/distinct_test.go b/integration/distinct_test.go index 049c7e7e83b6..272d514f2924 100644 --- a/integration/distinct_test.go +++ b/integration/distinct_test.go @@ -36,7 +36,8 @@ func TestDistinctCommandErrors(t *testing.T) { collName any // optional, defaults to coll.Name() filter any // required - err *mongo.CommandError + err *mongo.CommandError + altMessage string }{ "StringFilter": { command: "a", @@ -64,8 +65,9 @@ func TestDistinctCommandErrors(t *testing.T) { err: &mongo.CommandError{ Code: 73, Name: "InvalidNamespace", - Message: "collection name has invalid type object", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type object", }, "WrongTypeObject": { command: bson.D{}, @@ -114,7 +116,7 @@ func TestDistinctCommandErrors(t *testing.T) { err := collection.Database().RunCommand(ctx, command).Decode(res) assert.Nil(t, res) - AssertEqualCommandError(t, *tc.err, err) + AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err) }) } } diff --git a/integration/query_test.go b/integration/query_test.go index 2c5c3e5487ce..e5d3ff7b93aa 100644 --- a/integration/query_test.go +++ b/integration/query_test.go @@ -39,104 +39,117 @@ func TestQueryBadFindType(t *testing.T) { ctx, collection := s.Ctx, s.Collection for name, tc := range map[string]struct { - value any - err *mongo.CommandError + value any + err *mongo.CommandError + altMessage string }{ "Document": { value: bson.D{}, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type object", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type object", }, "Array": { value: primitive.A{}, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type array", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type array", }, "Double": { value: 3.14, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type double", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type double", }, "Binary": { value: primitive.Binary{}, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type binData", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type binData", }, "ObjectID": { value: primitive.ObjectID{}, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type objectId", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type objectId", }, "Bool": { value: true, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type bool", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type bool", }, "Date": { value: time.Now(), err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type date", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type date", }, "Null": { value: nil, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type null", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type null", }, "Regex": { value: primitive.Regex{Pattern: "/foo/"}, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type regex", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type regex", }, "Int": { value: int32(42), err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type int", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type int", }, "Timestamp": { value: primitive.Timestamp{}, err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type timestamp", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type timestamp", }, "Long": { value: int64(42), err: &mongo.CommandError{ - Code: 2, - Name: "BadValue", - Message: "collection name has invalid type long", + Code: 73, + Name: "InvalidNamespace", + Message: "Failed to parse namespace element", }, + altMessage: "collection name has invalid type long", }, } { name, tc := name, tc @@ -153,7 +166,7 @@ func TestQueryBadFindType(t *testing.T) { err := collection.Database().RunCommand(ctx, cmd).Decode(&res) require.Nil(t, res) - AssertEqualCommandError(t, *tc.err, err) + AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err) }) } } diff --git a/integration/setup/client.go b/integration/setup/client.go index 8744036ff9bc..bcec546050b3 100644 --- a/integration/setup/client.go +++ b/integration/setup/client.go @@ -16,6 +16,9 @@ package setup import ( "context" + "net/url" + "os" + "path/filepath" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/mongo" @@ -76,3 +79,41 @@ func setupClient(tb testtb.TB, ctx context.Context, uri string) *mongo.Client { return client } + +// toAbsolutePathURI replaces the relative path of tlsCertificateKeyFile and tlsCaFile path +// to an absolute path. If the file is not found because the test is in subdirectory +// such as`integration/user/`, the parent directory is looked. +func toAbsolutePathURI(tb testtb.TB, uri string) string { + u, err := url.Parse(uri) + require.NoError(tb, err) + + values := url.Values{} + + for k, v := range u.Query() { + require.Len(tb, v, 1) + + switch k { + case "tlsCertificateKeyFile", "tlsCaFile": + file := v[0] + + if filepath.IsAbs(file) { + values[k] = []string{file} + + continue + } + + file = filepath.Join(Dir(tb), v[0]) + if _, err = os.Stat(file); os.IsNotExist(err) { + file = filepath.Join(Dir(tb), "..", v[0]) + } + + values[k] = []string{file} + default: + values[k] = v + } + } + + u.RawQuery = values.Encode() + + return u.String() +} diff --git a/integration/setup/setup.go b/integration/setup/setup.go index 83970fc5fab8..1c76b22a4f9e 100644 --- a/integration/setup/setup.go +++ b/integration/setup/setup.go @@ -17,6 +17,7 @@ package setup import ( "context" + "errors" "flag" "fmt" "net/url" @@ -24,6 +25,7 @@ import ( "slices" "strings" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" @@ -32,6 +34,7 @@ import ( "go.uber.org/zap" "github.com/FerretDB/FerretDB/integration/shareddata" + "github.com/FerretDB/FerretDB/internal/handler/handlererrors" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/observability" "github.com/FerretDB/FerretDB/internal/util/testutil" @@ -89,7 +92,7 @@ type SetupOpts struct { // ExtraOptions sets the options in MongoDB URI, when the option exists it overwrites that option. ExtraOptions url.Values - // SetupUser true creates a user. + // SetupUser true creates a user and returns an authenticated client. SetupUser bool } @@ -140,6 +143,8 @@ func SetupWithOpts(tb testtb.TB, opts *SetupOpts) *SetupResult { uri := *targetURLF if uri == "" { uri = setupListener(tb, setupCtx, logger) + } else { + uri = toAbsolutePathURI(tb, *targetURLF) } if opts.ExtraOptions != nil { @@ -164,12 +169,13 @@ func SetupWithOpts(tb testtb.TB, opts *SetupOpts) *SetupResult { // register cleanup function after setupListener registers its own to preserve full logs tb.Cleanup(cancel) - collection := setupCollection(tb, setupCtx, client, opts) - if opts.SetupUser { - setupUser(tb, ctx, client) + // user is created before the collection so that user can run collection cleanup + client = setupUser(tb, ctx, client, uri) } + collection := setupCollection(tb, ctx, client, opts) + level.SetLevel(*logLevelF) return &SetupResult{ @@ -330,23 +336,60 @@ func insertBenchmarkProvider(tb testtb.TB, ctx context.Context, collection *mong return } -// setupUser creates a user in admin database with supported mechanisms. +// setupUser creates a user in admin database with supported mechanisms. It returns an authenticated client. // // Without this, once the first user is created, the authentication fails as local exception no longer applies. -func setupUser(tb testtb.TB, ctx context.Context, client *mongo.Client) { +func setupUser(tb testtb.TB, ctx context.Context, client *mongo.Client, uri string) *mongo.Client { tb.Helper() - if IsMongoDB(tb) { - return + // username is unique per test so the user is deleted after the test + username, password := "username"+tb.Name(), "password" + err := client.Database("admin").RunCommand(ctx, bson.D{ + {"dropUser", username}, + }).Err() + + var ce mongo.CommandError + if errors.As(err, &ce) && ce.Code == int32(handlererrors.ErrUserNotFound) { + err = nil } - username, password := "username", "password" + require.NoError(tb, err) - err := client.Database("admin").RunCommand(ctx, bson.D{ + roles := bson.A{"root"} + if !IsMongoDB(tb) { + // use root role for FerretDB once authorization is implemented + // TODO https://github.com/FerretDB/FerretDB/issues/3974 + roles = bson.A{} + } + + err = client.Database("admin").RunCommand(ctx, bson.D{ {"createUser", username}, - {"roles", bson.A{}}, + {"roles", roles}, {"pwd", password}, - {"mechanisms", bson.A{"PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-256"}}, + {"mechanisms", bson.A{"SCRAM-SHA-1", "SCRAM-SHA-256"}}, }).Err() - require.NoErrorf(tb, err, "cannot create user") + require.NoError(tb, err) + + credential := options.Credential{ + AuthMechanism: "SCRAM-SHA-256", + AuthSource: "admin", + Username: username, + Password: password, + } + + opts := options.Client().ApplyURI(uri).SetAuth(credential) + + authenticatedClient, err := mongo.Connect(ctx, opts) + require.NoError(tb, err) + + tb.Cleanup(func() { + err = authenticatedClient.Database("admin").RunCommand(ctx, bson.D{ + {"dropUser", username}, + }).Err() + assert.NoError(tb, err) + + require.NoError(tb, authenticatedClient.Disconnect(ctx)) + }) + + return authenticatedClient } diff --git a/integration/users/connection_test.go b/integration/users/connection_test.go index 4ef5a6cc7b57..99f3f4a96f42 100644 --- a/integration/users/connection_test.go +++ b/integration/users/connection_test.go @@ -166,9 +166,16 @@ func TestAuthentication(t *testing.T) { setup.SkipForMongoDB(t, "PLAIN mechanism is not supported by MongoDB") } + // 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/FerretDB/FerretDB/issues/3974 + roles = bson.A{} + } + createPayload := bson.D{ {"createUser", tc.username}, - {"roles", bson.A{}}, + {"roles", roles}, {"pwd", tc.password}, {"mechanisms", mechanisms}, } @@ -345,9 +352,16 @@ func TestAuthenticationLocalhostException(tt *testing.T) { db = clientNoAuth.Database(db.Name()) username, password, mechanism := "testuser", "testpass", "SCRAM-SHA-256" + + roles := bson.A{"userAdmin"} + if !setup.IsMongoDB(t) { + // TODO https://github.com/FerretDB/FerretDB/issues/3974 + roles = bson.A{} + } + firstUser := bson.D{ {"createUser", username}, - {"roles", bson.A{}}, + {"roles", roles}, {"pwd", password}, {"mechanisms", bson.A{mechanism}}, } @@ -356,7 +370,7 @@ func TestAuthenticationLocalhostException(tt *testing.T) { secondUser := bson.D{ {"createUser", "anotheruser"}, - {"roles", bson.A{}}, + {"roles", roles}, {"pwd", "anotherpass"}, {"mechanisms", bson.A{mechanism}}, } diff --git a/internal/handler/common/find.go b/internal/handler/common/find.go index 8af8f06a0dfd..ace9752043bf 100644 --- a/internal/handler/common/find.go +++ b/internal/handler/common/find.go @@ -15,8 +15,6 @@ package common import ( - "errors" - "go.uber.org/zap" "github.com/FerretDB/FerretDB/internal/handler/handlererrors" @@ -72,16 +70,7 @@ func GetFindParams(doc *types.Document, l *zap.Logger) (*FindParams, error) { BatchSize: 101, } - err := handlerparams.ExtractParams(doc, "find", ¶ms, l) - - var ce *handlererrors.CommandError - if errors.As(err, &ce) { - if ce.Code() == handlererrors.ErrInvalidNamespace { - return nil, handlererrors.NewCommandErrorMsgWithArgument(handlererrors.ErrBadValue, ce.Err().Error(), "find") - } - } - - if err != nil { + if err := handlerparams.ExtractParams(doc, "find", ¶ms, l); err != nil { return nil, err }