diff --git a/fbconn/config.go b/fbconn/config.go index b08e25c..8004f56 100644 --- a/fbconn/config.go +++ b/fbconn/config.go @@ -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 diff --git a/fbconn/config_lazy_os_user_test.go b/fbconn/config_lazy_os_user_test.go new file mode 100644 index 0000000..f319597 --- /dev/null +++ b/fbconn/config_lazy_os_user_test.go @@ -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) + }) + } +} diff --git a/fbconn/defaults.go b/fbconn/defaults.go index 425e7a5..3773589 100644 --- a/fbconn/defaults.go +++ b/fbconn/defaults.go @@ -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) @@ -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 @@ -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" } diff --git a/fbconn/defaults_windows.go b/fbconn/defaults_windows.go index 3682193..1859c2d 100644 --- a/fbconn/defaults_windows.go +++ b/fbconn/defaults_windows.go @@ -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) @@ -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 @@ -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" } diff --git a/fbconn/export_test.go b/fbconn/export_test.go new file mode 100644 index 0000000..471234d --- /dev/null +++ b/fbconn/export_test.go @@ -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 } +}