From 8ed7b653f27e07ad1cd96c4142a5b7df2ef9191e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 22 Sep 2021 11:19:39 -0700 Subject: [PATCH] redact password when logging Redis URL (#8) --- redis_big_segments_impl.go | 2 +- redis_impl.go | 23 +++++++++++++++++++- redis_logging_test.go | 43 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 redis_logging_test.go diff --git a/redis_big_segments_impl.go b/redis_big_segments_impl.go index 4da512b..84e9b84 100644 --- a/redis_big_segments_impl.go +++ b/redis_big_segments_impl.go @@ -31,7 +31,7 @@ func newRedisBigSegmentStoreImpl( impl.loggers.SetPrefix("RedisBigSegmentStore:") if impl.pool == nil { - impl.loggers.Infof("Using URL: %s", builder.url) + logRedisURL(loggers, builder.url) impl.pool = newPool(builder.url, builder.dialOptions) } return impl diff --git a/redis_impl.go b/redis_impl.go index 57db794..ee00c91 100644 --- a/redis_impl.go +++ b/redis_impl.go @@ -1,6 +1,7 @@ package ldredis import ( + "net/url" "time" r "github.com/gomodule/redigo/redis" @@ -49,12 +50,32 @@ func newRedisDataStoreImpl( impl.loggers.SetPrefix("RedisDataStore:") if impl.pool == nil { - impl.loggers.Infof("Using URL: %s", builder.url) + logRedisURL(loggers, builder.url) impl.pool = newPool(builder.url, builder.dialOptions) } return impl } +func logRedisURL(loggers ldlog.Loggers, redisURL string) { + if parsed, err := url.Parse(redisURL); err == nil { + loggers.Infof("Using URL: %s", urlToRedactedString(parsed)) + } else { + loggers.Errorf("Invalid Redis URL: %s", redisURL) // we can assume that the Redis client will also fail + } +} + +// Equivalent to URL.Redacted() in Go 1.15+; currently we still support Go 1.14 +func urlToRedactedString(parsed *url.URL) string { + if parsed != nil && parsed.User != nil { + if _, hasPW := parsed.User.Password(); hasPW { + transformed := *parsed + transformed.User = url.UserPassword(parsed.User.Username(), "xxxxx") + return transformed.String() + } + } + return parsed.String() +} + func (store *redisDataStoreImpl) Init(allData []ldstoretypes.SerializedCollection) error { c := store.getConn() defer c.Close() // nolint:errcheck diff --git a/redis_logging_test.go b/redis_logging_test.go new file mode 100644 index 0000000..9ccc216 --- /dev/null +++ b/redis_logging_test.go @@ -0,0 +1,43 @@ +package ldredis + +import ( + "testing" + + "gopkg.in/launchdarkly/go-sdk-common.v2/ldlog" + "gopkg.in/launchdarkly/go-sdk-common.v2/ldlogtest" + "gopkg.in/launchdarkly/go-server-sdk.v5/ldcomponents" + "gopkg.in/launchdarkly/go-server-sdk.v5/testhelpers" + + "github.com/stretchr/testify/require" +) + +func doStartupLoggingTest(t *testing.T, url string, expectedLogURL string) { + mockLog1 := ldlogtest.NewMockLog() + mockLog2 := ldlogtest.NewMockLog() + defer mockLog1.DumpIfTestFailed(t) + defer mockLog2.DumpIfTestFailed(t) + context1 := testhelpers.NewSimpleClientContext("sdk-key"). + WithLogging(ldcomponents.Logging().Loggers(mockLog1.Loggers)) + context2 := testhelpers.NewSimpleClientContext("sdk-key"). + WithLogging(ldcomponents.Logging().Loggers(mockLog2.Loggers)) + + store1, err := DataStore().URL(url).CreatePersistentDataStore(context1) + require.NoError(t, err) + _ = store1.Close() + mockLog1.AssertMessageMatch(t, true, ldlog.Info, "Using URL: "+expectedLogURL) + + store2, err := DataStore().URL(url).CreateBigSegmentStore(context2) + require.NoError(t, err) + _ = store2.Close() + mockLog2.AssertMessageMatch(t, true, ldlog.Info, "Using URL: "+expectedLogURL) +} + +func TestURLAppearsInLogAtStartup(t *testing.T) { + doStartupLoggingTest(t, "redis://localhost:6379", "redis://localhost:6379") + doStartupLoggingTest(t, "redis://localhost:6379/1", "redis://localhost:6379/1") +} + +func TestURLPasswordIsObfuscatedInLog(t *testing.T) { + doStartupLoggingTest(t, "redis://username@localhost:6379", "redis://username@localhost:6379") + doStartupLoggingTest(t, "redis://username:very-secret-password@localhost:6379", "redis://username:xxxxx@localhost:6379") +}