diff --git a/internal/plugin/github.go b/internal/plugin/github.go index a2ed6ab6ad6..b3c14f9f2c3 100644 --- a/internal/plugin/github.go +++ b/internal/plugin/github.go @@ -4,6 +4,7 @@ import ( "cmp" "fmt" "io" + "log/slog" "net/http" "net/url" "os" @@ -104,12 +105,20 @@ func (p *githubPlugin) FileContent(subpath string) ([]byte, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { + authInfo := "No auth header was sent with this request." + if req.Header.Get("Authorization") != "" { + authInfo = fmt.Sprintf( + "The auth header `%s` was sent with this request.", + getRedactedAuthHeader(req), + ) + } return nil, 0, usererr.New( - "failed to get plugin %s @ %s (Status code %d). \nPlease make "+ + "failed to get plugin %s @ %s (Status code %d).\n%s\nPlease make "+ "sure a plugin.json file exists in plugin directory.", p.LockfileKey(), req.URL.String(), res.StatusCode, + authInfo, ) } body, err := io.ReadAll(res.Body) @@ -147,6 +156,11 @@ func (p *githubPlugin) request(contentURL string) (*http.Request, error) { if ghToken != "" { authValue := fmt.Sprintf("token %s", ghToken) req.Header.Add("Authorization", authValue) + slog.Debug( + "GITHUB_TOKEN env var found, adding to request's auth header", + "headerValue", + getRedactedAuthHeader(req), + ) } return req, nil @@ -155,3 +169,22 @@ func (p *githubPlugin) request(contentURL string) (*http.Request, error) { func (p *githubPlugin) LockfileKey() string { return p.ref.String() } + +func getRedactedAuthHeader(req *http.Request) string { + authHeader := req.Header.Get("Authorization") + parts := strings.SplitN(authHeader, " ", 2) + + if len(authHeader) < 10 || len(parts) < 2 { + // too short to safely reveal any part + return strings.Repeat("*", len(authHeader)) + } + + authType, token := parts[0], parts[1] + if len(token) < 10 { + // second word too short to reveal any, but show first word + return authType + " " + strings.Repeat("*", len(token)) + } + + // show first 4 chars of token to help with debugging (will often be "ghp_") + return authType + " " + token[:4] + strings.Repeat("*", len(token)-4) +} diff --git a/internal/plugin/github_test.go b/internal/plugin/github_test.go index f9671429035..7aaebcd8415 100644 --- a/internal/plugin/github_test.go +++ b/internal/plugin/github_test.go @@ -1,6 +1,7 @@ package plugin import ( + "net/http" "strings" "testing" @@ -134,3 +135,41 @@ func TestGithubPluginAuth(t *testing.T) { assert.Equal(t, "token gh_abcd", actual.Header.Get("Authorization")) }) } + +func TestGetRedactedAuthHeader(t *testing.T) { + testCases := []struct { + name string + authHeader string + expected string + }{ + { + "normal length token partially readable for debugging", + "token ghp_61b296fb898349778e20532cb65ce38e", + "token ghp_********************************", + }, + { + "short token redacted", + "token ghp_61b29", + "token *********", + }, + { + "short header fully redacted", + "token xyz", + "*********", + }, + { + "no token returns empty string", + "", + "", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) + assert.NoError(t, err) + req.Header.Add("Authorization", testCase.authHeader) + assert.Equal(t, testCase.expected, getRedactedAuthHeader(req)) + }) + } +}