diff --git a/client.go b/client.go index 2b6f90e5b..bd730bc04 100644 --- a/client.go +++ b/client.go @@ -90,7 +90,7 @@ type ( ) func init() { - // Wether or not we will enable Resty debugging output + // Whether we will enable Resty debugging output if apiDebug, ok := os.LookupEnv("LINODE_DEBUG"); ok { if parsed, err := strconv.ParseBool(apiDebug); err == nil { envDebug = parsed @@ -122,6 +122,8 @@ func (c *Client) R(ctx context.Context) *resty.Request { func (c *Client) SetDebug(debug bool) *Client { c.debug = debug c.resty.SetDebug(debug) + // this ensures that if there is an Authorization header present, the value is sanitized/masked + c.sanitizeAuthorizationHeader() return c } @@ -412,6 +414,14 @@ func (c *Client) SetHeader(name, value string) { c.resty.SetHeader(name, value) } +func (c *Client) sanitizeAuthorizationHeader() { + c.resty.OnRequestLog(func(r *resty.RequestLog) error { + // masking authorization header + r.Header.Set("Authorization", "Bearer *******************************") + return nil + }) +} + // NewClient factory to create new Client struct func NewClient(hc *http.Client) (client Client) { if hc != nil { diff --git a/client_test.go b/client_test.go index 7176d6cb7..3a4a38a1e 100644 --- a/client_test.go +++ b/client_test.go @@ -1,10 +1,16 @@ package linodego import ( + "bytes" + "context" "fmt" + "reflect" + "strings" "testing" "github.com/google/go-cmp/cmp" + "github.com/jarcoal/httpmock" + "github.com/linode/linodego/internal/testutil" ) func TestClient_SetAPIVersion(t *testing.T) { @@ -139,3 +145,56 @@ api_version = v4beta [cool] token = blah ` + +func TestDebugLogSanitization(t *testing.T) { + type instanceResponse struct { + ID int `json:"id"` + Region string `json:"region"` + Label string `json:"label"` + } + + var testResp = instanceResponse{ + ID: 100, + Region: "test-central", + Label: "this-is-a-test-linode", + } + var lgr bytes.Buffer + + plainTextToken := "NOTANAPIKEY" + + mockClient := testutil.CreateMockClient(t, NewClient) + logger := testutil.CreateLogger() + mockClient.SetLogger(logger) + logger.L.SetOutput(&lgr) + + mockClient.SetDebug(true) + if !mockClient.resty.Debug { + t.Fatal("debug should be enabled") + } + mockClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", plainTextToken)) + + if mockClient.resty.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", plainTextToken) { + t.Fatal("token not found in auth header") + } + + httpmock.RegisterRegexpResponder("GET", testutil.MockRequestURL("/linode/instances"), + httpmock.NewJsonResponderOrPanic(200, &testResp)) + + result, err := doGETRequest[instanceResponse]( + context.Background(), + mockClient, + "/linode/instances", + ) + if err != nil { + t.Fatal(err) + } + + logInfo := lgr.String() + if !strings.Contains(logInfo, "Bearer *******************************") { + t.Fatal("sanitized bearer token was expected") + } + + if !reflect.DeepEqual(*result, testResp) { + t.Fatalf("actual response does not equal desired response: %s", cmp.Diff(result, testResponse)) + } +} diff --git a/internal/testutil/mock.go b/internal/testutil/mock.go index 4afe4a92f..aaaeebb47 100644 --- a/internal/testutil/mock.go +++ b/internal/testutil/mock.go @@ -4,7 +4,9 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" + "os" "reflect" "regexp" "strings" @@ -87,3 +89,40 @@ func CreateMockClient[T any](t *testing.T, createFunc func(*http.Client) T) *T { result := createFunc(client) return &result } + +type Logger interface { + Errorf(format string, v ...interface{}) + Warnf(format string, v ...interface{}) + Debugf(format string, v ...interface{}) +} + +func CreateLogger() *TestLogger { + l := &TestLogger{L: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)} + return l +} + +var _ Logger = (*TestLogger)(nil) + +type TestLogger struct { + L *log.Logger +} + +func (l *TestLogger) Errorf(format string, v ...interface{}) { + l.outputf("ERROR RESTY "+format, v...) +} + +func (l *TestLogger) Warnf(format string, v ...interface{}) { + l.outputf("WARN RESTY "+format, v...) +} + +func (l *TestLogger) Debugf(format string, v ...interface{}) { + l.outputf("DEBUG RESTY "+format, v...) +} + +func (l *TestLogger) outputf(format string, v ...interface{}) { + if len(v) == 0 { + l.L.Print(format) + return + } + l.L.Printf(format, v...) +}