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

Support localhost exception #4156

Merged
merged 25 commits into from Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ferretdb/ferretdb.go
Expand Up @@ -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(),
Expand Down
120 changes: 77 additions & 43 deletions integration/commands_authentication_test.go
Expand Up @@ -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, "cannot connect to MongoDB")
AlekSi marked this conversation as resolved.
Show resolved Hide resolved

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")
Expand All @@ -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")
Expand All @@ -53,47 +66,68 @@ 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, nil)
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, "cannot connect to MongoDB")

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")
AlekSi marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)

actualUsersV, err := actualAuth.(*types.Document).Get("authenticatedUsers")
require.NoError(t, err)

expectedUsers := must.NotFail(types.NewArray(must.NotFail(types.NewDocument("user", username, "db", db.Name()))))
require.Equal(t, expectedUsers, actualUsersV.(*types.Array))

// 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)
}
2 changes: 1 addition & 1 deletion integration/commands_diagnostic_test.go
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions integration/setup/setup.go
Expand Up @@ -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.")
Expand Down Expand Up @@ -97,12 +97,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, "/")
Expand Down
3 changes: 1 addition & 2 deletions integration/users/connection_test.go
Expand Up @@ -266,11 +266,10 @@ func TestAuthenticationEnableNewAuthNoUserExists(t *testing.T) {
pingErr string
insertErr string
}{
"PLAINNonExistingUser": {
"LocalhostException": {
username: "plain-user",
password: "whatever",
mechanism: "PLAIN",
insertErr: `role "plain-user" does not exist`,
},
"PLAINBackendUser": {
username: "username",
Expand Down
6 changes: 5 additions & 1 deletion internal/clientconn/conn.go
Expand Up @@ -24,6 +24,7 @@
"fmt"
"io"
"net"
"net/netip"
"os"
"path/filepath"
"runtime/pprof"
Expand Down Expand Up @@ -140,7 +141,10 @@

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

Check warning on line 146 in internal/clientconn/conn.go

View check run for this annotation

Codecov / codecov/patch

internal/clientconn/conn.go#L146

Added line #L146 was not covered by tests
}
}

ctx = conninfo.Ctx(ctx, connInfo)
Expand Down
37 changes: 17 additions & 20 deletions internal/clientconn/conninfo/conn_info.go
Expand Up @@ -17,6 +17,7 @@ package conninfo

import (
"context"
"net/netip"
"sync"

"github.com/xdg-go/scram"
Expand All @@ -32,28 +33,35 @@ 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
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

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.
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()
Expand Down Expand Up @@ -89,8 +97,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
Expand Down Expand Up @@ -120,17 +128,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()
Expand Down
23 changes: 14 additions & 9 deletions internal/clientconn/conninfo/conn_info_test.go
Expand Up @@ -16,6 +16,7 @@ package conninfo

import (
"context"
"net/netip"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -25,26 +26,30 @@ 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,
henvic marked this conversation as resolved.
Show resolved Hide resolved
},
"NonLocal": {
peer: netip.MustParseAddrPort("192.168.0.1:1234"),
local: false,
},
} {
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())
})
}

Expand Down