diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1563be83c078..38c6e5da529a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -183,7 +183,8 @@ you can run those with `task test-unit` after starting the environment as descri We also have a set of "integration" tests in the `integration` directory. They use the Go MongoDB driver like a regular user application. -They could test any MongoDB-compatible database (such as FerretDB or MongoDB itself) via a regular TCP or TLS port or Unix socket. +They could test any MongoDB-compatible database (such as FerretDB or MongoDB itself) via a regular TCP or TLS port +or Unix domain socket. They also could test in-process FerretDB instances (meaning that integration tests start and stop them themselves) with a given backend. Finally, some integration tests (so-called compatibility or "compat" tests) connect to two systems diff --git a/ferretdb/ferretdb.go b/ferretdb/ferretdb.go index 9e8421604f12..08b1274b8f54 100644 --- a/ferretdb/ferretdb.go +++ b/ferretdb/ferretdb.go @@ -214,7 +214,7 @@ func (f *FerretDB) MongoDBURI() string { Path: "/", } case f.config.Listener.Unix != "": - // MongoDB really wants Unix socket path in the host part of the URI + // MongoDB really wants Unix domain socket path in the host part of the URI u = &url.URL{ Scheme: "mongodb", Host: f.l.UnixAddr().String(), diff --git a/integration/commands_authentication_test.go b/integration/commands_authentication_test.go index e8f0c885182f..b5f4bc7bc6a6 100644 --- a/integration/commands_authentication_test.go +++ b/integration/commands_authentication_test.go @@ -17,23 +17,36 @@ 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/mongo" + "go.mongodb.org/mongo-driver/mongo/options" "github.com/FerretDB/FerretDB/integration/setup" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/util/testutil" ) func TestCommandsAuthenticationLogout(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) - db := collection.Database() + s := setup.SetupWithOpts(t, nil) + ctx, db := s.Ctx, s.Collection.Database() + + opts := options.Client().ApplyURI(s.MongoDBURI) + client, err := mongo.Connect(ctx, opts) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, client.Disconnect(ctx)) + }) + + db = client.Database(db.Name()) - // the test user logs out var res bson.D - err := db.RunCommand(ctx, bson.D{{"logout", 1}}).Decode(&res) - assert.NoError(t, err) + err = db.RunCommand(ctx, bson.D{{"logout", 1}}).Decode(&res) + require.NoError(t, err) actual := ConvertDocument(t, res) actual.Remove("$clusterTime") @@ -44,7 +57,7 @@ func TestCommandsAuthenticationLogout(t *testing.T) { // the test user logs out again, it has no effect err = db.RunCommand(ctx, bson.D{{"logout", 1}}).Decode(&res) - assert.NoError(t, err) + require.NoError(t, err) actual = ConvertDocument(t, res) actual.Remove("$clusterTime") @@ -53,47 +66,73 @@ func TestCommandsAuthenticationLogout(t *testing.T) { testutil.AssertEqual(t, expected, actual) } -func TestCommandsAuthenticationLogoutTLS(t *testing.T) { - setup.SkipForMongoDB(t, "tls is not enabled for mongodb backend") - +func TestCommandsAuthenticationLogoutAuthenticatedUser(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) - db := collection.Database() - - // the test user is authenticated - expectedAuthenticated := bson.D{ - { - "authInfo", bson.D{ - {"authenticatedUsers", bson.A{bson.D{{"user", "username"}}}}, - {"authenticatedUserRoles", bson.A{}}, - {"authenticatedUserPrivileges", bson.A{}}, - }, - }, - {"ok", float64(1)}, + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx, db := s.Ctx, s.Collection.Database() + username, password, mechanism := "testuser", "testpass", "SCRAM-SHA-256" + + 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) - assert.NoError(t, err) - assert.Equal(t, expectedAuthenticated, res) + err = db.RunCommand(ctx, bson.D{{"connectionStatus", 1}}).Decode(&res) + require.NoError(t, err) + + actualAuth, err := ConvertDocument(t, res).Get("authInfo") + require.NoError(t, err) + + actualUsersV, err := actualAuth.(*types.Document).Get("authenticatedUsers") + require.NoError(t, err) + + actualUsers := actualUsersV.(*types.Array) + require.Equal(t, 1, actualUsers.Len()) + + actualUser := must.NotFail(actualUsers.Get(0)).(*types.Document) + user, err := actualUser.Get("user") + require.NoError(t, err) + require.Equal(t, username, user) - // the test user logs out err = db.RunCommand(ctx, bson.D{{"logout", 1}}).Decode(&res) - assert.NoError(t, err) - assert.Equal(t, bson.D{{"ok", float64(1)}}, res) - - // the test user is no longer authenticated - expectedUnauthenticated := bson.D{ - { - "authInfo", bson.D{ - {"authenticatedUsers", bson.A{}}, - {"authenticatedUserRoles", bson.A{}}, - {"authenticatedUserPrivileges", bson.A{}}, - }, - }, - {"ok", float64(1)}, - } + require.NoError(t, err) + + actual := ConvertDocument(t, res) + actual.Remove("$clusterTime") + actual.Remove("operationTime") + + expected := ConvertDocument(t, bson.D{{"ok", float64(1)}}) + testutil.AssertEqual(t, expected, actual) + err = db.RunCommand(ctx, bson.D{{"connectionStatus", 1}}).Decode(&res) - assert.NoError(t, err) - assert.Equal(t, expectedUnauthenticated, res) + require.NoError(t, err) + + actualAuth, err = ConvertDocument(t, res).Get("authInfo") + require.NoError(t, err) + + actualUsersV, err = actualAuth.(*types.Document).Get("authenticatedUsers") + require.NoError(t, err) + require.Empty(t, actualUsersV) } diff --git a/integration/commands_diagnostic_test.go b/integration/commands_diagnostic_test.go index 3c46e8bfe242..5c78689a8d06 100644 --- a/integration/commands_diagnostic_test.go +++ b/integration/commands_diagnostic_test.go @@ -403,7 +403,7 @@ func TestCommandsDiagnosticWhatsMyURI(t *testing.T) { databaseName := s.Collection.Database().Name() collectionName := s.Collection.Name() - // only check port number on TCP connection, no need to check on Unix socket + // only check port number on TCP connection, no need to check on Unix domain socket isUnix := s.IsUnixSocket(t) // setup second client connection to check that `whatsmyuri` returns different ports diff --git a/integration/hello_command_test.go b/integration/hello_command_test.go index 081e55d73c69..1948cf0defe9 100644 --- a/integration/hello_command_test.go +++ b/integration/hello_command_test.go @@ -61,8 +61,11 @@ func TestHello(t *testing.T) { func TestHelloWithSupportedMechs(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t, shareddata.Scalars, shareddata.Composites) - db := collection.Database() + s := setup.SetupWithOpts(t, &setup.SetupOpts{ + SetupUser: true, + Providers: []shareddata.Provider{shareddata.Scalars, shareddata.Composites}, + }) + ctx, db := s.Ctx, s.Collection.Database() usersPayload := []bson.D{ { diff --git a/integration/setup/setup.go b/integration/setup/setup.go index 8cbd81b06098..83970fc5fab8 100644 --- a/integration/setup/setup.go +++ b/integration/setup/setup.go @@ -45,7 +45,7 @@ var ( targetProxyAddrF = flag.String("target-proxy-addr", "", "in-process FerretDB: use given proxy") targetTLSF = flag.Bool("target-tls", false, "in-process FerretDB: use TLS") - targetUnixSocketF = flag.Bool("target-unix-socket", false, "in-process FerretDB: use Unix socket") + targetUnixSocketF = flag.Bool("target-unix-socket", false, "in-process FerretDB: use Unix domain socket") postgreSQLURLF = flag.String("postgresql-url", "", "in-process FerretDB: PostgreSQL URL for 'postgresql' handler.") sqliteURLF = flag.String("sqlite-url", "", "in-process FerretDB: SQLite URI for 'sqlite' handler.") @@ -88,6 +88,9 @@ 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 bool } // SetupResult represents setup results. @@ -97,12 +100,12 @@ type SetupResult struct { MongoDBURI string } -// IsUnixSocket returns true if MongoDB URI is a Unix socket. +// IsUnixSocket returns true if MongoDB URI is a Unix domain socket. func (s *SetupResult) IsUnixSocket(tb testtb.TB) bool { tb.Helper() // we can't use a regular url.Parse because - // MongoDB really wants Unix socket path in the host part of the URI + // MongoDB really wants Unix domain socket path in the host part of the URI opts := options.Client().ApplyURI(s.MongoDBURI) res := slices.ContainsFunc(opts.Hosts, func(host string) bool { return strings.Contains(host, "/") @@ -163,7 +166,9 @@ func SetupWithOpts(tb testtb.TB, opts *SetupOpts) *SetupResult { collection := setupCollection(tb, setupCtx, client, opts) - setupUser(tb, setupCtx, client) + if opts.SetupUser { + setupUser(tb, ctx, client) + } level.SetLevel(*logLevelF) @@ -326,11 +331,8 @@ func insertBenchmarkProvider(tb testtb.TB, ctx context.Context, collection *mong } // setupUser creates a user in admin database with supported mechanisms. -// The user uses username/password credential which is the same as the database -// credentials. This is done to avoid the need to reconnect as different credential. // -// Without this, once the first user is created, the authentication fails -// as username/password does not exist in admin.system.users collection. +// 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) { tb.Helper() diff --git a/integration/users/connection_test.go b/integration/users/connection_test.go index 55dd04fe92d2..14cb513deb89 100644 --- a/integration/users/connection_test.go +++ b/integration/users/connection_test.go @@ -28,13 +28,16 @@ import ( "github.com/FerretDB/FerretDB/integration" "github.com/FerretDB/FerretDB/integration/setup" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/util/testutil" "github.com/FerretDB/FerretDB/internal/util/testutil/testtb" ) func TestAuthentication(t *testing.T) { t.Parallel() - s := setup.SetupWithOpts(t, nil) + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) ctx := s.Ctx collection := s.Collection db := collection.Database() @@ -204,7 +207,7 @@ func TestAuthentication(t *testing.T) { opts := options.Client().ApplyURI(s.MongoDBURI).SetAuth(credential) client, err := mongo.Connect(ctx, opts) - require.NoError(t, err, "cannot connect to MongoDB") + require.NoError(t, err) // Ping to force connection to be established and tested. err = client.Ping(ctx, nil) @@ -219,7 +222,7 @@ func TestAuthentication(t *testing.T) { return } - require.NoError(t, err, "cannot ping MongoDB") + require.NoError(t, err) connCollection := client.Database(db.Name()).Collection(collection.Name()) @@ -240,110 +243,157 @@ func TestAuthentication(t *testing.T) { } } -// TestAuthenticationEnableNewAuthNoUser tests that the backend authentication -// is used when there is no user in the database. This ensures that there is -// some form of authentication even if there is no user. -func TestAuthenticationEnableNewAuthNoUserExists(t *testing.T) { +func TestAuthenticationOnAuthenticatedConnection(t *testing.T) { t.Parallel() - s := setup.SetupWithOpts(t, nil) - ctx := s.Ctx - collection := s.Collection - db := collection.Database() + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx, db := s.Ctx, s.Collection.Database() + username, password, mechanism := "testuser", "testpass", "SCRAM-SHA-256" - if !setup.IsMongoDB(t) { - // drop the user created in the setup - err := db.Client().Database("admin").RunCommand(ctx, bson.D{ - {"dropUser", "username"}, - }).Err() - require.NoError(t, err, "cannot drop user") + 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, } - testCases := map[string]struct { - username string - password string - mechanism string + opts := options.Client().ApplyURI(s.MongoDBURI).SetAuth(credential) - pingErr string - insertErr string - }{ - "PLAINNonExistingUser": { - username: "plain-user", - password: "whatever", - mechanism: "PLAIN", - insertErr: `role "plain-user" does not exist`, - }, - "PLAINBackendUser": { - username: "username", - password: "password", - mechanism: "PLAIN", - }, - "SHA256NonExistingUser": { - username: "sha256-user", - password: "whatever", - mechanism: "SCRAM-SHA-256", - pingErr: "Authentication failed", - }, + 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) + + actualAuth, err := integration.ConvertDocument(t, res).Get("authInfo") + require.NoError(t, err) + + actualUsersV, err := actualAuth.(*types.Document).Get("authenticatedUsers") + require.NoError(t, err) + + actualUsers := actualUsersV.(*types.Array) + require.Equal(t, 1, actualUsers.Len()) + + actualUser := must.NotFail(actualUsers.Get(0)).(*types.Document) + user, err := actualUser.Get("user") + require.NoError(t, err) + require.Equal(t, username, user) + + 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) - for name, tc := range testCases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - if tc.mechanism == "PLAIN" { - setup.SkipForMongoDB(t, "PLAIN mechanism is not supported by MongoDB") - } + err = db.RunCommand(ctx, bson.D{{"connectionStatus", 1}}).Decode(&res) + require.NoError(t, err) - t.Parallel() + actualAuth, err = integration.ConvertDocument(t, res).Get("authInfo") + require.NoError(t, err) - credential := options.Credential{ - AuthMechanism: tc.mechanism, - AuthSource: db.Name(), - Username: tc.username, - Password: tc.password, - } + actualUsersV, err = actualAuth.(*types.Document).Get("authenticatedUsers") + require.NoError(t, err) - opts := options.Client().ApplyURI(s.MongoDBURI).SetAuth(credential) + actualUsers = actualUsersV.(*types.Array) + require.Equal(t, 1, actualUsers.Len()) - client, err := mongo.Connect(ctx, opts) - require.NoError(t, err, "cannot connect to MongoDB") + actualUser = must.NotFail(actualUsers.Get(0)).(*types.Document) + user, err = actualUser.Get("user") + require.NoError(t, err) + require.Equal(t, username, user) - t.Cleanup(func() { - require.NoError(t, client.Disconnect(ctx)) - }) + err = db.RunCommand(ctx, saslStart).Decode(&res) + require.NoError(t, err) +} - err = client.Ping(ctx, nil) +func TestAuthenticationLocalhostException(tt *testing.T) { + tt.Parallel() - if tc.pingErr != "" { - require.ErrorContains(t, err, tc.pingErr) - return - } + t := setup.FailsForMongoDB(tt, "MongoDB is not connected via localhost") - require.NoError(t, err, "cannot ping MongoDB") + s := setup.SetupWithOpts(t, &setup.SetupOpts{CollectionName: testutil.CollectionName(t)}) + ctx := s.Ctx + collection := s.Collection + db := collection.Database() - connCollection := client.Database(db.Name()).Collection(collection.Name()) - _, err = connCollection.InsertOne(ctx, bson.D{{"ping", "pong"}}) + opts := options.Client().ApplyURI(s.MongoDBURI) + clientNoAuth, err := mongo.Connect(ctx, opts) + require.NoError(t, err) - if tc.insertErr != "" { - if setup.IsSQLite(t) { - t.Skip("SQLite does not have backend authentication") - } + t.Cleanup(func() { + require.NoError(t, clientNoAuth.Disconnect(ctx)) + }) - require.ErrorContains(t, err, tc.insertErr) + db = clientNoAuth.Database(db.Name()) - return - } + username, password, mechanism := "testuser", "testpass", "SCRAM-SHA-256" + firstUser := bson.D{ + {"createUser", username}, + {"roles", bson.A{}}, + {"pwd", password}, + {"mechanisms", bson.A{mechanism}}, + } + err = db.RunCommand(ctx, firstUser).Err() + require.NoError(t, err, "cannot create user") - require.NoError(t, err, "cannot insert document") - }) + secondUser := bson.D{ + {"createUser", "anotheruser"}, + {"roles", bson.A{}}, + {"pwd", "anotherpass"}, + {"mechanisms", bson.A{mechanism}}, + } + err = db.RunCommand(ctx, secondUser).Err() + integration.AssertEqualCommandError(t, mongo.CommandError{ + Code: 18, + Name: "AuthenticationFailed", + Message: "Authentication failed", + }, err) + + 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()) + err = db.RunCommand(ctx, secondUser).Err() + require.NoError(t, err, "cannot create user") } -func TestAuthenticationEnableNewAuthPLAIN(t *testing.T) { - setup.SkipForMongoDB(t, "PLAIN mechanism is not supported by MongoDB") +func TestAuthenticationEnableNewAuthPLAIN(tt *testing.T) { + tt.Parallel() - t.Parallel() + t := setup.FailsForMongoDB(tt, "PLAIN mechanism is not supported by MongoDB") - s := setup.SetupWithOpts(t, nil) + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) ctx, cName, db := s.Ctx, s.Collection.Name(), s.Collection.Database() err := db.RunCommand(ctx, bson.D{ @@ -408,7 +458,7 @@ func TestAuthenticationEnableNewAuthPLAIN(t *testing.T) { for name, tc := range testCases { name, tc := name, tc - t.Run(name, func(t *testing.T) { + tt.Run(name, func(t *testing.T) { t.Parallel() credential := options.Credential{ @@ -421,7 +471,7 @@ func TestAuthenticationEnableNewAuthPLAIN(t *testing.T) { opts := options.Client().ApplyURI(s.MongoDBURI).SetAuth(credential) client, err := mongo.Connect(ctx, opts) - require.NoError(t, err, "cannot connect to MongoDB") + require.NoError(t, err) t.Cleanup(func() { require.NoError(t, client.Disconnect(ctx)) diff --git a/integration/users/create_user_test.go b/integration/users/create_user_test.go index 63b204ae4f93..dd13ac1e4baa 100644 --- a/integration/users/create_user_test.go +++ b/integration/users/create_user_test.go @@ -34,8 +34,8 @@ import ( func TestCreateUser(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) - db := collection.Database() + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx, db := s.Ctx, s.Collection.Database() testCases := map[string]struct { //nolint:vet // for readability payload bson.D diff --git a/integration/users/drop_all_users_from_database_test.go b/integration/users/drop_all_users_from_database_test.go index 730743b5235f..6d563c2ca86f 100644 --- a/integration/users/drop_all_users_from_database_test.go +++ b/integration/users/drop_all_users_from_database_test.go @@ -34,9 +34,10 @@ import ( func TestDropAllUsersFromDatabase(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) - db := collection.Database() - client := collection.Database().Client() + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx := s.Ctx + db := s.Collection.Database() + client := db.Client() quantity := 5 // Add some users to the database. for i := 1; i <= quantity; i++ { diff --git a/integration/users/drop_user_test.go b/integration/users/drop_user_test.go index a492b33b5b86..d3ba91b2572d 100644 --- a/integration/users/drop_user_test.go +++ b/integration/users/drop_user_test.go @@ -31,8 +31,8 @@ import ( func TestDropUser(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) - db := collection.Database() + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx, db := s.Ctx, s.Collection.Database() err := db.RunCommand(ctx, bson.D{ {"createUser", "a_user"}, diff --git a/integration/users/update_user_test.go b/integration/users/update_user_test.go index d6d7ba1a4961..a924a662fb49 100644 --- a/integration/users/update_user_test.go +++ b/integration/users/update_user_test.go @@ -32,8 +32,8 @@ import ( func TestUpdateUser(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) - db := collection.Database() + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx, db := s.Ctx, s.Collection.Database() testCases := map[string]struct { //nolint:vet // for readability createPayload bson.D diff --git a/integration/users/usersinfo_test.go b/integration/users/usersinfo_test.go index 6bfdb77483ac..a177d5555551 100644 --- a/integration/users/usersinfo_test.go +++ b/integration/users/usersinfo_test.go @@ -42,7 +42,8 @@ func createUser(username, password string) bson.D { func TestUsersinfo(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t) + s := setup.SetupWithOpts(t, &setup.SetupOpts{SetupUser: true}) + ctx, collection := s.Ctx, s.Collection client := collection.Database().Client() dbToUsers := []struct { diff --git a/internal/clientconn/conn.go b/internal/clientconn/conn.go index c4977652caa5..7385debfdf46 100644 --- a/internal/clientconn/conn.go +++ b/internal/clientconn/conn.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net" + "net/netip" "os" "path/filepath" "runtime/pprof" @@ -140,7 +141,10 @@ func (c *conn) run(ctx context.Context) (err error) { connInfo := conninfo.New() if c.netConn.RemoteAddr().Network() != "unix" { - connInfo.PeerAddr = c.netConn.RemoteAddr().String() + connInfo.Peer, err = netip.ParseAddrPort(c.netConn.RemoteAddr().String()) + if err != nil { + return + } } ctx = conninfo.Ctx(ctx, connInfo) diff --git a/internal/clientconn/conninfo/conn_info.go b/internal/clientconn/conninfo/conn_info.go index 8d70b81d631f..52676c92919f 100644 --- a/internal/clientconn/conninfo/conn_info.go +++ b/internal/clientconn/conninfo/conn_info.go @@ -17,6 +17,7 @@ package conninfo import ( "context" + "net/netip" "sync" "github.com/xdg-go/scram" @@ -32,22 +33,24 @@ var connInfoKey = contextKey{} type ConnInfo struct { // the order of fields is weird to make the struct smaller due to alignment - PeerAddr string - username string // protected by rw - password string // protected by rw - mechanism string // protected by rw - metadataRecv bool // protected by rw - sc *scram.ServerConversation // protected by rw + Peer netip.AddrPort // invalid for Unix domain sockets + + username string // protected by rw + password string // protected by rw + mechanism string // protected by rw + + rw sync.RWMutex + + metadataRecv bool // protected by rw + // If true, backend implementations should not perform authentication // by adding username and password to the connection string. // It is set to true for background connections (such us capped collections cleanup) // and by the new authentication mechanism. // See where it is used for more details. bypassBackendAuth bool // protected by rw - - rw sync.RWMutex } // New returns a new ConnInfo. @@ -55,6 +58,11 @@ func New() *ConnInfo { return new(ConnInfo) } +// LocalPeer returns whether the peer is considered local (using Unix domain socket or loopback IP). +func (connInfo *ConnInfo) LocalPeer() bool { + return !connInfo.Peer.IsValid() || connInfo.Peer.Addr().IsLoopback() +} + // Username returns stored username. func (connInfo *ConnInfo) Username() string { connInfo.rw.RLock() @@ -91,8 +99,8 @@ func (connInfo *ConnInfo) Conv() *scram.ServerConversation { // SetConv stores the SCRAM server conversation. func (connInfo *ConnInfo) SetConv(sc *scram.ServerConversation) { - connInfo.rw.RLock() - defer connInfo.rw.RUnlock() + connInfo.rw.Lock() + defer connInfo.rw.Unlock() connInfo.username = sc.Username() connInfo.sc = sc @@ -122,17 +130,6 @@ func (connInfo *ConnInfo) SetBypassBackendAuth() { connInfo.bypassBackendAuth = true } -// UnsetBypassBackendAuth marks the connection as requiring backend authentication. -// -// This is a workaround to use backend authentication. -// TODO https://github.com/FerretDB/FerretDB/issues/4100 -func (connInfo *ConnInfo) UnsetBypassBackendAuth() { - connInfo.rw.Lock() - defer connInfo.rw.Unlock() - - connInfo.bypassBackendAuth = false -} - // BypassBackendAuth returns whether the connection requires backend authentication. func (connInfo *ConnInfo) BypassBackendAuth() bool { connInfo.rw.RLock() diff --git a/internal/clientconn/conninfo/conn_info_test.go b/internal/clientconn/conninfo/conn_info_test.go index fdb24436ab8f..5c505612b4e6 100644 --- a/internal/clientconn/conninfo/conn_info_test.go +++ b/internal/clientconn/conninfo/conn_info_test.go @@ -16,6 +16,7 @@ package conninfo import ( "context" + "net/netip" "testing" "github.com/stretchr/testify/assert" @@ -25,26 +26,34 @@ func TestGet(t *testing.T) { t.Parallel() for name, tc := range map[string]struct { - peerAddr string + peer netip.AddrPort + local bool }{ - "EmptyPeerAddr": { - peerAddr: "", + "Unix": { + local: true, }, - "NonEmptyPeerAddr": { - peerAddr: "127.0.0.8:1234", + "Local": { + peer: netip.MustParseAddrPort("127.42.7.1:1234"), + local: true, + }, + "NonLocal": { + peer: netip.MustParseAddrPort("192.168.0.1:1234"), + local: false, + }, + "LocalIPv6": { + peer: netip.MustParseAddrPort("[::1]:1234"), + local: true, }, } { - tc := tc t.Run(name, func(t *testing.T) { - t.Parallel() - ctx := context.Background() connInfo := &ConnInfo{ - PeerAddr: tc.peerAddr, + Peer: tc.peer, } ctx = Ctx(ctx, connInfo) actual := Get(ctx) assert.Equal(t, connInfo, actual) + assert.Equal(t, tc.local, actual.LocalPeer()) }) } diff --git a/internal/handler/authenticate.go b/internal/handler/authenticate.go index 3ba5102db34b..5be95eaab23e 100644 --- a/internal/handler/authenticate.go +++ b/internal/handler/authenticate.go @@ -17,6 +17,7 @@ package handler import ( "context" "errors" + "fmt" "github.com/FerretDB/FerretDB/internal/clientconn/conninfo" "github.com/FerretDB/FerretDB/internal/handler/common" @@ -32,17 +33,14 @@ import ( // If EnableNewAuth is false or bypass backend auth is set false, it succeeds // authentication and let backend handle it. // -// When admin.systems.user contains no user, the authentication is delegated to -// the backend. -// TODO https://github.com/FerretDB/FerretDB/issues/4100 +// When admin.systems.user contains no user, and the client is connected from +// the localhost, it bypasses credentials check. func (h *Handler) authenticate(ctx context.Context) error { if !h.EnableNewAuth { return nil } - if !conninfo.Get(ctx).BypassBackendAuth() { - return nil - } + conninfo.Get(ctx).SetBypassBackendAuth() adminDB, err := h.b.Database("admin") if err != nil { @@ -61,10 +59,13 @@ func (h *Handler) authenticate(ctx context.Context) error { // SCRAM calls back scramCredentialLookup each time Step is called, // and that checks the authentication. return nil - case "PLAIN": + case "PLAIN", "": + // mechanism may be empty for local host exception break default: - return lazyerrors.Errorf("Unsupported authentication mechanism %q", mechanism) + msg := fmt.Sprintf("Unsupported authentication mechanism %q.\n", mechanism) + + "See https://docs.ferretdb.io/security/authentication/ for more details." + return handlererrors.NewCommandErrorMsgWithArgument(handlererrors.ErrAuthenticationFailed, msg, mechanism) } // For `PLAIN` mechanism $db field is always `$external` upon saslStart. @@ -111,14 +112,7 @@ func (h *Handler) authenticate(ctx context.Context) error { } } - if !hasUser { - // There is no user in the database, let the backend check the authentication. - // We do not want unauthenticated users accessing the database, while allowing - // users with valid backend credentials to access the database. - // TODO https://github.com/FerretDB/FerretDB/issues/4100 - conninfo.Get(ctx).UnsetBypassBackendAuth() - h.L.Error("backend is used for authentication - no user in admin.system.users collection") - + if !hasUser && conninfo.Get(ctx).LocalPeer() { return nil } diff --git a/internal/handler/commands.go b/internal/handler/commands.go index 03d6868697de..ded0b2b853d5 100644 --- a/internal/handler/commands.go +++ b/internal/handler/commands.go @@ -203,8 +203,9 @@ func (h *Handler) initCommands() { Help: "Returns a summary of indexes of the specified collection.", }, "logout": { - Handler: h.MsgLogout, - Help: "Logs out from the current session.", + Handler: h.MsgLogout, + anonymous: true, + Help: "Logs out from the current session.", }, "ping": { Handler: h.MsgPing, diff --git a/internal/handler/msg_whatsmyuri.go b/internal/handler/msg_whatsmyuri.go index 20d7ac680deb..025e96d7e982 100644 --- a/internal/handler/msg_whatsmyuri.go +++ b/internal/handler/msg_whatsmyuri.go @@ -28,7 +28,7 @@ func (h *Handler) MsgWhatsMyURI(ctx context.Context, msg *wire.OpMsg) (*wire.OpM var reply wire.OpMsg must.NoError(reply.SetSections(wire.MakeOpMsgSection( must.NotFail(types.NewDocument( - "you", conninfo.Get(ctx).PeerAddr, + "you", conninfo.Get(ctx).Peer.String(), "ok", float64(1), )), ))) diff --git a/website/blog/2022-11-21-ferretdb-0-6-2-version-release.md b/website/blog/2022-11-21-ferretdb-0-6-2-version-release.md index c2c29734c190..6f888a68352e 100644 --- a/website/blog/2022-11-21-ferretdb-0-6-2-version-release.md +++ b/website/blog/2022-11-21-ferretdb-0-6-2-version-release.md @@ -33,7 +33,7 @@ In the latest release, we have published our commands parity guide with MongoDB, ## Bug Fixes -We've fixed issues with Unix socket listeners, where you get internal errors or panic when running FerretDB with a Unix socket listener. +We've fixed issues with Unix domain socket listeners, where you get internal errors or panic when running FerretDB with a Unix domain socket listener. ## Other changes and enhancements