Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions fbconn/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,25 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con
}

settings := mergeSettings(defaultSettings, envSettings, connStringSettings)

// Resolving the default system_user requires looking up the OS user account,
// which can be slow or crash in some restricted NSS/CGO environments.
// Memoize the lookup within this parse and perform it only when system_user
// is empty. The database user and password are independent of system_user.
var cachedOSUserSettings map[string]string
lazyOSUserSettings := func() map[string]string {
if cachedOSUserSettings == nil {
cachedOSUserSettings = osUserSettings()
}
return cachedOSUserSettings
}

// Fall back to the OS account name when system_user is empty after parsing
// the connection string.
if settings["system_user"] == "" {
settings = mergeSettings(lazyOSUserSettings(), settings)
}

encoding, err := fbproto.NewEncoding(settings["encoding"])
if err != nil {
return nil, err
Expand Down
117 changes: 117 additions & 0 deletions fbconn/config_lazy_os_user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package fbconn_test

import (
"errors"
"os/user"
"runtime"
"testing"

"github.com/firebirdsql/fbx/fbconn"
"github.com/firebirdsql/fbx/fbproto/auth"
"github.com/stretchr/testify/require"
)

func TestParseConfigDoesNotLookUpOSUserWhenSystemUserExplicit(t *testing.T) {
restore := fbconn.SetOSUserLookupForTest(func() (*user.User, error) {
panic("OS user lookup must not run when system_user is explicit")
})
t.Cleanup(restore)

config, err := fbconn.ParseConfig("host=localhost port=3050 database=mydb system_user=explicit-os-user")
require.NoError(t, err)
require.Equal(t, "explicit-os-user", config.SystemUser)
}

func TestParseConfigDoesNotLookUpOSUserWhenSystemUserAndPasswordExplicit(t *testing.T) {
restore := fbconn.SetOSUserLookupForTest(func() (*user.User, error) {
panic("OS user lookup must not run when system_user is explicit")
})
t.Cleanup(restore)

config, err := fbconn.ParseConfig("firebird://ivan:secret@localhost:3050/mydb?system_user=explicit-os-user")
require.NoError(t, err)
require.Equal(t, "explicit-os-user", config.SystemUser)
require.Equal(t, "ivan", config.User)
require.Equal(t, "secret", config.Password)
}

func TestParseConfigStillLooksUpOSUserWhenNeeded(t *testing.T) {
calls := 0
restore := fbconn.SetOSUserLookupForTest(func() (*user.User, error) {
calls++
name := "mocked-os-user"
if runtime.GOOS == "windows" {
name = `DOMAIN\mocked-os-user`
}
return &user.User{Username: name}, nil
})
t.Cleanup(restore)

// Explicit database credentials must not suppress the independent system_user default.
config, err := fbconn.ParseConfig("firebird://ivan:secret@localhost:3050/mydb")
require.NoError(t, err)
require.Equal(t, 1, calls)
require.Equal(t, "mocked-os-user", config.SystemUser)
require.Equal(t, "ivan", config.User)
}

func TestParseConfigOSUserLookupFailureStillUsesExplicitCredentials(t *testing.T) {
restore := fbconn.SetOSUserLookupForTest(func() (*user.User, error) {
return nil, errors.New("OS account lookup failed")
})
t.Cleanup(restore)

config, err := fbconn.ParseConfig("firebird://ivan:secret@localhost:3050/mydb")
require.NoError(t, err)
require.Empty(t, config.SystemUser)
require.Equal(t, "ivan", config.User)
require.Equal(t, "secret", config.Password)
}

// fbx uses Firebird wire encryption rather than home-directory TLS files.
// Skipping or failing the OS account lookup must preserve its security defaults.
func TestParseConfigSecurityDefaultsIndependentOfOSUserLookup(t *testing.T) {
tests := []struct {
name string
connString string
lookupFails bool
skipLookup bool
}{
{
name: "explicit system user",
connString: "firebird://ivan:secret@localhost:3050/mydb?system_user=explicit-os-user",
skipLookup: true,
},
{
name: "default system user",
connString: "firebird://ivan:secret@localhost:3050/mydb",
},
{
name: "lookup failure",
connString: "firebird://ivan:secret@localhost:3050/mydb",
lookupFails: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
restore := fbconn.SetOSUserLookupForTest(func() (*user.User, error) {
if tt.skipLookup {
panic("OS user lookup must not run when system_user is explicit")
}
if tt.lookupFails {
return nil, errors.New("OS account lookup failed")
}
return &user.User{Username: "mocked-os-user"}, nil
})
t.Cleanup(restore)

config, err := fbconn.ParseConfig(tt.connString)
require.NoError(t, err)
require.True(t, config.WireCrypt)
require.Equal(t, auth.DefaultPluginSequence(), config.AuthPluginList)
require.Equal(t, "ivan", config.User)
require.Equal(t, "secret", config.Password)
})
}
}
31 changes: 26 additions & 5 deletions fbconn/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import (
"github.com/firebirdsql/fbx/fbproto/auth"
)

// currentOSUser resolves the current OS user account. It is a seam so tests
// can simulate os/user.Current failing -- including the unrecoverable
// crashes reported in some restricted/broken-NSS container environments --
// without needing such an environment.
var currentOSUser = user.Current

func defaultSettings() map[string]string {
settings := make(map[string]string)

Expand All @@ -24,11 +30,6 @@ func defaultSettings() map[string]string {
settings["max_inline_blob_size"] = "65536"
settings["max_blob_cache_size"] = "10485760"

user, err := user.Current()
if err == nil {
settings["system_user"] = user.Username
}

hostname, err := os.Hostname()
if err == nil {
settings["system_host"] = hostname
Expand All @@ -43,6 +44,26 @@ func defaultSettings() map[string]string {
return settings
}

// osUserSettings returns the default system_user derived from the current
// OS account name. This is independent of the database user.
//
// Resolving this requires looking up the OS user account, which can be slow
// or, in some restricted container environments (e.g. distroless images
// with a broken NSS/CGO setup), crash the process. Callers must only call
// osUserSettings when system_user is empty after parsing the connection string.
func osUserSettings() map[string]string {
settings := make(map[string]string)

// Ignore lookup errors and leave system_user without a default. Applications
// can supply system_user explicitly in the connection string.
user, err := currentOSUser()
if err == nil {
settings["system_user"] = user.Username
}

return settings
}

func defaultHost() string {
return "localhost"
}
43 changes: 33 additions & 10 deletions fbconn/defaults_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
"github.com/firebirdsql/fbx/fbproto/auth"
)

// currentOSUser resolves the current OS user account. It is a seam so tests
// can simulate os/user.Current failing -- including the unrecoverable
// crashes reported in some restricted/broken-NSS container environments --
// without needing such an environment.
var currentOSUser = user.Current

func defaultSettings() map[string]string {
settings := make(map[string]string)

Expand All @@ -23,16 +29,6 @@ func defaultSettings() map[string]string {
settings["max_inline_blob_size"] = "65536"
settings["max_blob_cache_size"] = "10485760"

user, err := user.Current()
if err == nil {
username := user.Username
if strings.Contains(username, "\\") {
username = username[strings.LastIndex(username, "\\")+1:]
}

settings["system_user"] = username
}

hostname, err := os.Hostname()
if err == nil {
settings["system_host"] = hostname
Expand All @@ -47,6 +43,33 @@ func defaultSettings() map[string]string {
return settings
}

// osUserSettings returns the default system_user derived from the current
// OS account name. This is independent of the database user.
//
// Resolving this requires looking up the OS user account, which can be slow
// or, in some restricted container environments (e.g. distroless images
// with a broken NSS/CGO setup), crash the process. Callers must only call
// osUserSettings when system_user is empty after parsing the connection string.
func osUserSettings() map[string]string {
settings := make(map[string]string)

// Ignore lookup errors and leave system_user without a default. Applications
// can supply system_user explicitly in the connection string.
user, err := currentOSUser()
if err == nil {
// Windows gives us the username here as `DOMAIN\user` or `LOCALPCNAME\user`,
// but we strip off the first part.
username := user.Username
if strings.Contains(username, "\\") {
username = username[strings.LastIndex(username, "\\")+1:]
}

settings["system_user"] = username
}

return settings
}

func defaultHost() string {
return "localhost"
}
11 changes: 11 additions & 0 deletions fbconn/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package fbconn

import "os/user"

// SetOSUserLookupForTest replaces the lookup used for the default system_user.
// Tests using it must not run in parallel.
func SetOSUserLookupForTest(lookup func() (*user.User, error)) (restore func()) {
prev := currentOSUser
currentOSUser = lookup
return func() { currentOSUser = prev }
}