diff --git a/internal/handlers/cargo_registry.go b/internal/handlers/cargo_registry.go index aef718f..7109e34 100644 --- a/internal/handlers/cargo_registry.go +++ b/internal/handlers/cargo_registry.go @@ -47,10 +47,10 @@ type cargoRepositoryCredentials struct { password string } -func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandler { +func NewCargoRegistryHandler(credentials config.Credentials, client *http.Client) *CargoRegistryHandler { handler := CargoRegistryHandler{ credentials: []cargoRepositoryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, credential := range credentials { @@ -68,7 +68,7 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl if oidcCred, _, _ := handler.oidcRegistry.Register(credential, []string{"url"}, "cargo registry"); oidcCred != nil { continue } - } else if oidcCred, _ := oidc.CreateOIDCCredential(credential); oidcCred != nil { + } else if oidcCred, _ := oidc.CreateOIDCCredential(credential, client); oidcCred != nil { continue } diff --git a/internal/handlers/cargo_registry_test.go b/internal/handlers/cargo_registry_test.go index 9bf6a46..69f8dec 100644 --- a/internal/handlers/cargo_registry_test.go +++ b/internal/handlers/cargo_registry_test.go @@ -46,7 +46,7 @@ func TestCargoRegistryHandler(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // valid request, should authenticate url := validURL @@ -108,7 +108,7 @@ func TestCargoRegistryHandlerWithHost(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // matching host should authenticate req := httptest.NewRequestWithContext(t.Context(), "GET", "https://cargo.example.com/some/path", nil) @@ -139,7 +139,7 @@ func TestCargoRegistryHandlerWithUsernamePassword(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // matching url should authenticate with basic auth req := httptest.NewRequestWithContext(t.Context(), "GET", "https://cargo.example.com/registry/crate", nil) @@ -165,7 +165,7 @@ func TestCargoRegistryHandlerWithHostAndUsernamePassword(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // matching host should authenticate with basic auth req := httptest.NewRequestWithContext(t.Context(), "GET", "https://cargo.example.com/any/path", nil) @@ -191,7 +191,7 @@ func TestCargoRegistryHandlerTokenTakesPrecedenceOverPassword(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // token should take precedence over username/password req := httptest.NewRequestWithContext(t.Context(), "GET", "https://cargo.example.com/registry/crate", nil) @@ -207,7 +207,7 @@ func TestCargoRegistryHandlerIgnoresNoUrlOrHost(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // should not authenticate any request since no url or host was provided req := httptest.NewRequestWithContext(t.Context(), "GET", "https://anything.example.com/path", nil) @@ -227,7 +227,7 @@ func TestCargoRegistryHandlerUrlScopingNotBypassedByHost(t *testing.T) { }, } - handler := NewCargoRegistryHandler(credentials) + handler := NewCargoRegistryHandler(credentials, testOIDCClient) // in-scope path should authenticate req := httptest.NewRequestWithContext(t.Context(), "GET", "https://cargo.example.com/myorg/crate", nil) diff --git a/internal/handlers/composer.go b/internal/handlers/composer.go index 4546e7a..8533908 100644 --- a/internal/handlers/composer.go +++ b/internal/handlers/composer.go @@ -26,10 +26,10 @@ type composerCredentials struct { } // NewComposerHandler returns a new ComposerHandler. -func NewComposerHandler(creds config.Credentials) *ComposerHandler { +func NewComposerHandler(creds config.Credentials, client *http.Client) *ComposerHandler { handler := ComposerHandler{ credentials: []composerCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/composer_test.go b/internal/handlers/composer_test.go index 1327b85..e0018ed 100644 --- a/internal/handlers/composer_test.go +++ b/internal/handlers/composer_test.go @@ -64,7 +64,7 @@ func TestComposerHandler(t *testing.T) { "token": "", }, } - handler := NewComposerHandler(credentials) + handler := NewComposerHandler(credentials, testOIDCClient) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://phpreg.bigco.com/somepkg", nil) req = handleRequestAndClose(handler, req, nil) diff --git a/internal/handlers/docker_registry.go b/internal/handlers/docker_registry.go index e2bda23..4cf3197 100644 --- a/internal/handlers/docker_registry.go +++ b/internal/handlers/docker_registry.go @@ -29,7 +29,7 @@ type ecrClient interface { GetAuthorizationToken(ctx context.Context, input *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) } -type getECRClient func(ctx context.Context, region, keyID, secretKey string) (ecrClient, error) +type getECRClient func(ctx context.Context, region, keyID, secretKey string, client *http.Client) (ecrClient, error) // DockerRegistryHandler handles requests to Docker registries, adding auth. type DockerRegistryHandler struct { @@ -39,11 +39,12 @@ type DockerRegistryHandler struct { } // NewDockerRegistryHandler returns a new DockerRegistryHandler. -func NewDockerRegistryHandler(creds config.Credentials, transport http.RoundTripper, getECRClient getECRClient) *DockerRegistryHandler { +func NewDockerRegistryHandler(creds config.Credentials, client *http.Client, getECRClient getECRClient) *DockerRegistryHandler { + oidcRegistry := oidc.NewOIDCRegistry(client) handler := DockerRegistryHandler{ credentials: []*dockerRegistryCredentials{}, - transport: transport, - oidcRegistry: oidc.NewOIDCRegistry(), + transport: client.Transport, + oidcRegistry: oidcRegistry, } if getECRClient == nil { @@ -69,6 +70,7 @@ func NewDockerRegistryHandler(creds config.Credentials, transport http.RoundTrip registry: registry, username: cred.GetString("username"), password: cred.GetString("password"), + httpClient: client, getECRClient: getECRClient, } handler.credentials = append(handler.credentials, registryCred) @@ -147,11 +149,12 @@ func (h *DockerRegistryHandler) HandleRequest(req *http.Request, proxyCtx *gopro return req, nil } -func defaultGetECRClient(ctx context.Context, region, keyID, secretKey string) (ecrClient, error) { +func defaultGetECRClient(ctx context.Context, region, keyID, secretKey string, client *http.Client) (ecrClient, error) { cfg, err := awsconfig.LoadDefaultConfig( ctx, awsconfig.WithRegion(region), awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(keyID, secretKey, "")), + awsconfig.WithHTTPClient(client), ) if err != nil { return nil, err @@ -166,6 +169,7 @@ type dockerRegistryCredentials struct { password string ecrUsername string ecrPassword string + httpClient *http.Client getECRClient getECRClient } @@ -185,7 +189,7 @@ func (c *dockerRegistryCredentials) getECRCredentials(requestCtx context.Context } region := match[1] - ecrSvc, err := c.getECRClient(requestCtx, region, c.username, c.password) + ecrSvc, err := c.getECRClient(requestCtx, region, c.username, c.password, c.httpClient) if err != nil { logging.RequestLogf(proxyCtx, "! failed to initialize aws ecr client (key_id=%s)", c.username) return false diff --git a/internal/handlers/docker_registry_test.go b/internal/handlers/docker_registry_test.go index 7dd3036..6d691e7 100644 --- a/internal/handlers/docker_registry_test.go +++ b/internal/handlers/docker_registry_test.go @@ -3,8 +3,10 @@ package handlers import ( "context" "encoding/base64" + "io" "net/http" "net/http/httptest" + "strings" "testing" "github.com/aws/aws-sdk-go-v2/service/ecr" @@ -12,6 +14,7 @@ import ( "github.com/elazarl/goproxy" "github.com/stackrox/docker-registry-client/registry" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/dependabot/proxy/internal/config" ) @@ -60,15 +63,17 @@ func TestDockerRegistryHandler(t *testing.T) { }, } mockECR := &mockECRClient{user: ecrDockerUser, token: ecrDockerPassword} + httpClient := &http.Client{Transport: &http.Transport{}, Timeout: testOIDCClient.Timeout} var factoryContext context.Context - getECRClient := func(ctx context.Context, region, keyID, secretKey string) (ecrClient, error) { + getECRClient := func(ctx context.Context, region, keyID, secretKey string, client *http.Client) (ecrClient, error) { factoryContext = ctx + assert.Same(t, httpClient, client, "ECR uses the bounded handler client") assert.Equal(t, "us-east-2", region, "ecr region is parsed from the registry host") assert.Equal(t, ecrKeyID, keyID, "docker username is used as the aws access key id") assert.Equal(t, ecrSecretKey, secretKey, "docker password is used as the aws secret access key") return mockECR, nil } - handler := NewDockerRegistryHandler(credentials, &http.Transport{}, getECRClient) + handler := NewDockerRegistryHandler(credentials, httpClient, getECRClient) // Regular private registry req := httptest.NewRequestWithContext(t.Context(), "GET", "https://registry.hub.docker.com/my-repo", nil) @@ -164,6 +169,35 @@ func TestDockerRegistryHandler(t *testing.T) { assert.Equal(t, "https://nexus.someco.com", trans.URL, "correct URL is set") } +//nolint:gosec // The test credentials are intentionally fake fixtures. +func TestDefaultGetECRClientUsesInjectedHTTPClient(t *testing.T) { + transport := &recordingECRTransport{} + client := &http.Client{Transport: transport, Timeout: testOIDCClient.Timeout} + + ecrClient, err := defaultGetECRClient(t.Context(), "us-east-2", "access-key", "secret-key", client) + require.NoError(t, err) + _, err = ecrClient.GetAuthorizationToken(t.Context(), &ecr.GetAuthorizationTokenInput{}) + require.NoError(t, err) + + require.NotNil(t, transport.request) + assert.Equal(t, http.MethodPost, transport.request.Method) + assert.Equal(t, "api.ecr.us-east-2.amazonaws.com", transport.request.URL.Host) +} + +type recordingECRTransport struct { + request *http.Request +} + +func (t *recordingECRTransport) RoundTrip(req *http.Request) (*http.Response, error) { + t.request = req + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"authorizationData":[]}`)), + Header: make(http.Header), + Request: req, + }, nil +} + type mockECRClient struct { user string token string diff --git a/internal/handlers/goproxy_server_handler.go b/internal/handlers/goproxy_server_handler.go index f050070..e4889e1 100644 --- a/internal/handlers/goproxy_server_handler.go +++ b/internal/handlers/goproxy_server_handler.go @@ -24,10 +24,10 @@ type goProxyServerCredentials struct { } // NewGoProxyServerHandler returns a new GoProxyServerHandler. -func NewGoProxyServerHandler(creds config.Credentials) *GoProxyServerHandler { +func NewGoProxyServerHandler(creds config.Credentials, client *http.Client) *GoProxyServerHandler { handler := GoProxyServerHandler{ credentials: []goProxyServerCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/goproxy_server_handler_test.go b/internal/handlers/goproxy_server_handler_test.go index 54a3afd..c8e1f69 100644 --- a/internal/handlers/goproxy_server_handler_test.go +++ b/internal/handlers/goproxy_server_handler_test.go @@ -36,7 +36,7 @@ func TestGoProxyHandler(t *testing.T) { "password": deltaForcePassword, }, } - handler := NewGoProxyServerHandler(credentials) + handler := NewGoProxyServerHandler(credentials, testOIDCClient) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://corp.dependabot.com/packages/somepkg", nil) req = handleRequestAndClose(handler, req, nil) diff --git a/internal/handlers/helm_registry.go b/internal/handlers/helm_registry.go index b82aa2e..639ced0 100644 --- a/internal/handlers/helm_registry.go +++ b/internal/handlers/helm_registry.go @@ -24,10 +24,10 @@ type helmRegistryCredentials struct { } // NewHelmRegistryHandler returns a new HelmRegistryHandler. -func NewHelmRegistryHandler(creds config.Credentials) *HelmRegistryHandler { +func NewHelmRegistryHandler(creds config.Credentials, client *http.Client) *HelmRegistryHandler { handler := HelmRegistryHandler{ credentials: []helmRegistryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/helm_registry_test.go b/internal/handlers/helm_registry_test.go index cdeb825..129a28f 100644 --- a/internal/handlers/helm_registry_test.go +++ b/internal/handlers/helm_registry_test.go @@ -31,7 +31,7 @@ func TestHelmRegistryHandler(t *testing.T) { "password": bigCoPassword, }, } - handler := NewHelmRegistryHandler(credentials) + handler := NewHelmRegistryHandler(credentials, testOIDCClient) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://helmreg.bigco.com/some_chart", nil) req = handleRequestAndClose(handler, req, nil) diff --git a/internal/handlers/hex_repository.go b/internal/handlers/hex_repository.go index b923263..e7c2a9f 100644 --- a/internal/handlers/hex_repository.go +++ b/internal/handlers/hex_repository.go @@ -23,10 +23,10 @@ type hexRepositoryCredentials struct { authKey string } -func NewHexRepositoryHandler(creds config.Credentials) *HexRepositoryHandler { +func NewHexRepositoryHandler(creds config.Credentials, client *http.Client) *HexRepositoryHandler { handler := HexRepositoryHandler{ credentials: []hexRepositoryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { @@ -43,7 +43,7 @@ func NewHexRepositoryHandler(creds config.Credentials) *HexRepositoryHandler { if oidcCred, _, _ := handler.oidcRegistry.Register(cred, []string{"url"}, "hex repository"); oidcCred != nil { continue } - } else if oidcCred, _ := oidc.CreateOIDCCredential(cred); oidcCred != nil { + } else if oidcCred, _ := oidc.CreateOIDCCredential(cred, client); oidcCred != nil { continue } diff --git a/internal/handlers/hex_repository_test.go b/internal/handlers/hex_repository_test.go index 8ccdc0a..a31796a 100644 --- a/internal/handlers/hex_repository_test.go +++ b/internal/handlers/hex_repository_test.go @@ -29,7 +29,7 @@ func TestHexRepositoryHandler(t *testing.T) { validPath := "/repos/my_wonderful_repo/version" - handler := NewHexRepositoryHandler(credentials) + handler := NewHexRepositoryHandler(credentials, testOIDCClient) // valid request, should authenticate url := validConfigUrl + validPath diff --git a/internal/handlers/maven_repository.go b/internal/handlers/maven_repository.go index 948db01..f5dd9a9 100644 --- a/internal/handlers/maven_repository.go +++ b/internal/handlers/maven_repository.go @@ -25,10 +25,10 @@ type mavenRepositoryCredentials struct { } // NewMavenRepositoryHandler returns a new MavenRepositoryHandler. -func NewMavenRepositoryHandler(creds config.Credentials) *MavenRepositoryHandler { +func NewMavenRepositoryHandler(creds config.Credentials, client *http.Client) *MavenRepositoryHandler { handler := MavenRepositoryHandler{ credentials: []mavenRepositoryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/maven_repository_test.go b/internal/handlers/maven_repository_test.go index 7d89e1a..2e4f97a 100644 --- a/internal/handlers/maven_repository_test.go +++ b/internal/handlers/maven_repository_test.go @@ -36,7 +36,7 @@ func TestMavenRepositoryHandler(t *testing.T) { "password": deltaForcePassword, }, } - handler := NewMavenRepositoryHandler(credentials) + handler := NewMavenRepositoryHandler(credentials, testOIDCClient) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://corp.dependabot.com/packages/somepkg", nil) req = handleRequestAndClose(handler, req, nil) diff --git a/internal/handlers/npm_registry.go b/internal/handlers/npm_registry.go index c7d80b9..2542e1f 100644 --- a/internal/handlers/npm_registry.go +++ b/internal/handlers/npm_registry.go @@ -28,10 +28,10 @@ type npmRegistryCredentials struct { } // NewNPMRegistryHandler returns a new NPMRegistryHandler, -func NewNPMRegistryHandler(creds config.Credentials) *NPMRegistryHandler { +func NewNPMRegistryHandler(creds config.Credentials, client *http.Client) *NPMRegistryHandler { handler := NPMRegistryHandler{ credentials: []npmRegistryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/npm_registry_test.go b/internal/handlers/npm_registry_test.go index 47c1655..641d377 100644 --- a/internal/handlers/npm_registry_test.go +++ b/internal/handlers/npm_registry_test.go @@ -41,7 +41,7 @@ func TestNPMRegistryHandler(t *testing.T) { "token": privateRegToken, }, } - handler := NewNPMRegistryHandler(credentials) + handler := NewNPMRegistryHandler(credentials, testOIDCClient) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://registry.npmjs.org/private-package", nil) req = handleRequestAndClose(handler, req, nil) @@ -104,7 +104,7 @@ func TestNPMRegistryHandler_SameHostDifferentPaths(t *testing.T) { "token": teamBToken, }, } - handler := NewNPMRegistryHandler(credentials) + handler := NewNPMRegistryHandler(credentials, testOIDCClient) // Request to team-a path should use team-a token req := httptest.NewRequestWithContext(t.Context(), "GET", "https://artifactory.example.com/api/npm/team-a-npm/@scope/pkg", nil) diff --git a/internal/handlers/nuget_feed.go b/internal/handlers/nuget_feed.go index 40548b4..32e1b95 100644 --- a/internal/handlers/nuget_feed.go +++ b/internal/handlers/nuget_feed.go @@ -62,12 +62,12 @@ type nugetDiscoveryAuth struct { } // NewNugetFeedHandler returns a new NugetFeedHandler. -func NewNugetFeedHandler(creds config.Credentials) *NugetFeedHandler { +func NewNugetFeedHandler(creds config.Credentials, client *http.Client) *NugetFeedHandler { handler := NugetFeedHandler{ credentials: []nugetFeedCredentials{}, credentialURLs: make(map[string]struct{}), discoverySourceURLs: make(map[string]struct{}), - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/nuget_feed_test.go b/internal/handlers/nuget_feed_test.go index 8417b58..f479f42 100644 --- a/internal/handlers/nuget_feed_test.go +++ b/internal/handlers/nuget_feed_test.go @@ -114,7 +114,7 @@ func TestNugetFeedHandler(t *testing.T) { var buf bytes.Buffer testhelpers.CaptureStandardLog(t, &buf) - handler := NewNugetFeedHandler(credentials) + handler := newTestNugetFeedHandler(credentials) discoverNugetFeed(t, handler, "https://corp.dependabot.com/nuget/", http.StatusOK, mustMarshalJSON(t, rsp)) discoverNugetFeed(t, handler, "https://nuget.example.com/v2", http.StatusOK, xmlResponse) @@ -314,7 +314,7 @@ func TestExtraAuthenticatedURLsAreReportedInTheLog(t *testing.T) { var buf bytes.Buffer testhelpers.CaptureStandardLog(t, &buf) - handler := NewNugetFeedHandler(credentials) + handler := newTestNugetFeedHandler(credentials) discoverNugetFeed(t, handler, "https://nuget.example.com/index.json", http.StatusOK, jsonResponse) logContents := buf.String() @@ -327,7 +327,7 @@ func TestNewNugetFeedHandlerDoesNotMakeHTTPRequests(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - NewNugetFeedHandler(config.Credentials{ + newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://unreachable.example.com/index.json", "some-token"), }) @@ -335,7 +335,7 @@ func TestNewNugetFeedHandlerDoesNotMakeHTTPRequests(t *testing.T) { } func TestNugetFeedHandlerDiscoversFromPreparedResponse(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) discoverNugetFeed(t, handler, "https://nuget.example.com/index.json", http.StatusOK, @@ -347,7 +347,7 @@ func TestNugetFeedHandlerDiscoversFromPreparedResponse(t *testing.T) { } func TestNugetFeedHandlerSkipsDiscoveryFromUnsuccessfulResponse(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) discoverNugetFeed(t, handler, "https://nuget.example.com/index.json", http.StatusUnauthorized, @@ -359,7 +359,7 @@ func TestNugetFeedHandlerSkipsDiscoveryFromUnsuccessfulResponse(t *testing.T) { } func TestNugetFeedHandlerLeavesBodylessResponseUnchanged(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) @@ -377,7 +377,7 @@ func TestNugetFeedHandlerLeavesBodylessResponseUnchanged(t *testing.T) { } func TestNugetFeedHandlerReplaysBodyAfterReadError(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) proxyCtx := &goproxy.ProxyCtx{} @@ -397,7 +397,7 @@ func TestNugetFeedHandlerReplaysBodyAfterReadError(t *testing.T) { } func TestNugetFeedHandlerOnlyDiscoversFromConfiguredServiceIndex(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/v3/", "some-token"), }) proxyCtx := &goproxy.ProxyCtx{} @@ -450,7 +450,7 @@ func TestNugetFeedHandlerRequiresConfiguredServiceIndexSchemeForDiscovery(t *tes for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential(testCase.configuredURL, "some-token"), }) proxyCtx := &goproxy.ProxyCtx{} @@ -480,7 +480,7 @@ func TestNugetFeedHandlerKeepsHTTPAndHTTPSDiscoverySourcesDistinct(t *testing.T) {httpCredential, httpsCredential}, {httpsCredential, httpCredential}, } { - handler := NewNugetFeedHandler(credentials) + handler := newTestNugetFeedHandler(credentials) discoverNugetFeed(t, handler, "http://nuget.example.com/v3/index.json", http.StatusOK, nugetV3Response("https://http-resource.example.com/packages")) discoverNugetFeed(t, handler, "https://nuget.example.com/v3/index.json", http.StatusOK, @@ -497,7 +497,7 @@ func TestNugetFeedHandlerKeepsHTTPAndHTTPSDiscoverySourcesDistinct(t *testing.T) } func TestNugetFeedHandlerConcurrentDiscoveryIsDeduplicated(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) const workers = 50 @@ -546,7 +546,7 @@ func TestNugetFeedHandlerIgnoresUnusableStaticCredentials(t *testing.T) { {unusableCredential, usableCredential}, {usableCredential, unusableCredential}, } { - handler := NewNugetFeedHandler(credentials) + handler := newTestNugetFeedHandler(credentials) req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "https://nuget.example.com/v3/index.json", nil) req = handleRequestAndClose(handler, req, &goproxy.ProxyCtx{}) assertHasTokenAuth(t, req, "Bearer", "some-token", "usable duplicate credential") @@ -556,7 +556,7 @@ func TestNugetFeedHandlerIgnoresUnusableStaticCredentials(t *testing.T) { func TestNugetFeedHandlerLogsIgnoredDuplicateResourceURL(t *testing.T) { var buf bytes.Buffer testhelpers.CaptureStandardLog(t, &buf) - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://first.example.com/index.json", "first-token"), testNugetFeedCredential("https://second.example.com/index.json", "second-token"), }) @@ -572,7 +572,7 @@ func TestNugetFeedHandlerLogsIgnoredDuplicateResourceURL(t *testing.T) { } func TestNugetFeedHandlerUnusableCredentialDoesNotBlockDiscoveredCredential(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ config.Credential{ "type": "nuget_feed", "url": "https://cdn.example.com/packages", @@ -588,7 +588,7 @@ func TestNugetFeedHandlerUnusableCredentialDoesNotBlockDiscoveredCredential(t *t } func TestNugetFeedHandlerPrefersMostSpecificURLCredential(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/feed", "broad-token"), testNugetFeedCredential("https://nuget.example.com/feed/specific", "specific-token"), config.Credential{ @@ -605,7 +605,7 @@ func TestNugetFeedHandlerPrefersMostSpecificURLCredential(t *testing.T) { } func TestNugetFeedHandlerDiscoversThroughCrossOriginRedirectWithoutLeakingCredentials(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) @@ -646,7 +646,7 @@ func TestNugetFeedHandlerDiscoversThroughCrossOriginRedirectWithoutLeakingCreden } func TestNugetFeedHandlerAuthenticatesSameOriginServiceIndexRedirect(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential("https://nuget.example.com/index.json", "some-token"), }) @@ -687,7 +687,7 @@ func TestNugetFeedHandlerResolvesRelativeRedirectAgainstRequestedServiceIndexURL for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - handler := NewNugetFeedHandler(config.Credentials{ + handler := newTestNugetFeedHandler(config.Credentials{ testNugetFeedCredential(testCase.configuredURL, "some-token"), }) @@ -777,3 +777,7 @@ func (r *readErrorThenData) Read(p []byte) (int, error) { } func (r *readErrorThenData) Close() error { return nil } + +func newTestNugetFeedHandler(credentials config.Credentials) *NugetFeedHandler { + return NewNugetFeedHandler(credentials, testOIDCClient) +} diff --git a/internal/handlers/oidc_handling_test.go b/internal/handlers/oidc_handling_test.go index 92d2df4..0793a7e 100644 --- a/internal/handlers/oidc_handling_test.go +++ b/internal/handlers/oidc_handling_test.go @@ -43,7 +43,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Cargo", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewCargoRegistryHandler(creds) + return NewCargoRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -67,7 +67,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Cargo", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewCargoRegistryHandler(creds) + return NewCargoRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -88,7 +88,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Cargo", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewCargoRegistryHandler(creds) + return NewCargoRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -108,7 +108,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Cargo", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewCargoRegistryHandler(creds) + return NewCargoRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -130,7 +130,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Cargo", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewCargoRegistryHandler(creds) + return NewCargoRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -153,7 +153,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Composer", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewComposerHandler(creds) + return NewComposerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -177,7 +177,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Composer", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewComposerHandler(creds) + return NewComposerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -198,7 +198,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Composer", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewComposerHandler(creds) + return NewComposerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -219,7 +219,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Composer", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewComposerHandler(creds) + return NewComposerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -241,7 +241,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Composer", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewComposerHandler(creds) + return NewComposerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -265,7 +265,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Docker", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewDockerRegistryHandler(creds, &http.Transport{}, nil) + return NewDockerRegistryHandler(creds, testOIDCClient, nil) }, credentials: config.Credentials{ config.Credential{ @@ -289,7 +289,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Docker", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewDockerRegistryHandler(creds, &http.Transport{}, nil) + return NewDockerRegistryHandler(creds, testOIDCClient, nil) }, credentials: config.Credentials{ config.Credential{ @@ -310,7 +310,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Docker with URL", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewDockerRegistryHandler(creds, &http.Transport{}, nil) + return NewDockerRegistryHandler(creds, testOIDCClient, nil) }, credentials: config.Credentials{ config.Credential{ @@ -330,7 +330,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Docker", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewDockerRegistryHandler(creds, &http.Transport{}, nil) + return NewDockerRegistryHandler(creds, testOIDCClient, nil) }, credentials: config.Credentials{ config.Credential{ @@ -352,7 +352,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Docker", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewDockerRegistryHandler(creds, &http.Transport{}, nil) + return NewDockerRegistryHandler(creds, testOIDCClient, nil) }, credentials: config.Credentials{ config.Credential{ @@ -375,7 +375,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Go proxy", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewGoProxyServerHandler(creds) + return NewGoProxyServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -399,7 +399,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Go proxy with host", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewGoProxyServerHandler(creds) + return NewGoProxyServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -420,7 +420,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Go proxy", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewGoProxyServerHandler(creds) + return NewGoProxyServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -440,7 +440,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Go proxy", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewGoProxyServerHandler(creds) + return NewGoProxyServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -462,7 +462,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Go proxy", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewGoProxyServerHandler(creds) + return NewGoProxyServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -485,7 +485,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Helm registry", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHelmRegistryHandler(creds) + return NewHelmRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -509,7 +509,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Helm registry", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHelmRegistryHandler(creds) + return NewHelmRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -530,7 +530,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Helm registry with url", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHelmRegistryHandler(creds) + return NewHelmRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -550,7 +550,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Helm registry", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHelmRegistryHandler(creds) + return NewHelmRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -572,7 +572,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Helm registry", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHelmRegistryHandler(creds) + return NewHelmRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -595,7 +595,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Hex", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHexRepositoryHandler(creds) + return NewHexRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -619,7 +619,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Hex", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHexRepositoryHandler(creds) + return NewHexRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -640,7 +640,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Hex", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHexRepositoryHandler(creds) + return NewHexRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -660,7 +660,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Hex", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHexRepositoryHandler(creds) + return NewHexRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -682,7 +682,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Hex", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewHexRepositoryHandler(creds) + return NewHexRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -705,7 +705,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Maven", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewMavenRepositoryHandler(creds) + return NewMavenRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -729,7 +729,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Maven", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewMavenRepositoryHandler(creds) + return NewMavenRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -750,7 +750,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Maven", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewMavenRepositoryHandler(creds) + return NewMavenRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -770,7 +770,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Maven", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewMavenRepositoryHandler(creds) + return NewMavenRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -792,7 +792,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Maven", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewMavenRepositoryHandler(creds) + return NewMavenRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -815,7 +815,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NPM", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNPMRegistryHandler(creds) + return NewNPMRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -839,7 +839,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NPM", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNPMRegistryHandler(creds) + return NewNPMRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -860,7 +860,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NPM", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNPMRegistryHandler(creds) + return NewNPMRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -880,7 +880,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NPM", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNPMRegistryHandler(creds) + return NewNPMRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -902,7 +902,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NPM", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNPMRegistryHandler(creds) + return NewNPMRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -925,7 +925,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NuGet", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNugetFeedHandler(creds) + return NewNugetFeedHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -952,7 +952,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NuGet", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNugetFeedHandler(creds) + return NewNugetFeedHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -976,7 +976,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NuGet", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNugetFeedHandler(creds) + return NewNugetFeedHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -999,7 +999,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NuGet", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNugetFeedHandler(creds) + return NewNugetFeedHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1024,7 +1024,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "NuGet", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewNugetFeedHandler(creds) + return NewNugetFeedHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1050,7 +1050,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Pub", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPubRepositoryHandler(creds) + return NewPubRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1074,7 +1074,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Pub", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPubRepositoryHandler(creds) + return NewPubRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1095,7 +1095,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Pub", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPubRepositoryHandler(creds) + return NewPubRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1115,7 +1115,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Pub", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPubRepositoryHandler(creds) + return NewPubRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1137,7 +1137,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Pub", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPubRepositoryHandler(creds) + return NewPubRepositoryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1160,7 +1160,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Python", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPythonIndexHandler(creds) + return NewPythonIndexHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1184,7 +1184,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Python", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPythonIndexHandler(creds) + return NewPythonIndexHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1205,7 +1205,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Python", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPythonIndexHandler(creds) + return NewPythonIndexHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1225,7 +1225,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Python", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPythonIndexHandler(creds) + return NewPythonIndexHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1247,7 +1247,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Python", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewPythonIndexHandler(creds) + return NewPythonIndexHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1270,7 +1270,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "RubyGems", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewRubyGemsServerHandler(creds) + return NewRubyGemsServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1294,7 +1294,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "RubyGems", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewRubyGemsServerHandler(creds) + return NewRubyGemsServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1315,7 +1315,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "RubyGems", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewRubyGemsServerHandler(creds) + return NewRubyGemsServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1336,7 +1336,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "RubyGems", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewRubyGemsServerHandler(creds) + return NewRubyGemsServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1359,7 +1359,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "RubyGems", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewRubyGemsServerHandler(creds) + return NewRubyGemsServerHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1383,7 +1383,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Terraform", provider: "aws", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewTerraformRegistryHandler(creds) + return NewTerraformRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1407,7 +1407,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Terraform with host", provider: "azure", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewTerraformRegistryHandler(creds) + return NewTerraformRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1428,7 +1428,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Terraform", provider: "jfrog", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewTerraformRegistryHandler(creds) + return NewTerraformRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1448,7 +1448,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Terraform", provider: "cloudsmith", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewTerraformRegistryHandler(creds) + return NewTerraformRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1470,7 +1470,7 @@ func TestOIDCURLsAreAuthenticated(t *testing.T) { name: "Terraform", provider: "gcp", handlerFactory: func(creds config.Credentials) oidcHandler { - return NewTerraformRegistryHandler(creds) + return NewTerraformRegistryHandler(creds, testOIDCClient) }, credentials: config.Credentials{ config.Credential{ @@ -1636,7 +1636,7 @@ func TestPythonOIDCSimpleSuffixStripping(t *testing.T) { }, } - handler := NewPythonIndexHandler(creds) + handler := NewPythonIndexHandler(creds, testOIDCClient) // /+simple/ should be stripped → registered as /org/feed-A/ reqA := httptest.NewRequestWithContext(t.Context(), "GET", "https://pkgs.example.com/org/feed-A/pkg/a", nil) @@ -1672,7 +1672,7 @@ func TestPythonOIDCAuthenticatesDiscoveredDownloadPrefix(t *testing.T) { "tenant-id": tenantID, "client-id": clientID, }, - }) + }, testOIDCClient) proxyCtx := &goproxy.ProxyCtx{} indexReq := httptest.NewRequestWithContext(t.Context(), @@ -1746,7 +1746,7 @@ func TestNPMOIDCSameHostDifferentPaths(t *testing.T) { }, } - handler := NewNPMRegistryHandler(creds) + handler := NewNPMRegistryHandler(creds, testOIDCClient) // Request to feed-A path should get token A reqA := httptest.NewRequestWithContext(t.Context(), "GET", "https://pkgs.example.com/org/feed-A/some-package", nil) @@ -1799,7 +1799,7 @@ func TestTerraformOIDCSameHostDifferentPaths(t *testing.T) { }, } - handler := NewTerraformRegistryHandler(creds) + handler := NewTerraformRegistryHandler(creds, testOIDCClient) // Request to feed-A path should get token A reqA := httptest.NewRequestWithContext(t.Context(), "GET", "https://terraform.example.com/org/feed-A/v1/providers/org/name", nil) diff --git a/internal/handlers/opentofu_registry.go b/internal/handlers/opentofu_registry.go index d98da66..2a0d54b 100644 --- a/internal/handlers/opentofu_registry.go +++ b/internal/handlers/opentofu_registry.go @@ -23,10 +23,10 @@ type openTofuRegistryCredentials struct { token string } -func NewOpenTofuRegistryHandler(credentials config.Credentials) *OpenTofuRegistryHandler { +func NewOpenTofuRegistryHandler(credentials config.Credentials, client *http.Client) *OpenTofuRegistryHandler { handler := OpenTofuRegistryHandler{ credentials: []openTofuRegistryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, credential := range credentials { diff --git a/internal/handlers/opentofu_registry_test.go b/internal/handlers/opentofu_registry_test.go index 6ab6a43..bb8acee 100644 --- a/internal/handlers/opentofu_registry_test.go +++ b/internal/handlers/opentofu_registry_test.go @@ -65,7 +65,7 @@ func TestOpenTofuRegistryHandler(t *testing.T) { } for _, tt := range tests { t.Run(strings.Join([]string{tt.registryType, tt.host, tt.token}, " "), func(t *testing.T) { - handler := NewOpenTofuRegistryHandler(tt.credentials) + handler := NewOpenTofuRegistryHandler(tt.credentials, testOIDCClient) request := handleRequestAndClose(handler, httptest.NewRequestWithContext(t.Context(), "GET", tt.url, nil), nil) @@ -74,7 +74,7 @@ func TestOpenTofuRegistryHandler(t *testing.T) { } t.Run("HandleRequest without credentials", func(t *testing.T) { - handler := NewOpenTofuRegistryHandler(config.Credentials{}) + handler := NewOpenTofuRegistryHandler(config.Credentials{}, testOIDCClient) url := "https://registry.opentofu.org/v1/providers/org/name/versions" request := handleRequestAndClose(handler, httptest.NewRequestWithContext(t.Context(), "GET", url, nil), nil) @@ -87,7 +87,7 @@ func TestOpenTofuRegistryHandler(t *testing.T) { config.Credential{"type": "opentofu_registry", "url": "https://registry.example.com/org1", "token": "token-org1"}, config.Credential{"type": "opentofu_registry", "url": "https://registry.example.com/org2", "token": "token-org2"}, } - handler := NewOpenTofuRegistryHandler(credentials) + handler := NewOpenTofuRegistryHandler(credentials, testOIDCClient) // Request to org1 path should use org1 token req1 := handleRequestAndClose(handler, httptest.NewRequestWithContext(t.Context(), "GET", "https://registry.example.com/org1/v1/providers/foo", nil), nil) @@ -106,7 +106,7 @@ func TestOpenTofuRegistryHandler(t *testing.T) { credentials := config.Credentials{ config.Credential{"type": "opentofu_registry", "host": "registry.example.org", "token": ""}, } - handler := NewOpenTofuRegistryHandler(credentials) + handler := NewOpenTofuRegistryHandler(credentials, testOIDCClient) assert.Equal(t, 0, len(handler.credentials), "should skip credential with empty token") }) @@ -114,7 +114,7 @@ func TestOpenTofuRegistryHandler(t *testing.T) { credentials := config.Credentials{ config.Credential{"type": "opentofu_registry", "token": "some-token"}, } - handler := NewOpenTofuRegistryHandler(credentials) + handler := NewOpenTofuRegistryHandler(credentials, testOIDCClient) assert.Equal(t, 0, len(handler.credentials), "should skip credential with empty host and url") }) @@ -124,7 +124,7 @@ func TestOpenTofuRegistryHandler(t *testing.T) { config.Credential{"type": "opentofu_registry", "url": "https://registry.example.com/org", "token": "token-org"}, config.Credential{"type": "opentofu_registry", "url": "https://registry.example.com/org1", "token": "token-org1"}, } - handler := NewOpenTofuRegistryHandler(credentials) + handler := NewOpenTofuRegistryHandler(credentials, testOIDCClient) assert.Equal(t, "https://registry.example.com/org1", handler.credentials[0].url, "longer path should be first") assert.Equal(t, "https://registry.example.com/org", handler.credentials[1].url, "shorter path should be second") diff --git a/internal/handlers/pub_repository.go b/internal/handlers/pub_repository.go index adc8cf1..3086174 100644 --- a/internal/handlers/pub_repository.go +++ b/internal/handlers/pub_repository.go @@ -25,10 +25,10 @@ type pubRepositoryCredentials struct { token string } -func NewPubRepositoryHandler(credentials config.Credentials) *PubRepositoryHandler { +func NewPubRepositoryHandler(credentials config.Credentials, client *http.Client) *PubRepositoryHandler { handler := PubRepositoryHandler{ credentials: []pubRepositoryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, credential := range credentials { @@ -45,7 +45,7 @@ func NewPubRepositoryHandler(credentials config.Credentials) *PubRepositoryHandl if oidcCred, _, _ := handler.oidcRegistry.Register(credential, []string{"url"}, "pub repository"); oidcCred != nil { continue } - } else if oidcCred, _ := oidc.CreateOIDCCredential(credential); oidcCred != nil { + } else if oidcCred, _ := oidc.CreateOIDCCredential(credential, client); oidcCred != nil { continue } diff --git a/internal/handlers/pub_repository_test.go b/internal/handlers/pub_repository_test.go index 98186e7..02aff70 100644 --- a/internal/handlers/pub_repository_test.go +++ b/internal/handlers/pub_repository_test.go @@ -46,7 +46,7 @@ func TestPubRepositoryHandler(t *testing.T) { }, } - handler := NewPubRepositoryHandler(credentials) + handler := NewPubRepositoryHandler(credentials, testOIDCClient) // valid request, should authenticate url := validURL diff --git a/internal/handlers/python_index.go b/internal/handlers/python_index.go index 4dc92b8..cb36ab7 100644 --- a/internal/handlers/python_index.go +++ b/internal/handlers/python_index.go @@ -29,11 +29,11 @@ type pythonIndexCredentials struct { } // NewPythonIndexHandler returns a new PythonIndexHandler. -func NewPythonIndexHandler(creds config.Credentials) *PythonIndexHandler { +func NewPythonIndexHandler(creds config.Credentials, client *http.Client) *PythonIndexHandler { handler := PythonIndexHandler{ credentials: []pythonIndexCredentials{}, downloadAuth: newPythonIndexDownloadAuthStore(), - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { @@ -43,7 +43,7 @@ func NewPythonIndexHandler(creds config.Credentials) *PythonIndexHandler { indexURL := cred.GetString("index-url") - oidcCredential, _ := oidc.CreateOIDCCredential(cred) + oidcCredential, _ := oidc.CreateOIDCCredential(cred, client) if oidcCredential != nil { // Normalize the registration URL by stripping the /simple or /+simple // suffix, matching how static credentials are matched at request time. diff --git a/internal/handlers/python_index_test.go b/internal/handlers/python_index_test.go index ce3dfd4..c6c5ff2 100644 --- a/internal/handlers/python_index_test.go +++ b/internal/handlers/python_index_test.go @@ -55,7 +55,7 @@ func TestPythonIndexHandler(t *testing.T) { "token": fmt.Sprintf("%s:%s", deltaForceUser, deltaForcePassword), }, } - handler := NewPythonIndexHandler(credentials) + handler := newTestPythonIndexHandler(credentials) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://corp.dependabot.com/pyreg", nil) req = handleRequestAndClose(handler, req, nil) @@ -115,7 +115,7 @@ func TestPythonIndexHandler(t *testing.T) { } func TestPythonIndexHandlerAuthenticatesDiscoveredDownloadPrefixFromHTML(t *testing.T) { - handler := NewPythonIndexHandler(config.Credentials{ + handler := newTestPythonIndexHandler(config.Credentials{ config.Credential{ "type": "python_index", "index-url": "https://pkgs.example.com/my-org/my-project/_packaging/my-feed/pypi/simple/", @@ -177,7 +177,7 @@ func TestPythonIndexHandlerAuthenticatesDiscoveredDownloadPrefixFromHTML(t *test } func TestPythonIndexHandlerAuthenticatesDiscoveredDownloadPrefixFromJSON(t *testing.T) { - handler := NewPythonIndexHandler(config.Credentials{ + handler := newTestPythonIndexHandler(config.Credentials{ config.Credential{ "type": "python_index", "index-url": "https://pkgs.example.com/my-org/my-project/_packaging/my-feed/pypi/simple/", @@ -255,7 +255,7 @@ func TestPythonDownloadPrefixFromSimpleLinkRejectsUnscopedLinks(t *testing.T) { } func TestPythonIndexHandlerSkipsDiscoveryForAuthenticatedNonSimpleResponse(t *testing.T) { - handler := NewPythonIndexHandler(config.Credentials{ + handler := newTestPythonIndexHandler(config.Credentials{ config.Credential{ "type": "python_index", "index-url": "https://pkgs.example.com/org/project/", @@ -292,7 +292,7 @@ func TestPythonIndexHandlerSkipsDiscoveryForAuthenticatedNonSimpleResponse(t *te } func TestPythonIndexHandlerPreservesDiscoveredDownloadPrefixPort(t *testing.T) { - handler := NewPythonIndexHandler(config.Credentials{ + handler := newTestPythonIndexHandler(config.Credentials{ config.Credential{ "type": "python_index", "index-url": "https://pkgs.example.com:8443/my-org/my-project/_packaging/my-feed/pypi/simple/", @@ -341,7 +341,7 @@ func TestPythonIndexHandlerPreservesDiscoveredDownloadPrefixPort(t *testing.T) { } func TestPythonIndexHandlerPreservesDiscoveredDownloadPrefixIPv6Host(t *testing.T) { - handler := NewPythonIndexHandler(config.Credentials{ + handler := newTestPythonIndexHandler(config.Credentials{ config.Credential{ "type": "python_index", "index-url": "https://[2001:db8::1]/my-org/my-project/_packaging/my-feed/pypi/simple/", @@ -424,7 +424,7 @@ func TestPythonIndexDownloadAuthStoreEvictsOldestEntryAtLimit(t *testing.T) { } func TestPythonIndexHandlerSkipsDiscoveryForLargeSimpleResponse(t *testing.T) { - handler := NewPythonIndexHandler(config.Credentials{ + handler := newTestPythonIndexHandler(config.Credentials{ config.Credential{ "type": "python_index", "index-url": "https://pkgs.example.com/my-org/my-project/_packaging/my-feed/pypi/simple/", @@ -461,3 +461,7 @@ func TestPythonIndexHandlerSkipsDiscoveryForLargeSimpleResponse(t *testing.T) { downloadReq = handleRequestAndClose(handler, downloadReq, &goproxy.ProxyCtx{}) assertUnauthenticated(t, downloadReq, "large Simple API response should not be used for discovery") } + +func newTestPythonIndexHandler(credentials config.Credentials) *PythonIndexHandler { + return NewPythonIndexHandler(credentials, testOIDCClient) +} diff --git a/internal/handlers/rubygems_server.go b/internal/handlers/rubygems_server.go index 4ad797f..37ddf64 100644 --- a/internal/handlers/rubygems_server.go +++ b/internal/handlers/rubygems_server.go @@ -25,10 +25,10 @@ type rubyGemsServerCredentials struct { } // NewRubyGemsServerHandler returns a new RubyGemsServerHandler. -func NewRubyGemsServerHandler(creds config.Credentials) *RubyGemsServerHandler { +func NewRubyGemsServerHandler(creds config.Credentials, client *http.Client) *RubyGemsServerHandler { handler := RubyGemsServerHandler{ credentials: []rubyGemsServerCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, cred := range creds { diff --git a/internal/handlers/rubygems_server_test.go b/internal/handlers/rubygems_server_test.go index 128afa7..acb8ca7 100644 --- a/internal/handlers/rubygems_server_test.go +++ b/internal/handlers/rubygems_server_test.go @@ -37,7 +37,7 @@ func TestRubyGemsServerHandler(t *testing.T) { "token": fmt.Sprintf("%s:%s", pathUser, pathPassword), }, } - handler := NewRubyGemsServerHandler(credentials) + handler := NewRubyGemsServerHandler(credentials, testOIDCClient) req := httptest.NewRequestWithContext(t.Context(), "GET", "https://corp.dependabot.com/gems", nil) req = handleRequestAndClose(handler, req, nil) diff --git a/internal/handlers/terraform_registry.go b/internal/handlers/terraform_registry.go index 2430ab3..5e2d7f8 100644 --- a/internal/handlers/terraform_registry.go +++ b/internal/handlers/terraform_registry.go @@ -24,10 +24,10 @@ type terraformRegistryCredentials struct { token string } -func NewTerraformRegistryHandler(credentials config.Credentials) *TerraformRegistryHandler { +func NewTerraformRegistryHandler(credentials config.Credentials, client *http.Client) *TerraformRegistryHandler { handler := TerraformRegistryHandler{ credentials: []terraformRegistryCredentials{}, - oidcRegistry: oidc.NewOIDCRegistry(), + oidcRegistry: oidc.NewOIDCRegistry(client), } for _, credential := range credentials { diff --git a/internal/handlers/terraform_registry_test.go b/internal/handlers/terraform_registry_test.go index 401c923..2f835ef 100644 --- a/internal/handlers/terraform_registry_test.go +++ b/internal/handlers/terraform_registry_test.go @@ -58,7 +58,7 @@ func TestTerraformRegistryHandler(t *testing.T) { } for _, tt := range tests { t.Run(strings.Join([]string{tt.registryType, tt.host, tt.token}, " "), func(t *testing.T) { - handler := NewTerraformRegistryHandler(tt.credentials) + handler := NewTerraformRegistryHandler(tt.credentials, testOIDCClient) request := handleRequestAndClose(handler, httptest.NewRequestWithContext(t.Context(), "GET", tt.url, nil), nil) @@ -67,7 +67,7 @@ func TestTerraformRegistryHandler(t *testing.T) { } t.Run("HandleRequest without credentials", func(t *testing.T) { - handler := NewTerraformRegistryHandler(config.Credentials{}) + handler := NewTerraformRegistryHandler(config.Credentials{}, testOIDCClient) url := "https://registry.terraform.io/v1/providers/org/name/versions" request := handleRequestAndClose(handler, httptest.NewRequestWithContext(t.Context(), "GET", url, nil), nil) @@ -80,7 +80,7 @@ func TestTerraformRegistryHandler(t *testing.T) { config.Credential{"type": "terraform_registry", "url": "https://terraform.example.com/org1", "token": "token-org1"}, config.Credential{"type": "terraform_registry", "url": "https://terraform.example.com/org2", "token": "token-org2"}, } - handler := NewTerraformRegistryHandler(credentials) + handler := NewTerraformRegistryHandler(credentials, testOIDCClient) // Request to org1 path should use org1 token req1 := handleRequestAndClose(handler, httptest.NewRequestWithContext(t.Context(), "GET", "https://terraform.example.com/org1/v1/providers/foo", nil), nil) @@ -99,7 +99,7 @@ func TestTerraformRegistryHandler(t *testing.T) { credentials := config.Credentials{ config.Credential{"type": "terraform_registry", "host": "terraform.example.org", "token": ""}, } - handler := NewTerraformRegistryHandler(credentials) + handler := NewTerraformRegistryHandler(credentials, testOIDCClient) assert.Equal(t, 0, len(handler.credentials), "should skip credential with empty token") }) @@ -107,7 +107,7 @@ func TestTerraformRegistryHandler(t *testing.T) { credentials := config.Credentials{ config.Credential{"type": "terraform_registry", "token": "some-token"}, } - handler := NewTerraformRegistryHandler(credentials) + handler := NewTerraformRegistryHandler(credentials, testOIDCClient) assert.Equal(t, 0, len(handler.credentials), "should skip credential with empty host and url") }) @@ -117,7 +117,7 @@ func TestTerraformRegistryHandler(t *testing.T) { config.Credential{"type": "terraform_registry", "url": "https://terraform.example.com/org", "token": "token-org"}, config.Credential{"type": "terraform_registry", "url": "https://terraform.example.com/org1", "token": "token-org1"}, } - handler := NewTerraformRegistryHandler(credentials) + handler := NewTerraformRegistryHandler(credentials, testOIDCClient) assert.Equal(t, "https://terraform.example.com/org1", handler.credentials[0].url, "longer path should be first") assert.Equal(t, "https://terraform.example.com/org", handler.credentials[1].url, "shorter path should be second") diff --git a/internal/handlers/test_helpers.go b/internal/handlers/test_helpers.go index 3541323..82cde71 100644 --- a/internal/handlers/test_helpers.go +++ b/internal/handlers/test_helpers.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" @@ -13,6 +14,17 @@ import ( "github.com/dependabot/proxy/internal/config" ) +var testOIDCClient = &http.Client{ + Transport: currentDefaultTransport{}, + Timeout: 10 * time.Second, +} + +type currentDefaultTransport struct{} + +func (currentDefaultTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return http.DefaultTransport.RoundTrip(req) +} + // handleRequestAndClose calls handler.HandleRequest and closes any response body. // Most handlers return nil responses, but the linter can't prove that. func handleRequestAndClose(handler interface { diff --git a/internal/oidc/actions_oidc.go b/internal/oidc/actions_oidc.go index 93e857f..f37a957 100644 --- a/internal/oidc/actions_oidc.go +++ b/internal/oidc/actions_oidc.go @@ -55,8 +55,11 @@ func GetRequestToken() string { return os.Getenv(envActionsIDTokenRequestToken) } -// GetToken retrieves a GitHub Actions OIDC token with an optional audience -func GetToken(ctx context.Context, audience string) (_ string, err error) { +// GetToken retrieves a GitHub Actions OIDC token with an optional audience. +func GetToken(ctx context.Context, audience string, client *http.Client) (_ string, err error) { + if err := validateHTTPClient(client); err != nil { + return "", err + } if !IsOIDCConfigured() { return "", fmt.Errorf("GitHub Actions OIDC is not available: missing %s or %s environment variables", envActionsIDTokenRequestURL, envActionsIDTokenRequestToken) @@ -76,10 +79,6 @@ func GetToken(ctx context.Context, audience string) (_ string, err error) { parsedURL.RawQuery = query.Encode() } - client := &http.Client{ - Timeout: 10 * time.Second, - } - req, err := http.NewRequestWithContext(ctx, "GET", parsedURL.String(), nil) if err != nil { return "", fmt.Errorf("failed to create OIDC request: %w", err) @@ -118,8 +117,21 @@ func GetToken(ctx context.Context, audience string) (_ string, err error) { // GetTokenForAzureADExchange retrieves a GitHub Actions OIDC token specifically // configured for Azure AD token exchange -func GetTokenForAzureADExchange(ctx context.Context) (string, error) { - return GetToken(ctx, "api://AzureADTokenExchange") +func GetTokenForAzureADExchange(ctx context.Context, client *http.Client) (string, error) { + return GetToken(ctx, "api://AzureADTokenExchange", client) +} + +func validateHTTPClient(client *http.Client) error { + if client == nil { + return fmt.Errorf("OIDC HTTP client is required") + } + if client.Transport == nil { + return fmt.Errorf("OIDC HTTP client transport is required") + } + if client.Timeout <= 0 { + return fmt.Errorf("OIDC HTTP client timeout must be positive") + } + return nil } // azureTokenResponse represents the response from Azure AD OAuth2 token endpoint @@ -228,7 +240,10 @@ type OIDCAccessToken struct { // githubToken: The GitHub Actions OIDC token obtained via GetTokenForAzureADExchange // // Returns an Azure AD access token scoped for Azure DevOps (499b84ac-1321-427f-aa17-267ca6975798/.default) -func GetAzureAccessToken(ctx context.Context, params AzureOIDCParameters, githubToken string) (_ *OIDCAccessToken, err error) { +func GetAzureAccessToken(ctx context.Context, params AzureOIDCParameters, githubToken string, client *http.Client) (_ *OIDCAccessToken, err error) { + if err := validateHTTPClient(client); err != nil { + return nil, err + } if params.TenantID == "" { return nil, fmt.Errorf("tenant ID is required") } @@ -252,10 +267,6 @@ func GetAzureAccessToken(ctx context.Context, params AzureOIDCParameters, github formData.Set("client_assertion", githubToken) formData.Set("grant_type", "client_credentials") - client := &http.Client{ - Timeout: 10 * time.Second, - } - req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(formData.Encode())) if err != nil { return nil, fmt.Errorf("failed to create Azure token request: %w", err) @@ -296,19 +307,19 @@ func GetAzureAccessToken(ctx context.Context, params AzureOIDCParameters, github // GetAzureAccessTokenForDevOps is a convenience function that combines fetching the GitHub OIDC token // and exchanging it for an Azure AD access token in a single call. -func GetAzureAccessTokenForDevOps(ctx context.Context, params AzureOIDCParameters) (*OIDCAccessToken, error) { +func GetAzureAccessTokenForDevOps(ctx context.Context, params AzureOIDCParameters, client *http.Client) (*OIDCAccessToken, error) { if !IsOIDCConfigured() { return nil, fmt.Errorf("GitHub Actions OIDC is not configured") } // Get GitHub OIDC token - githubToken, err := GetTokenForAzureADExchange(ctx) + githubToken, err := GetTokenForAzureADExchange(ctx, client) if err != nil { return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err) } // Exchange for Azure token - azureToken, err := GetAzureAccessToken(ctx, params, githubToken) + azureToken, err := GetAzureAccessToken(ctx, params, githubToken, client) if err != nil { return nil, fmt.Errorf("failed to exchange GitHub token for Azure token: %w", err) } @@ -324,7 +335,10 @@ func GetAzureAccessTokenForDevOps(ctx context.Context, params AzureOIDCParameter // githubToken: The GitHub Actions OIDC token obtained via GetToken // // Returns a JFrog access token -func GetJFrogAccessToken(ctx context.Context, params JFrogOIDCParameters, githubToken string) (_ *OIDCAccessToken, err error) { +func GetJFrogAccessToken(ctx context.Context, params JFrogOIDCParameters, githubToken string, client *http.Client) (_ *OIDCAccessToken, err error) { + if err := validateHTTPClient(client); err != nil { + return nil, err + } if params.JFrogURL == "" { return nil, fmt.Errorf("token URL base is required") } @@ -359,9 +373,6 @@ func GetJFrogAccessToken(ctx context.Context, params JFrogOIDCParameters, github req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "dependabot-proxy/1.0") - client := &http.Client{ - Timeout: 10 * time.Second, - } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute JFrog token request: %w", err) @@ -397,19 +408,19 @@ func GetJFrogAccessToken(ctx context.Context, params JFrogOIDCParameters, github }, nil } -func GetJFrogAccessTokenForDevOps(ctx context.Context, params JFrogOIDCParameters) (*OIDCAccessToken, error) { +func GetJFrogAccessTokenForDevOps(ctx context.Context, params JFrogOIDCParameters, client *http.Client) (*OIDCAccessToken, error) { if !IsOIDCConfigured() { return nil, fmt.Errorf("GitHub Actions OIDC is not configured") } // Get GitHub OIDC token - githubToken, err := GetToken(ctx, params.Audience) + githubToken, err := GetToken(ctx, params.Audience, client) if err != nil { return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err) } // Exchange for JFrog token - jfrogToken, err := GetJFrogAccessToken(ctx, params, githubToken) + jfrogToken, err := GetJFrogAccessToken(ctx, params, githubToken, client) if err != nil { return nil, fmt.Errorf("failed to exchange GitHub token for JFrog token: %w", err) } @@ -425,7 +436,10 @@ func GetJFrogAccessTokenForDevOps(ctx context.Context, params JFrogOIDCParameter // githubToken: The GitHub Actions OIDC token obtained via GetToken // // Returns temporary AWS credentials -func GetAWSAccessToken(ctx context.Context, params AWSOIDCParameters, githubToken string) (_ *OIDCAccessToken, err error) { +func GetAWSAccessToken(ctx context.Context, params AWSOIDCParameters, githubToken string, client *http.Client) (_ *OIDCAccessToken, err error) { + if err := validateHTTPClient(client); err != nil { + return nil, err + } if params.Region == "" { return nil, fmt.Errorf("AWS region is required") } @@ -453,10 +467,6 @@ func GetAWSAccessToken(ctx context.Context, params AWSOIDCParameters, githubToke formData.Set("RoleSessionName", "dependabot-update") formData.Set("WebIdentityToken", githubToken) - client := &http.Client{ - Timeout: 10 * time.Second, - } - req, err := http.NewRequestWithContext(ctx, "POST", awsCodeArtifactSTSRequestUrl, strings.NewReader(formData.Encode())) if err != nil { return nil, fmt.Errorf("failed to create AWS credential request: %w", err) @@ -573,19 +583,19 @@ func GetAWSAccessToken(ctx context.Context, params AWSOIDCParameters, githubToke }, nil } -func GetAWSAccessTokenForDevOps(ctx context.Context, params AWSOIDCParameters) (*OIDCAccessToken, error) { +func GetAWSAccessTokenForDevOps(ctx context.Context, params AWSOIDCParameters, client *http.Client) (*OIDCAccessToken, error) { if !IsOIDCConfigured() { return nil, fmt.Errorf("GitHub Actions OIDC is not configured") } // Get GitHub OIDC token - githubToken, err := GetToken(ctx, params.Audience) + githubToken, err := GetToken(ctx, params.Audience, client) if err != nil { return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err) } // Exchange for AWS token - awsToken, err := GetAWSAccessToken(ctx, params, githubToken) + awsToken, err := GetAWSAccessToken(ctx, params, githubToken, client) if err != nil { return nil, fmt.Errorf("failed to exchange GitHub token for AWS token: %w", err) } @@ -593,7 +603,10 @@ func GetAWSAccessTokenForDevOps(ctx context.Context, params AWSOIDCParameters) ( return awsToken, nil } -func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParameters, githubToken string) (_ *OIDCAccessToken, err error) { +func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParameters, githubToken string, client *http.Client) (_ *OIDCAccessToken, err error) { + if err := validateHTTPClient(client); err != nil { + return nil, err + } if params.ServiceSlug == "" { return nil, fmt.Errorf("service slug is required") } @@ -603,6 +616,9 @@ func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParamete if params.OrgName == "" { return nil, fmt.Errorf("org name is required") } + if params.Audience == "" { + return nil, fmt.Errorf("audience is required") + } if githubToken == "" { return nil, fmt.Errorf("GitHub token is required") } @@ -627,9 +643,6 @@ func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParamete req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "dependabot-proxy/1.0") - client := &http.Client{ - Timeout: 10 * time.Second, - } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute cloudsmith token request: %w", err) @@ -661,18 +674,18 @@ func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParamete }, nil } -func GetCloudsmithAccessTokenForDevOps(ctx context.Context, params CloudsmithOIDCParameters) (*OIDCAccessToken, error) { +func GetCloudsmithAccessTokenForDevOps(ctx context.Context, params CloudsmithOIDCParameters, client *http.Client) (*OIDCAccessToken, error) { if !IsOIDCConfigured() { return nil, fmt.Errorf("GitHub Actions OIDC is not configured") } // Get GitHub OIDC token - githubToken, err := GetToken(ctx, params.Audience) + githubToken, err := GetToken(ctx, params.Audience, client) if err != nil { return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err) } - cloudsmithToken, err := GetCloudsmithAccessToken(ctx, params, githubToken) + cloudsmithToken, err := GetCloudsmithAccessToken(ctx, params, githubToken, client) if err != nil { return nil, fmt.Errorf("failed to exchange GitHub token for cloudsmith token: %w", err) } @@ -680,7 +693,10 @@ func GetCloudsmithAccessTokenForDevOps(ctx context.Context, params CloudsmithOID return cloudsmithToken, nil } -func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToken string) (_ *OIDCAccessToken, err error) { +func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToken string, client *http.Client) (_ *OIDCAccessToken, err error) { + if err := validateHTTPClient(client); err != nil { + return nil, err + } if params.WorkloadIdentityProvider == "" { return nil, fmt.Errorf("workload-identity-provider is required") } @@ -715,9 +731,6 @@ func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToke stsReq.Header.Set("Accept", "application/json") stsReq.Header.Set("User-Agent", "dependabot-proxy/1.0") - client := &http.Client{ - Timeout: 10 * time.Second, - } stsResp, err := client.Do(stsReq) if err != nil { return nil, fmt.Errorf("failed to execute GCP STS request: %w", err) @@ -815,18 +828,18 @@ func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToke }, nil } -func GetGCPAccessTokenForDevOps(ctx context.Context, params GCPOIDCParameters) (*OIDCAccessToken, error) { +func GetGCPAccessTokenForDevOps(ctx context.Context, params GCPOIDCParameters, client *http.Client) (*OIDCAccessToken, error) { if !IsOIDCConfigured() { return nil, fmt.Errorf("GitHub Actions OIDC is not configured") } // Get GitHub OIDC token - githubToken, err := GetToken(ctx, params.Audience) + githubToken, err := GetToken(ctx, params.Audience, client) if err != nil { return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err) } - gcpToken, err := GetGCPAccessToken(ctx, params, githubToken) + gcpToken, err := GetGCPAccessToken(ctx, params, githubToken, client) if err != nil { return nil, fmt.Errorf("failed to exchange GitHub token for GCP token: %w", err) } diff --git a/internal/oidc/actions_oidc_test.go b/internal/oidc/actions_oidc_test.go index bb7c4a3..77d7a82 100644 --- a/internal/oidc/actions_oidc_test.go +++ b/internal/oidc/actions_oidc_test.go @@ -6,17 +6,33 @@ import ( "encoding/xml" "fmt" "io" + "net" "net/http" "net/http/httptest" "os" "strings" "testing" + "time" "github.com/jarcoal/httpmock" + "github.com/rs/dnscache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/dependabot/proxy/internal/dialer" ) +var testHTTPClient = &http.Client{ + Transport: currentDefaultTransport{}, + Timeout: 10 * time.Second, +} + +type currentDefaultTransport struct{} + +func (currentDefaultTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return http.DefaultTransport.RoundTrip(req) +} + func unsetEnv(t *testing.T, key string) { t.Helper() @@ -193,7 +209,7 @@ func TestGetToken(t *testing.T) { ctx := context.Background() - token, err := GetToken(ctx, tt.audience) + token, err := GetToken(ctx, tt.audience, testHTTPClient) if tt.expectError { require.Error(t, err) @@ -205,6 +221,38 @@ func TestGetToken(t *testing.T) { } } +func TestGetTokenRejectsBlockedDestination(t *testing.T) { + const metadataIP = "169.254.169.254" + t.Setenv(envActionsIDTokenRequestURL, "http://"+metadataIP+"/token") + t.Setenv(envActionsIDTokenRequestToken, "request-token") + + resolver := &dnscache.Resolver{} + safeDialer := dialer.New(resolver, []net.IP{net.ParseIP(metadataIP)}) + client := &http.Client{ + Transport: &http.Transport{DialContext: safeDialer.DialContext}, + Timeout: testHTTPClient.Timeout, + } + + _, err := GetToken(t.Context(), "audience", client) + require.Error(t, err) + assert.ErrorIs(t, err, dialer.ErrForbiddenRequest) +} + +func TestGetTokenRequiresExplicitTransport(t *testing.T) { + for name, client := range map[string]*http.Client{ + "nil client": nil, + "nil transport": {Timeout: testHTTPClient.Timeout}, + "zero timeout": {Transport: http.DefaultTransport}, + } { + t.Run(name, func(t *testing.T) { + assert.NotPanics(t, func() { + _, err := GetToken(t.Context(), "", client) + assert.Error(t, err) + }) + }) + } +} + func TestGetTokenForAzureADExchange(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify the audience parameter is set correctly @@ -221,7 +269,7 @@ func TestGetTokenForAzureADExchange(t *testing.T) { ctx := context.Background() - token, err := GetTokenForAzureADExchange(ctx) + token, err := GetTokenForAzureADExchange(ctx, testHTTPClient) require.NoError(t, err) assert.Equal(t, "azure-exchange-token", token) @@ -309,7 +357,7 @@ func TestGetToken_URLParsing(t *testing.T) { t.Setenv(envActionsIDTokenRequestToken, "test-token") ctx := context.Background() - _, err := GetToken(ctx, tt.audience) + _, err := GetToken(ctx, tt.audience, testHTTPClient) if tt.expectError { require.Error(t, err) @@ -458,7 +506,7 @@ func TestGetAzureAccessToken(t *testing.T) { TenantID: tt.tenantID, ClientID: tt.clientID, } - azureToken, err = GetAzureAccessToken(ctx, params, tt.githubToken) + azureToken, err = GetAzureAccessToken(ctx, params, tt.githubToken, testHTTPClient) if tt.expectError { require.Error(t, err) @@ -638,7 +686,7 @@ func TestGetJFrogAccessToken(t *testing.T) { Audience: tt.audience, IdentityMappingName: tt.identityMappingName, } - jfrogToken, err = GetJFrogAccessToken(ctx, params, tt.githubToken) + jfrogToken, err = GetJFrogAccessToken(ctx, params, tt.githubToken, testHTTPClient) if tt.expectError { require.Error(t, err) @@ -828,7 +876,7 @@ func TestGetAWSAccessToken(t *testing.T) { Domain: tt.domain, DomainOwner: tt.domainOwner, } - awsToken, err = GetAWSAccessToken(ctx, params, tt.githubToken) + awsToken, err = GetAWSAccessToken(ctx, params, tt.githubToken, testHTTPClient) if tt.expectError { require.Error(t, err) @@ -1010,7 +1058,7 @@ func TestGetCloudsmithAccessToken(t *testing.T) { })) } - cloudsmithToken, err = GetCloudsmithAccessToken(ctx, tt.params, tt.githubToken) + cloudsmithToken, err = GetCloudsmithAccessToken(ctx, tt.params, tt.githubToken, testHTTPClient) if tt.expectError { require.Error(t, err) @@ -1333,7 +1381,7 @@ func TestGetGCPAccessToken(t *testing.T) { })) } - gcpToken, err := GetGCPAccessToken(ctx, tt.params, tt.githubToken) + gcpToken, err := GetGCPAccessToken(ctx, tt.params, tt.githubToken, testHTTPClient) if tt.expectError { require.Error(t, err) diff --git a/internal/oidc/oidc_credential.go b/internal/oidc/oidc_credential.go index d3d591a..b51cb2d 100644 --- a/internal/oidc/oidc_credential.go +++ b/internal/oidc/oidc_credential.go @@ -3,6 +3,7 @@ package oidc import ( "context" "fmt" + "net/http" "net/url" "sync" "time" @@ -74,13 +75,17 @@ type OIDCCredential struct { tokenExpiry time.Time isRejected bool mutex sync.RWMutex + httpClient *http.Client } func (c *OIDCCredential) Provider() string { return c.parameters.Name() } -func CreateOIDCCredential(cred config.Credential) (*OIDCCredential, error) { +func CreateOIDCCredential(cred config.Credential, client *http.Client) (*OIDCCredential, error) { + if err := validateHTTPClient(client); err != nil { + return nil, err + } if !IsOIDCConfigured() { return nil, fmt.Errorf("OIDC is not configured") } @@ -173,6 +178,7 @@ func CreateOIDCCredential(cred config.Credential) (*OIDCCredential, error) { return &OIDCCredential{ parameters: parameters, + httpClient: client, }, nil } @@ -201,15 +207,15 @@ func GetOrRefreshOIDCToken(cred *OIDCCredential, ctx context.Context) (string, e var err error switch params := cred.parameters.(type) { case *AzureOIDCParameters: - oidcAccessToken, err = GetAzureAccessTokenForDevOps(ctx, *params) + oidcAccessToken, err = GetAzureAccessTokenForDevOps(ctx, *params, cred.httpClient) case *JFrogOIDCParameters: - oidcAccessToken, err = GetJFrogAccessTokenForDevOps(ctx, *params) + oidcAccessToken, err = GetJFrogAccessTokenForDevOps(ctx, *params, cred.httpClient) case *AWSOIDCParameters: - oidcAccessToken, err = GetAWSAccessTokenForDevOps(ctx, *params) + oidcAccessToken, err = GetAWSAccessTokenForDevOps(ctx, *params, cred.httpClient) case *CloudsmithOIDCParameters: - oidcAccessToken, err = GetCloudsmithAccessTokenForDevOps(ctx, *params) + oidcAccessToken, err = GetCloudsmithAccessTokenForDevOps(ctx, *params, cred.httpClient) case *GCPOIDCParameters: - oidcAccessToken, err = GetGCPAccessTokenForDevOps(ctx, *params) + oidcAccessToken, err = GetGCPAccessTokenForDevOps(ctx, *params, cred.httpClient) default: return "", fmt.Errorf("unsupported OIDC provider: %s", cred.Provider()) } diff --git a/internal/oidc/oidc_credential_test.go b/internal/oidc/oidc_credential_test.go index efc2413..2fd4a42 100644 --- a/internal/oidc/oidc_credential_test.go +++ b/internal/oidc/oidc_credential_test.go @@ -27,7 +27,7 @@ func TestSuccessfulAuthenticationDoesNotMakeARepeatedRequest(t *testing.T) { creds, err := CreateOIDCCredential(config.Credential{ "tenant-id": "test-tenant-id", "client-id": "test-client-id", - }) + }, testHTTPClient) require.NoError(t, err) // ensure of type azure @@ -88,7 +88,7 @@ func TestFailedAuthenticationIsNotRetried(t *testing.T) { creds, err := CreateOIDCCredential(config.Credential{ "tenant-id": "test-tenant-id", "client-id": "test-client-id", - }) + }, testHTTPClient) require.NoError(t, err) // ensure of type azure @@ -334,7 +334,7 @@ func TestTryCreateOIDCCredential(t *testing.T) { t.Setenv(envActionsIDTokenRequestURL, "https://example.com/token") t.Setenv(envActionsIDTokenRequestToken, "test-token") - actual, _ := CreateOIDCCredential(tc.cred) + actual, _ := CreateOIDCCredential(tc.cred, testHTTPClient) if tc.expectedParameters == nil { assert.Nil(t, actual) return @@ -350,3 +350,50 @@ func TestTryCreateOIDCCredential(t *testing.T) { }) } } + +func TestOIDCCredentialUsesOneClientForAssertionAndExchange(t *testing.T) { + t.Setenv(envActionsIDTokenRequestURL, "https://actions.example.test/token") + t.Setenv(envActionsIDTokenRequestToken, "request-token") + + recorder := &recordingOIDCTransport{} + client := &http.Client{Transport: recorder, Timeout: testHTTPClient.Timeout} + credential, err := CreateOIDCCredential(config.Credential{ + "tenant-id": "test-tenant-id", + "client-id": "test-client-id", + }, client) + require.NoError(t, err) + require.Same(t, client, credential.httpClient) + + token, err := GetOrRefreshOIDCToken(credential, t.Context()) + require.NoError(t, err) + assert.Equal(t, "access-token", token) + assert.Equal(t, []string{ + "GET https://actions.example.test/token?audience=api%3A%2F%2FAzureADTokenExchange", + "POST https://login.microsoftonline.com/test-tenant-id/oauth2/v2.0/token", + }, recorder.requests) +} + +type recordingOIDCTransport struct { + requests []string +} + +func (r *recordingOIDCTransport) RoundTrip(req *http.Request) (*http.Response, error) { + r.requests = append(r.requests, req.Method+" "+req.URL.String()) + + var body string + switch req.URL.Host { + case "actions.example.test": + body = `{"count":1,"value":"assertion"}` + case "login.microsoftonline.com": + body = `{"access_token":"access-token","expires_in":3600,"token_type":"Bearer"}` + default: + return nil, fmt.Errorf("unexpected OIDC request: %s", req.URL) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(body)), + Header: make(http.Header), + Request: req, + }, nil +} diff --git a/internal/oidc/oidc_registry.go b/internal/oidc/oidc_registry.go index ed7dd11..ef7f7ff 100644 --- a/internal/oidc/oidc_registry.go +++ b/internal/oidc/oidc_registry.go @@ -19,6 +19,7 @@ import ( type OIDCRegistry struct { byHost map[string][]oidcEntry mutex sync.RWMutex + client *http.Client } type oidcEntry struct { @@ -27,10 +28,15 @@ type oidcEntry struct { credential *OIDCCredential } -// NewOIDCRegistry creates an empty registry. -func NewOIDCRegistry() *OIDCRegistry { +// NewOIDCRegistry creates an empty registry. client is used for every outbound +// OIDC request and must use the same restricted transport as the proxy. +func NewOIDCRegistry(client *http.Client) *OIDCRegistry { + if err := validateHTTPClient(client); err != nil { + panic(err) + } return &OIDCRegistry{ byHost: make(map[string][]oidcEntry), + client: client, } } @@ -47,7 +53,7 @@ func (r *OIDCRegistry) Register( urlFields []string, registryType string, ) (*OIDCCredential, string, bool) { - oidcCredential, _ := CreateOIDCCredential(cred) + oidcCredential, _ := CreateOIDCCredential(cred, r.client) if oidcCredential == nil { return nil, "", false } diff --git a/internal/oidc/oidc_registry_test.go b/internal/oidc/oidc_registry_test.go index defbd5c..2b0349b 100644 --- a/internal/oidc/oidc_registry_test.go +++ b/internal/oidc/oidc_registry_test.go @@ -57,7 +57,7 @@ func azureCredWithRegistry(tenantID, clientID, registry string) config.Credentia func TestOIDCRegistry_Register_SingleCredential(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://registry.example.com/packages") oidcCred, key, ok := r.Register(cred, []string{"url"}, "test registry") @@ -69,7 +69,7 @@ func TestOIDCRegistry_Register_SingleCredential(t *testing.T) { func TestOIDCRegistry_Register_URLFieldPriority(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := config.Credential{ "type": "test_registry", @@ -87,7 +87,7 @@ func TestOIDCRegistry_Register_URLFieldPriority(t *testing.T) { func TestOIDCRegistry_Register_FallsBackToHost(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := config.Credential{ "type": "test_registry", @@ -107,7 +107,7 @@ func TestOIDCRegistry_Register_NotOIDC(t *testing.T) { t.Setenv(envActionsIDTokenRequestURL, "") t.Setenv(envActionsIDTokenRequestToken, "") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := config.Credential{ "type": "test_registry", "url": "https://registry.example.com", @@ -122,7 +122,7 @@ func TestOIDCRegistry_Register_NotOIDC(t *testing.T) { func TestOIDCRegistry_Register_NoKeyAvailable(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) // Credential with OIDC params but no URL or host cred := config.Credential{ @@ -144,7 +144,7 @@ func TestOIDCRegistry_TryAuth_SingleCredential(t *testing.T) { defer httpmock.DeactivateAndReset() mockAzureOIDC(t, "tenant-1", "__test_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://registry.example.com/packages") r.Register(cred, []string{"url"}, "test registry") @@ -163,7 +163,7 @@ func TestOIDCRegistry_TryAuth_SameHostDifferentPaths_NoCollision(t *testing.T) { mockAzureOIDC(t, "tenant-A", "token-feed-A") mockAzureOIDC(t, "tenant-B", "token-feed-B") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) // Two registries on the same host with different paths credA := azureCredWithURL("tenant-A", "client-A", @@ -201,7 +201,7 @@ func TestOIDCRegistry_TryAuth_HostOnlyMatchesAnyPath(t *testing.T) { defer httpmock.DeactivateAndReset() mockAzureOIDC(t, "tenant-1", "__test_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) // Register with host only (no path) cred := config.Credential{ @@ -222,7 +222,7 @@ func TestOIDCRegistry_TryAuth_HostOnlyMatchesAnyPath(t *testing.T) { func TestOIDCRegistry_TryAuth_NoMatch(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://registry.example.com/packages") r.Register(cred, []string{"url"}, "test registry") @@ -237,7 +237,7 @@ func TestOIDCRegistry_TryAuth_NoMatch(t *testing.T) { func TestOIDCRegistry_TryAuth_WrongPathNoMatch(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://pkgs.dev.azure.com/org/_packaging/feed-A/npm/registry/") @@ -253,7 +253,7 @@ func TestOIDCRegistry_TryAuth_WrongPathNoMatch(t *testing.T) { } func TestOIDCRegistry_CredentialForRequestConcurrentRegistration(t *testing.T) { - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) credential := &OIDCCredential{ parameters: &AzureOIDCParameters{ TenantID: "tenant-1", @@ -293,7 +293,7 @@ func TestOIDCRegistry_RegisterURL(t *testing.T) { defer httpmock.DeactivateAndReset() mockAzureOIDC(t, "tenant-1", "__test_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) // Register primary URL cred := azureCredWithURL("tenant-1", "client-1", "https://nuget.example.com/v3/index.json") @@ -314,7 +314,7 @@ func TestOIDCRegistry_RegisterURL(t *testing.T) { func TestOIDCRegistry_TryAuth_PortMismatch(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://registry.example.com:8443/packages") r.Register(cred, []string{"url"}, "test registry") @@ -328,7 +328,7 @@ func TestOIDCRegistry_TryAuth_PortMismatch(t *testing.T) { func TestOIDCRegistry_Register_RegistryField(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithRegistry("tenant-1", "client-1", "ghcr.io") _, key, ok := r.Register(cred, []string{"registry"}, "docker registry") @@ -344,7 +344,7 @@ func TestOIDCRegistry_TryAuth_PathSpecificBeatsHostOnly(t *testing.T) { mockAzureOIDC(t, "tenant-1", "__host_only_token__") mockAzureOIDC(t, "tenant-2", "__path_specific_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) hostOnlyCred := config.Credential{ "type": "test_registry", @@ -372,7 +372,7 @@ func TestOIDCRegistry_TryAuth_LongestPathPrefixWins(t *testing.T) { mockAzureOIDC(t, "tenant-1", "__short_prefix_token__") mockAzureOIDC(t, "tenant-2", "__long_prefix_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) shortPrefixCred := azureCredWithURL("tenant-1", "client-1", "https://registry.example.com/packages") longPrefixCred := azureCredWithURL("tenant-2", "client-2", "https://registry.example.com/packages/private") @@ -394,7 +394,7 @@ func TestOIDCRegistry_TryAuth_CaseInsensitiveHost(t *testing.T) { defer httpmock.DeactivateAndReset() mockAzureOIDC(t, "tenant-1", "__test_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://Registry.Example.COM/packages") r.Register(cred, []string{"url"}, "test registry") @@ -432,7 +432,7 @@ func TestOIDCRegistry_TryAuth_Cloudsmith_UsesAPIKey(t *testing.T) { defer httpmock.DeactivateAndReset() mockCloudsmithOIDC(t, "my-org", "__cs_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := cloudsmithCred("my-org", "my-service", "https://cloudsmith.io", "https://dl.cloudsmith.io/basic/my-org/my-repo") r.Register(cred, []string{"url"}, "test registry") @@ -467,7 +467,7 @@ func TestOIDCRegistry_TryAuth_GCP_UsesBearer(t *testing.T) { defer httpmock.DeactivateAndReset() mockGCPOIDC(t, "__gcp_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := gcpCred("projects/123/locations/global/workloadIdentityPools/pool/providers/prov", "https://us-central1-python.pkg.dev/my-project/my-repo/simple") r.Register(cred, []string{"url"}, "test registry") @@ -486,7 +486,7 @@ func TestOIDCRegistry_TryAuth_GCP_DockerUsesBasicAuth(t *testing.T) { defer httpmock.DeactivateAndReset() mockGCPOIDC(t, "__gcp_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := gcpCred("projects/123/locations/global/workloadIdentityPools/pool/providers/prov", "https://us-central1-docker.pkg.dev/my-project/my-repo") r.Register(cred, []string{"url"}, "docker registry") @@ -505,7 +505,7 @@ func TestOIDCRegistry_TryAuth_GCP_DockerUsesBasicAuth(t *testing.T) { func TestOIDCRegistry_Register_IndexURLField(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCred("tenant-1", "client-1") cred["index-url"] = "https://pkgs.dev.azure.com/org/_packaging/feed/pypi/simple" @@ -522,7 +522,7 @@ func TestOIDCRegistry_TryAuth_URLWithoutProtocol(t *testing.T) { defer httpmock.DeactivateAndReset() mockAzureOIDC(t, "tenant-1", "__test_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCred("tenant-1", "client-1") cred["url"] = "registry.example.com/packages" @@ -541,7 +541,7 @@ func TestOIDCRegistry_RegisterURL_MultipleOnSameHost(t *testing.T) { defer httpmock.DeactivateAndReset() mockAzureOIDC(t, "tenant-1", "__test_token__") - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred := azureCredWithURL("tenant-1", "client-1", "https://nuget.example.com/v3/index.json") oidcCred, _, ok := r.Register(cred, []string{"url"}, "nuget feed") @@ -563,7 +563,7 @@ func TestOIDCRegistry_RegisterURL_MultipleOnSameHost(t *testing.T) { func TestOIDCRegistry_Register_NoDuplicateEntries(t *testing.T) { setupOIDCEnv(t) - r := NewOIDCRegistry() + r := NewOIDCRegistry(testHTTPClient) cred1 := azureCredWithURL("tenant-1", "client-1", "https://registry.example.com/packages") cred2 := azureCredWithURL("tenant-2", "client-2", "https://registry.example.com/packages") diff --git a/proxy.go b/proxy.go index 40bb2ea..3960609 100644 --- a/proxy.go +++ b/proxy.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/elazarl/goproxy" "github.com/rs/dnscache" @@ -51,6 +52,10 @@ func newProxyWithCacheDir(envSettings config.ProxyEnvSettings, cfg *config.Confi }, Proxy: http.ProxyFromEnvironment, } + oidcClient := &http.Client{ + Timeout: 10 * time.Second, + Transport: transport, + } apiClient := apiclient.New(envSettings.APIEndpoint, envSettings.JobToken, envSettings.JobID, apiclient.WithTransport(transport)) metricsClient := metrics.New(envSettings, apiClient) @@ -68,7 +73,7 @@ func newProxyWithCacheDir(envSettings config.ProxyEnvSettings, cfg *config.Confi proxy.OnRequest().DoFunc(logger.logRequest) proxy.OnResponse().DoFunc(logger.logResponse) - nugetFeedHandler := handlers.NewNugetFeedHandler(cfg.Credentials) + nugetFeedHandler := handlers.NewNugetFeedHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(nugetFeedHandler.PrepareRequest) enableCache := os.Getenv("PROXY_CACHE") == "true" @@ -98,50 +103,50 @@ func newProxyWithCacheDir(envSettings config.ProxyEnvSettings, cfg *config.Confi proxy.OnRequest().DoFunc(gitServerHandler.HandleRequest) proxy.OnResponse().DoFunc(gitServerHandler.HandleResponse) - npmRegistryHandler := handlers.NewNPMRegistryHandler(cfg.Credentials) + npmRegistryHandler := handlers.NewNPMRegistryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(npmRegistryHandler.HandleRequest) hexOrganizationHandler := handlers.NewHexOrganizationHandler(cfg.Credentials) proxy.OnRequest().DoFunc(hexOrganizationHandler.HandleRequest) - hexRepositoryHandler := handlers.NewHexRepositoryHandler(cfg.Credentials) + hexRepositoryHandler := handlers.NewHexRepositoryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(hexRepositoryHandler.HandleRequest) - pythonHandler := handlers.NewPythonIndexHandler(cfg.Credentials) + pythonHandler := handlers.NewPythonIndexHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(pythonHandler.HandleRequest) proxy.OnResponse().DoFunc(pythonHandler.HandleResponse) - composerHandler := handlers.NewComposerHandler(cfg.Credentials) + composerHandler := handlers.NewComposerHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(composerHandler.HandleRequest) - dockerRegistryHandler := handlers.NewDockerRegistryHandler(cfg.Credentials, transport, nil) + dockerRegistryHandler := handlers.NewDockerRegistryHandler(cfg.Credentials, oidcClient, nil) proxy.OnRequest().DoFunc(dockerRegistryHandler.HandleRequest) - rubyGemsServerHandler := handlers.NewRubyGemsServerHandler(cfg.Credentials) + rubyGemsServerHandler := handlers.NewRubyGemsServerHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(rubyGemsServerHandler.HandleRequest) proxy.OnRequest().DoFunc(nugetFeedHandler.HandleRequest) proxy.OnResponse().DoFunc(nugetFeedHandler.HandleResponse) - mavenRepositoryHandler := handlers.NewMavenRepositoryHandler(cfg.Credentials) + mavenRepositoryHandler := handlers.NewMavenRepositoryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(mavenRepositoryHandler.HandleRequest) - terraformRegistryHandler := handlers.NewTerraformRegistryHandler(cfg.Credentials) + terraformRegistryHandler := handlers.NewTerraformRegistryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(terraformRegistryHandler.HandleRequest) - openTofuRegistryHandler := handlers.NewOpenTofuRegistryHandler(cfg.Credentials) + openTofuRegistryHandler := handlers.NewOpenTofuRegistryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(openTofuRegistryHandler.HandleRequest) - pubRepositoryHandler := handlers.NewPubRepositoryHandler(cfg.Credentials) + pubRepositoryHandler := handlers.NewPubRepositoryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(pubRepositoryHandler.HandleRequest) - cargoRegistryHandler := handlers.NewCargoRegistryHandler(cfg.Credentials) + cargoRegistryHandler := handlers.NewCargoRegistryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(cargoRegistryHandler.HandleRequest) - goProxyServerHandler := handlers.NewGoProxyServerHandler(cfg.Credentials) + goProxyServerHandler := handlers.NewGoProxyServerHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(goProxyServerHandler.HandleRequest) - helmRegistryHandler := handlers.NewHelmRegistryHandler(cfg.Credentials) + helmRegistryHandler := handlers.NewHelmRegistryHandler(cfg.Credentials, oidcClient) proxy.OnRequest().DoFunc(helmRegistryHandler.HandleRequest) proxy.OnResponse().DoFunc(cacher.OnResponse)