From 948c8697e5317895e623b5cefb44f533ccc04ccf Mon Sep 17 00:00:00 2001 From: Hariharan Thavachelvam <164553783+thavaahariharangit@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:07:18 +0000 Subject: [PATCH 1/3] Add JIT access fallback for GitHub API requests --- internal/handlers/github_api.go | 105 +++++++++++++++-- internal/handlers/github_api_test.go | 167 ++++++++++++++++++++++++--- proxy.go | 2 +- 3 files changed, 247 insertions(+), 27 deletions(-) diff --git a/internal/handlers/github_api.go b/internal/handlers/github_api.go index 98c4d08..b65d24c 100644 --- a/internal/handlers/github_api.go +++ b/internal/handlers/github_api.go @@ -19,7 +19,9 @@ import ( // This allows git credentials for "github.com" to apply to "api.github.com" and // will allow git credentials for ".ghe.com" to apply to "api..ghe.com" in Proxima. type GitHubAPIHandler struct { - credentials *gitCredentialsMap + credentials *gitCredentialsMap + jitAccessByHost map[string]jitAccessConfig + client ScopeRequester } const ghAPIAddedAuthCtxKey = "gh-api.added-auth" @@ -27,20 +29,27 @@ const reservedProximaIdentity = "proxima-service-identity" // NewGitHubAPIHandler returns a new GitHubAPIHandler, extracting the app // access token from the array of credentials -func NewGitHubAPIHandler(creds config.Credentials) *GitHubAPIHandler { +func NewGitHubAPIHandler(creds config.Credentials, client ScopeRequester) *GitHubAPIHandler { handler := GitHubAPIHandler{ - credentials: newGitCredentialsMap(), + credentials: newGitCredentialsMap(), + jitAccessByHost: map[string]jitAccessConfig{}, + client: client, } for _, cred := range creds { - host := cred.Host() - if host == "" { - continue - } - if cred["type"] != "git_source" || (host != "github.com" && !(strings.HasSuffix(fmt.Sprint(host), ".ghe.com"))) { - continue + switch cred["type"] { + case "git_source": + host := cred.Host() + if host == "" { + continue + } + if host != "github.com" && !strings.HasSuffix(fmt.Sprint(host), ".ghe.com") { + continue + } + handler.credentials.addGitSourceCredentials("api."+host, cred) + case "jit_access": + handler.addJITAccess(cred) } - handler.credentials.addGitSourceCredentials("api."+host, cred) } if len(handler.credentials.data) == 0 { @@ -50,6 +59,27 @@ func NewGitHubAPIHandler(creds config.Credentials) *GitHubAPIHandler { return &handler } +func (h *GitHubAPIHandler) addJITAccess(cred config.Credential) { + if cred.GetString("credential-type") != "git_source" { + return + } + + host := strings.ToLower(cred.GetString("host")) + if host == "" { + return + } + + if !strings.HasPrefix(host, "api.") { + host = "api." + host + } + + h.jitAccessByHost[host] = jitAccessConfig{ + endpoint: cred.GetString("endpoint"), + username: cred.GetString("username"), + password: cred.GetString("password"), + } +} + // HandleRequest adds auth to a GitHub API request func (h *GitHubAPIHandler) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { if !h.isHandledGitHubAPIRequest(req) { @@ -84,10 +114,11 @@ func (h *GitHubAPIHandler) HandleResponse(rsp *http.Response, ctx *goproxy.Proxy if rsp == nil { return rsp } - if addedAuth, ok := ctxdata.GetBool(ctx, ghAPIAddedAuthCtxKey); !ok || !addedAuth { + if !h.isHandledGitHubAPIRequest(ctx.Req) { return rsp } - if !h.isHandledGitHubAPIRequest(ctx.Req) { + addedAuth, _ := ctxdata.GetBool(ctx, ghAPIAddedAuthCtxKey) + if !addedAuth && h.jitAccessByHost[helpers.GetHost(ctx.Req)].endpoint == "" { return rsp } if !isPotentialAuthFailure(rsp.StatusCode) { @@ -121,9 +152,59 @@ func (h *GitHubAPIHandler) HandleResponse(rsp *http.Response, ctx *goproxy.Proxy logging.RequestLogf(ctx, "* re-auth'd request returned %d, ignoring response", newRsp.StatusCode) helpers.DrainAndClose(newRsp) } + + // All known credentials have been tried, try to JIT create access credentials. + jitCreds := h.getJITCredentialsForRequest(ctx) + if jitCreds != nil { + newReq := ctx.Req.Clone(ctx.Req.Context()) + logging.RequestLogf(ctx, "* auth'd github api request failed authentication, retrying with jit access auth") + newReq.Header.Set("Authorization", "token "+jitCreds.password) + newRsp, err := ctx.RoundTrip(newReq) + if err != nil { + return rsp + } + + if !isPotentialAuthFailure(newRsp.StatusCode) { + helpers.DrainAndClose(rsp) + logging.RequestLogf(ctx, "* re-auth'd jit request returned %d, replacing response", newRsp.StatusCode) + return newRsp + } + logging.RequestLogf(ctx, "* re-auth'd jit request returned %d, ignoring response", newRsp.StatusCode) + helpers.DrainAndClose(newRsp) + } + return rsp } +func (h *GitHubAPIHandler) getJITCredentialsForRequest(ctx *goproxy.ProxyCtx) *gitCredentials { + host := helpers.GetHost(ctx.Req) + jitConfig := h.jitAccessByHost[host] + if jitConfig.endpoint == "" { + return nil + } + + org, repo, ok := gitHubAPIExtractOrgAndRepo(ctx.Req.URL.Path) + if !ok { + return nil + } + + logging.RequestLogf(ctx, "* requesting JIT access for github api request") + if h.client == nil { + return nil + } + credential, err := h.client.RequestJITAccess(ctx, jitConfig.endpoint, jitConfig.username, jitConfig.password, org, repo) + if credential == nil || err != nil { + return nil + } + + repoNWO := fmt.Sprintf("%s/%s", org, repo) + + // Add the returned credentials to the beginning of the repo-scoped list, so that + // they are prioritized over existing tokens for future requests. + hostCreds := h.credentials.get(host) + return hostCreds.addToken(repoNWO, credential.GetString("username"), credential.GetString("password"), true) +} + func (h *GitHubAPIHandler) isHandledGitHubAPIRequest(req *http.Request) bool { return req.URL.Scheme == "https" && helpers.MethodPermitted(req, "GET", "HEAD") && helpers.CheckGitHubAPIHost(req) } diff --git a/internal/handlers/github_api_test.go b/internal/handlers/github_api_test.go index b4759f2..41e579a 100644 --- a/internal/handlers/github_api_test.go +++ b/internal/handlers/github_api_test.go @@ -24,7 +24,7 @@ func TestGitHubAPIHandler_withUrlFallback(t *testing.T) { "password": "super-secret-token", }} - handler := NewGitHubAPIHandler(usingURL) + handler := NewGitHubAPIHandler(usingURL, nil) req := httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) req = handleRequestAndClose(handler, req, nil) @@ -59,7 +59,7 @@ func TestGitHubAPIHandler(t *testing.T) { bitBucketCred, rubygemsCred, } - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) // Valid API request, prioritises non-installation token req := httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) @@ -84,7 +84,7 @@ func TestGitHubAPIHandler(t *testing.T) { // With only the installation GitHub token credentials := config.Credentials{installationCred, bitBucketCred, rubygemsCred} - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) // Valid API request, uses installation token req := httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) @@ -93,7 +93,7 @@ func TestGitHubAPIHandler(t *testing.T) { // With only the proxima token credentials = config.Credentials{proximaCred, bitBucketCred, rubygemsCred} - handler = NewGitHubAPIHandler(credentials) + handler = NewGitHubAPIHandler(credentials, nil) // Valid API request, uses installation token req = httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) @@ -153,7 +153,7 @@ func TestGitHubAPIHandler_AuthenticatedAccessToGitHubRepos(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - handler := NewGitHubAPIHandler(tt.credentials) + handler := NewGitHubAPIHandler(tt.credentials, nil) // Valid github git request, prioritises non-installation token req := httptest.NewRequest("GET", fmt.Sprintf("https://api.github.com/%s", tt.repoNWO), nil) @@ -197,7 +197,7 @@ func TestGitHubAPIHandlerInProxima(t *testing.T) { bitBucketCred, rubygemsCred, } - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) // Valid API request, prioritises non-installation token req := httptest.NewRequest("GET", "https://api.foo.ghe.com/some-repo", nil) @@ -222,7 +222,7 @@ func TestGitHubAPIHandlerInProxima(t *testing.T) { // With only the installation GitHub token credentials := config.Credentials{installationCred, bitBucketCred, rubygemsCred} - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) // Valid API request, uses installation token req := httptest.NewRequest("GET", "https://api.foo.ghe.com/some-repo", nil) @@ -250,7 +250,7 @@ func TestGitHubAPIHandlerWithMulipleHosts(t *testing.T) { tt.personalAccessToken, fooGheCred, } - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) // Request to github.com, using the correct token req := httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) @@ -271,7 +271,7 @@ func TestGitHubAPIHandlerWithMulipleHosts(t *testing.T) { // With only the github.com token credentials := config.Credentials{githubCred} - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) // Valid API request, uses only github.com token req := httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) @@ -280,7 +280,7 @@ func TestGitHubAPIHandlerWithMulipleHosts(t *testing.T) { // With only the foo.ghe.com token fooGheCredentials := config.Credentials{fooGheCred} - fooGhehandler := NewGitHubAPIHandler(fooGheCredentials) + fooGhehandler := NewGitHubAPIHandler(fooGheCredentials, nil) // Valid API request, uses only foo.ghe.com token fooGheReq := httptest.NewRequest("GET", "https://api.foo.ghe.com/some-repo", nil) @@ -291,7 +291,7 @@ func TestGitHubAPIHandlerWithMulipleHosts(t *testing.T) { func TestGitHubAPIHandler_InstallationTokenFormat(t *testing.T) { installationCred := testGitSourceCred("github.com", "x-access-token", "ghs_fakefakefakesuperfake") credentials := config.Credentials{installationCred} - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) req := httptest.NewRequest("GET", "https://api.github.com/some-repo", nil) req = handleRequestAndClose(handler, req, nil) @@ -301,7 +301,7 @@ func TestGitHubAPIHandler_InstallationTokenFormat(t *testing.T) { func TestGitHubAPIHandler_InstallationTokenFormat_Proxima(t *testing.T) { installationCred := testGitSourceCred("foo.ghe.com", "x-access-token", "ghs_fakefakefakesuperfake") credentials := config.Credentials{installationCred} - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) req := httptest.NewRequest("GET", "https://api.foo.ghe.com/some-repo", nil) req = handleRequestAndClose(handler, req, nil) @@ -321,7 +321,7 @@ func TestGitHubAPIHandler_TokenFallback(t *testing.T) { testGitSourceCred("github.com", "x-access-token", userToken1), testGitSourceCred("github.com", "x-access-token", userToken2), } - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) tests := []struct { name string @@ -438,7 +438,7 @@ func TestGitHubAPIHandler_TokenFallback_In_Proxima(t *testing.T) { testGitSourceCred("foo.ghe.com", "x-access-token", userToken1), testGitSourceCred("foo.ghe.com", "x-access-token", userToken2), } - handler := NewGitHubAPIHandler(credentials) + handler := NewGitHubAPIHandler(credentials, nil) url, err := url.Parse("https://api.foo.ghe.com/repos/github/dependabot-action") if err != nil { t.Errorf("parsing url: %v", err) @@ -533,3 +533,142 @@ func TestGitHubAPIHandler_TokenFallback_In_Proxima(t *testing.T) { }) } } + +type testGitHubAPIScopeRequester struct { + callCount int + result *config.Credential +} + +func (t *testGitHubAPIScopeRequester) RequestJITAccess(ctx *goproxy.ProxyCtx, endpoint string, username string, password string, account string, repo string) (*config.Credential, error) { + t.callCount++ + return t.result, nil +} + +func TestGitHubAPIHandler_JITAccessFallback(t *testing.T) { + staticToken := "ghp_static" + jitToken := "ghp_jit" + credentials := config.Credentials{ + testGitSourceCred("github.com", "x-access-token", staticToken), + { + "type": "jit_access", + "credential-type": "git_source", + "host": "github.com", + "endpoint": "https://dependabot.example.com/jit_access", + }, + } + requester := &testGitHubAPIScopeRequester{ + result: &config.Credential{ + "username": "x-access-token", + "password": jitToken, + }, + } + handler := NewGitHubAPIHandler(credentials, requester) + + var capturedTokens []string + roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { + token := strings.TrimPrefix(r.Header.Get("Authorization"), "token ") + capturedTokens = append(capturedTokens, token) + if token == jitToken { + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("jit-ok"))}, nil + } + return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("not found"))}, nil + }) + + req := httptest.NewRequest("GET", "https://api.github.com:443/repos/example/internal-repo/releases?per_page=100", nil) + ctx := &goproxy.ProxyCtx{Req: req, RoundTripper: roundTripper} + rsp := &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("initial"))} + + _ = handleRequestAndClose(handler, req, ctx) + newRsp := handler.HandleResponse(rsp, ctx) + defer newRsp.Body.Close() + + body, err := io.ReadAll(newRsp.Body) + require.NoError(t, err) + assert.Equal(t, 200, newRsp.StatusCode) + assert.Equal(t, "jit-ok", string(body)) + assert.Equal(t, []string{staticToken, jitToken}, capturedTokens) + assert.Equal(t, 1, requester.callCount) +} + +func TestGitHubAPIHandler_JITAccessFallbackWithoutStaticCredentials(t *testing.T) { + jitToken := "ghp_jit" + credentials := config.Credentials{ + { + "type": "jit_access", + "credential-type": "git_source", + "host": "github.com", + "endpoint": "https://dependabot.example.com/jit_access", + }, + } + requester := &testGitHubAPIScopeRequester{ + result: &config.Credential{ + "username": "x-access-token", + "password": jitToken, + }, + } + handler := NewGitHubAPIHandler(credentials, requester) + + roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { + assert.Equal(t, "token "+jitToken, r.Header.Get("Authorization")) + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("jit-ok"))}, nil + }) + req := httptest.NewRequest("GET", "https://api.github.com/repos/example/internal-repo/releases", nil) + ctx := &goproxy.ProxyCtx{Req: req, RoundTripper: roundTripper} + rsp := &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("initial"))} + + _ = handleRequestAndClose(handler, req, ctx) + newRsp := handler.HandleResponse(rsp, ctx) + defer newRsp.Body.Close() + + assert.Equal(t, 200, newRsp.StatusCode) + assert.Equal(t, 1, requester.callCount) +} + +func TestGitHubAPIHandler_JITAccessTokenIsCached(t *testing.T) { + staticToken := "ghp_static" + jitToken := "ghp_jit" + credentials := config.Credentials{ + testGitSourceCred("github.com", "x-access-token", staticToken), + { + "type": "jit_access", + "credential-type": "git_source", + "host": "github.com", + "endpoint": "https://dependabot.example.com/jit_access", + }, + } + requester := &testGitHubAPIScopeRequester{ + result: &config.Credential{ + "username": "x-access-token", + "password": jitToken, + }, + } + handler := NewGitHubAPIHandler(credentials, requester) + + roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { + token := strings.TrimPrefix(r.Header.Get("Authorization"), "token ") + if token == jitToken { + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("jit-ok"))}, nil + } + return &http.Response{StatusCode: 401, Body: io.NopCloser(strings.NewReader("denied"))}, nil + }) + + req1 := httptest.NewRequest("GET", "https://api.github.com/repos/dependabot/proxy", nil) + ctx1 := &goproxy.ProxyCtx{Req: req1, RoundTripper: roundTripper} + rsp1 := &http.Response{StatusCode: 401, Body: io.NopCloser(strings.NewReader("initial"))} + _ = handleRequestAndClose(handler, req1, ctx1) + newRsp1 := handler.HandleResponse(rsp1, ctx1) + defer newRsp1.Body.Close() + assert.Equal(t, 200, newRsp1.StatusCode) + assert.Equal(t, 1, requester.callCount) + + req2 := httptest.NewRequest("GET", "https://api.github.com/repos/dependabot/proxy", nil) + ctx2 := &goproxy.ProxyCtx{Req: req2, RoundTripper: roundTripper} + rsp2 := &http.Response{StatusCode: 401, Body: io.NopCloser(strings.NewReader("initial"))} + _ = handleRequestAndClose(handler, req2, ctx2) + newRsp2 := handler.HandleResponse(rsp2, ctx2) + defer newRsp2.Body.Close() + assert.Equal(t, 200, newRsp2.StatusCode) + + // Cached repo-scoped credentials should be retried before requesting JIT again. + assert.Equal(t, 1, requester.callCount) +} diff --git a/proxy.go b/proxy.go index 16f1bef..8f9e071 100644 --- a/proxy.go +++ b/proxy.go @@ -77,7 +77,7 @@ func newProxy(envSettings config.ProxyEnvSettings, cfg *config.Config, blockedIp proxy.OnRequest().DoFunc(metricsHandler.HandleRequest) proxy.OnResponse().DoFunc(metricsHandler.HandleResponse) - gitHubAPIHandler := handlers.NewGitHubAPIHandler(cfg.Credentials) + gitHubAPIHandler := handlers.NewGitHubAPIHandler(cfg.Credentials, apiClient) proxy.OnRequest().DoFunc(gitHubAPIHandler.HandleRequest) proxy.OnResponse().DoFunc(gitHubAPIHandler.HandleResponse) From 5c2a6c8a181cdc795c617cbac9e61ca1b1f26e3c Mon Sep 17 00:00:00 2001 From: Hariharan Thavachelvam <164553783+thavaahariharangit@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:30:42 +0000 Subject: [PATCH 2/3] Address GitHub API JIT review feedback --- internal/handlers/git_server.go | 7 +++++++ internal/handlers/github_api.go | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/handlers/git_server.go b/internal/handlers/git_server.go index cb3b50b..c2b0366 100644 --- a/internal/handlers/git_server.go +++ b/internal/handlers/git_server.go @@ -64,6 +64,13 @@ func newGitCredentialsMap() *gitCredentialsMap { } } +func (g *gitCredentialsMap) isEmpty() bool { + g.RLock() + defer g.RUnlock() + + return len(g.data) == 0 +} + func (g *gitCredentialsMap) get(host string) *hostCredentialMap { g.Lock() defer g.Unlock() diff --git a/internal/handlers/github_api.go b/internal/handlers/github_api.go index b65d24c..fba2871 100644 --- a/internal/handlers/github_api.go +++ b/internal/handlers/github_api.go @@ -52,7 +52,7 @@ func NewGitHubAPIHandler(creds config.Credentials, client ScopeRequester) *GitHu } } - if len(handler.credentials.data) == 0 { + if handler.credentials.isEmpty() && len(handler.jitAccessByHost) == 0 { logrus.Warn("GitHubAPIHandler has no app access tokens") } @@ -85,7 +85,7 @@ func (h *GitHubAPIHandler) HandleRequest(req *http.Request, ctx *goproxy.ProxyCt if !h.isHandledGitHubAPIRequest(req) { return req, nil } - if len(h.credentials.data) == 0 { + if h.credentials.isEmpty() { return req, nil } From f0c48edb97fceae40af5bfd9cf0febc243025e9a Mon Sep 17 00:00:00 2001 From: Hariharan Thavachelvam <164553783+thavaahariharangit@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:23:50 +0000 Subject: [PATCH 3/3] Address GitHub API JIT review feedback --- internal/handlers/git_server.go | 7 ---- internal/handlers/github_api.go | 51 ++++++++++++++++++++------ internal/handlers/github_api_test.go | 55 +++++++++++++++++++++------- proxy.go | 10 ++++- proxy_test.go | 19 ++++++++++ 5 files changed, 109 insertions(+), 33 deletions(-) diff --git a/internal/handlers/git_server.go b/internal/handlers/git_server.go index c2b0366..cb3b50b 100644 --- a/internal/handlers/git_server.go +++ b/internal/handlers/git_server.go @@ -64,13 +64,6 @@ func newGitCredentialsMap() *gitCredentialsMap { } } -func (g *gitCredentialsMap) isEmpty() bool { - g.RLock() - defer g.RUnlock() - - return len(g.data) == 0 -} - func (g *gitCredentialsMap) get(host string) *hostCredentialMap { g.Lock() defer g.Unlock() diff --git a/internal/handlers/github_api.go b/internal/handlers/github_api.go index fba2871..be13eaa 100644 --- a/internal/handlers/github_api.go +++ b/internal/handlers/github_api.go @@ -13,6 +13,7 @@ import ( "github.com/dependabot/proxy/internal/ctxdata" "github.com/dependabot/proxy/internal/helpers" "github.com/dependabot/proxy/internal/logging" + "github.com/dependabot/proxy/internal/threadsafe" ) // GitHubAPIHandler handles requests destined for the GitHub API, adding auth @@ -22,6 +23,8 @@ type GitHubAPIHandler struct { credentials *gitCredentialsMap jitAccessByHost map[string]jitAccessConfig client ScopeRequester + + reposAlreadyTried *threadsafe.Map[string, struct{}] } const ghAPIAddedAuthCtxKey = "gh-api.added-auth" @@ -31,10 +34,12 @@ const reservedProximaIdentity = "proxima-service-identity" // access token from the array of credentials func NewGitHubAPIHandler(creds config.Credentials, client ScopeRequester) *GitHubAPIHandler { handler := GitHubAPIHandler{ - credentials: newGitCredentialsMap(), - jitAccessByHost: map[string]jitAccessConfig{}, - client: client, + credentials: newGitCredentialsMap(), + jitAccessByHost: map[string]jitAccessConfig{}, + client: client, + reposAlreadyTried: threadsafe.NewMap[string, struct{}](), } + hasGitSourceCredentials := false for _, cred := range creds { switch cred["type"] { @@ -47,12 +52,15 @@ func NewGitHubAPIHandler(creds config.Credentials, client ScopeRequester) *GitHu continue } handler.credentials.addGitSourceCredentials("api."+host, cred) + hasGitSourceCredentials = true case "jit_access": - handler.addJITAccess(cred) + if client != nil { + handler.addJITAccess(cred) + } } } - if handler.credentials.isEmpty() && len(handler.jitAccessByHost) == 0 { + if !hasGitSourceCredentials && len(handler.jitAccessByHost) == 0 { logrus.Warn("GitHubAPIHandler has no app access tokens") } @@ -85,9 +93,6 @@ func (h *GitHubAPIHandler) HandleRequest(req *http.Request, ctx *goproxy.ProxyCt if !h.isHandledGitHubAPIRequest(req) { return req, nil } - if h.credentials.isEmpty() { - return req, nil - } host := helpers.GetHost(req) creds := getCredentialsForRequest(req, h.credentials, gitHubAPIExtractOrgAndRepo) @@ -118,12 +123,19 @@ func (h *GitHubAPIHandler) HandleResponse(rsp *http.Response, ctx *goproxy.Proxy return rsp } addedAuth, _ := ctxdata.GetBool(ctx, ghAPIAddedAuthCtxKey) - if !addedAuth && h.jitAccessByHost[helpers.GetHost(ctx.Req)].endpoint == "" { + if !addedAuth { return rsp } if !isPotentialAuthFailure(rsp.StatusCode) { return rsp } + repoKey, hasRepo := h.jitRepositoryKey(ctx.Req) + if hasRepo { + if _, ok := h.reposAlreadyTried.Get(repoKey); ok { + logging.RequestLogf(ctx, "* github api repository previously retried, won't retry again") + return rsp + } + } username, password, reqWasAuthed := ctx.Req.BasicAuth() for _, creds := range getCredentialsForRequest(ctx.Req, h.credentials, gitHubAPIExtractOrgAndRepo) { @@ -158,7 +170,13 @@ func (h *GitHubAPIHandler) HandleResponse(rsp *http.Response, ctx *goproxy.Proxy if jitCreds != nil { newReq := ctx.Req.Clone(ctx.Req.Context()) logging.RequestLogf(ctx, "* auth'd github api request failed authentication, retrying with jit access auth") - newReq.Header.Set("Authorization", "token "+jitCreds.password) + newReq.Header.Del("Authorization") + newReq.Header.Del("X-GitHub-PSI-JWT") + if jitCreds.username == reservedProximaIdentity { + newReq.Header.Set("X-GitHub-PSI-JWT", jitCreds.password) + } else { + newReq.Header.Set("Authorization", "token "+jitCreds.password) + } newRsp, err := ctx.RoundTrip(newReq) if err != nil { return rsp @@ -172,6 +190,9 @@ func (h *GitHubAPIHandler) HandleResponse(rsp *http.Response, ctx *goproxy.Proxy logging.RequestLogf(ctx, "* re-auth'd jit request returned %d, ignoring response", newRsp.StatusCode) helpers.DrainAndClose(newRsp) } + if hasRepo { + h.reposAlreadyTried.Set(repoKey, struct{}{}) + } return rsp } @@ -188,10 +209,10 @@ func (h *GitHubAPIHandler) getJITCredentialsForRequest(ctx *goproxy.ProxyCtx) *g return nil } - logging.RequestLogf(ctx, "* requesting JIT access for github api request") if h.client == nil { return nil } + logging.RequestLogf(ctx, "* requesting JIT access for github api request") credential, err := h.client.RequestJITAccess(ctx, jitConfig.endpoint, jitConfig.username, jitConfig.password, org, repo) if credential == nil || err != nil { return nil @@ -205,6 +226,14 @@ func (h *GitHubAPIHandler) getJITCredentialsForRequest(ctx *goproxy.ProxyCtx) *g return hostCreds.addToken(repoNWO, credential.GetString("username"), credential.GetString("password"), true) } +func (h *GitHubAPIHandler) jitRepositoryKey(req *http.Request) (string, bool) { + org, repo, ok := gitHubAPIExtractOrgAndRepo(req.URL.Path) + if !ok { + return "", false + } + return fmt.Sprintf("%s/%s/%s", helpers.GetHost(req), org, repo), true +} + func (h *GitHubAPIHandler) isHandledGitHubAPIRequest(req *http.Request) bool { return req.URL.Scheme == "https" && helpers.MethodPermitted(req, "GET", "HEAD") && helpers.CheckGitHubAPIHost(req) } diff --git a/internal/handlers/github_api_test.go b/internal/handlers/github_api_test.go index 41e579a..3a08eae 100644 --- a/internal/handlers/github_api_test.go +++ b/internal/handlers/github_api_test.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "io" "net/http" @@ -321,7 +322,6 @@ func TestGitHubAPIHandler_TokenFallback(t *testing.T) { testGitSourceCred("github.com", "x-access-token", userToken1), testGitSourceCred("github.com", "x-access-token", userToken2), } - handler := NewGitHubAPIHandler(credentials, nil) tests := []struct { name string @@ -390,6 +390,7 @@ func TestGitHubAPIHandler_TokenFallback(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + handler := NewGitHubAPIHandler(credentials, nil) var capturedTokens []string roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "token ") @@ -438,7 +439,6 @@ func TestGitHubAPIHandler_TokenFallback_In_Proxima(t *testing.T) { testGitSourceCred("foo.ghe.com", "x-access-token", userToken1), testGitSourceCred("foo.ghe.com", "x-access-token", userToken2), } - handler := NewGitHubAPIHandler(credentials, nil) url, err := url.Parse("https://api.foo.ghe.com/repos/github/dependabot-action") if err != nil { t.Errorf("parsing url: %v", err) @@ -496,6 +496,7 @@ func TestGitHubAPIHandler_TokenFallback_In_Proxima(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + handler := NewGitHubAPIHandler(credentials, nil) var capturedTokens []string roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "token ") @@ -537,11 +538,12 @@ func TestGitHubAPIHandler_TokenFallback_In_Proxima(t *testing.T) { type testGitHubAPIScopeRequester struct { callCount int result *config.Credential + err error } func (t *testGitHubAPIScopeRequester) RequestJITAccess(ctx *goproxy.ProxyCtx, endpoint string, username string, password string, account string, repo string) (*config.Credential, error) { t.callCount++ - return t.result, nil + return t.result, t.err } func TestGitHubAPIHandler_JITAccessFallback(t *testing.T) { @@ -590,8 +592,7 @@ func TestGitHubAPIHandler_JITAccessFallback(t *testing.T) { assert.Equal(t, 1, requester.callCount) } -func TestGitHubAPIHandler_JITAccessFallbackWithoutStaticCredentials(t *testing.T) { - jitToken := "ghp_jit" +func TestGitHubAPIHandler_DoesNotFallbackWithoutStaticCredentials(t *testing.T) { credentials := config.Credentials{ { "type": "jit_access", @@ -600,17 +601,12 @@ func TestGitHubAPIHandler_JITAccessFallbackWithoutStaticCredentials(t *testing.T "endpoint": "https://dependabot.example.com/jit_access", }, } - requester := &testGitHubAPIScopeRequester{ - result: &config.Credential{ - "username": "x-access-token", - "password": jitToken, - }, - } + requester := &testGitHubAPIScopeRequester{} handler := NewGitHubAPIHandler(credentials, requester) roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { - assert.Equal(t, "token "+jitToken, r.Header.Get("Authorization")) - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("jit-ok"))}, nil + t.Fatal("request without proxy-added auth should not be retried") + return nil, nil }) req := httptest.NewRequest("GET", "https://api.github.com/repos/example/internal-repo/releases", nil) ctx := &goproxy.ProxyCtx{Req: req, RoundTripper: roundTripper} @@ -620,8 +616,39 @@ func TestGitHubAPIHandler_JITAccessFallbackWithoutStaticCredentials(t *testing.T newRsp := handler.HandleResponse(rsp, ctx) defer newRsp.Body.Close() - assert.Equal(t, 200, newRsp.StatusCode) + assert.Equal(t, 404, newRsp.StatusCode) + assert.Equal(t, 0, requester.callCount) +} + +func TestGitHubAPIHandler_FailedJITAccessIsNotRetriedForRepository(t *testing.T) { + credentials := config.Credentials{ + testGitSourceCred("github.com", "x-access-token", "ghp_static"), + { + "type": "jit_access", + "credential-type": "git_source", + "host": "github.com", + "endpoint": "https://dependabot.example.com/jit_access", + }, + } + requester := &testGitHubAPIScopeRequester{err: errors.New("JIT access unavailable")} + handler := NewGitHubAPIHandler(credentials, requester) + roundTripCount := 0 + roundTripper := goproxy.RoundTripperFunc(func(r *http.Request, c *goproxy.ProxyCtx) (*http.Response, error) { + roundTripCount++ + return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("not found"))}, nil + }) + + for range 2 { + req := httptest.NewRequest("GET", "https://api.github.com/repos/example/internal-repo/releases", nil) + ctx := &goproxy.ProxyCtx{Req: req, RoundTripper: roundTripper} + rsp := &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("initial"))} + _ = handleRequestAndClose(handler, req, ctx) + newRsp := handler.HandleResponse(rsp, ctx) + newRsp.Body.Close() + } + assert.Equal(t, 1, requester.callCount) + assert.Equal(t, 1, roundTripCount) } func TestGitHubAPIHandler_JITAccessTokenIsCached(t *testing.T) { diff --git a/proxy.go b/proxy.go index 8f9e071..f2aa018 100644 --- a/proxy.go +++ b/proxy.go @@ -26,6 +26,10 @@ type Proxy struct { Close func() error } +func githubAPIJITAccessEnabled() bool { + return os.Getenv("PROXY_GITHUB_API_JIT_ACCESS") == "true" +} + func newProxy(envSettings config.ProxyEnvSettings, cfg *config.Config, blockedIps []net.IP) *Proxy { var err error @@ -77,7 +81,11 @@ func newProxy(envSettings config.ProxyEnvSettings, cfg *config.Config, blockedIp proxy.OnRequest().DoFunc(metricsHandler.HandleRequest) proxy.OnResponse().DoFunc(metricsHandler.HandleResponse) - gitHubAPIHandler := handlers.NewGitHubAPIHandler(cfg.Credentials, apiClient) + var gitHubAPIJITClient handlers.ScopeRequester + if githubAPIJITAccessEnabled() { + gitHubAPIJITClient = apiClient + } + gitHubAPIHandler := handlers.NewGitHubAPIHandler(cfg.Credentials, gitHubAPIJITClient) proxy.OnRequest().DoFunc(gitHubAPIHandler.HandleRequest) proxy.OnResponse().DoFunc(gitHubAPIHandler.HandleResponse) diff --git a/proxy_test.go b/proxy_test.go index ce0c049..3ac0116 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -29,6 +29,25 @@ var ( } ) +func TestGitHubAPIJITAccessEnabled(t *testing.T) { + tests := []struct { + value string + enabled bool + }{ + {value: "", enabled: false}, + {value: "false", enabled: false}, + {value: "TRUE", enabled: false}, + {value: "true", enabled: true}, + } + + for _, test := range tests { + t.Run(test.value, func(t *testing.T) { + t.Setenv("PROXY_GITHUB_API_JIT_ACCESS", test.value) + assert.Equal(t, test.enabled, githubAPIJITAccessEnabled()) + }) + } +} + func TestProxyHTTPRequest(t *testing.T) { var blockedIPs []net.IP client, proxy := testProxyServer(t, testProxyConfig, blockedIPs)