fix(oci): route authorizer token fetches through provided transport
Some checks failed
ci / validate (lint) (push) Has been cancelled
ci / validate (validate-docs) (push) Has been cancelled
ci / validate (validate-go-mod) (push) Has been cancelled
ci / validate (validate-headers) (push) Has been cancelled
ci / binary (push) Has been cancelled
ci / bin-image-test (push) Has been cancelled
ci / test (push) Has been cancelled
ci / e2e (plugin, oldstable) (push) Has been cancelled
ci / e2e (standalone, oldstable) (push) Has been cancelled
ci / e2e (plugin, stable) (push) Has been cancelled
ci / e2e (standalone, stable) (push) Has been cancelled
merge / bin-image-prepare (push) Has been cancelled
merge / module-image (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
ci / binary-finalize (push) Has been cancelled
ci / coverage (push) Has been cancelled
ci / release (push) Has been cancelled
merge / bin-image (push) Has been cancelled
merge / desktop-edge-test (push) Has been cancelled

The transport passed to NewResolver was applied only via
docker.WithClient. Pass it via docker.WithAuthClient too so the
authorizer's OAuth token fetches use it instead of http.DefaultClient.

Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2026-06-24 15:37:46 +02:00 committed by Guillaume Lours
parent a3c1c0dc2e
commit e87f7b79df
2 changed files with 61 additions and 18 deletions

View file

@ -38,23 +38,28 @@ import (
// NewResolver sets up an OCI Resolver based on docker/cli config to provide
// registry credentials. When transport is non-nil it is used as the HTTP
// transport for all registry calls (e.g. to route through Docker Desktop's
// PAC-aware proxy); nil falls back to containerd's default transport.
// transport for both registry calls and the authorizer's token fetches
// (e.g. to route both through Docker Desktop's PAC-aware proxy); nil falls
// back to containerd's default transport.
func NewResolver(config *configfile.ConfigFile, transport http.RoundTripper, insecureRegistries ...string) remotes.Resolver {
authOpts := []docker.AuthorizerOpt{
docker.WithAuthCreds(func(host string) (string, string, error) {
host = registry.GetAuthConfigKey(host)
auth, err := config.GetAuthConfig(host)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}),
}
if transport != nil {
authOpts = append(authOpts, docker.WithAuthClient(&http.Client{Transport: transport}))
}
opts := []docker.RegistryOpt{
docker.WithAuthorizer(docker.NewDockerAuthorizer(
docker.WithAuthCreds(func(host string) (string, string, error) {
host = registry.GetAuthConfigKey(host)
auth, err := config.GetAuthConfig(host)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}),
)),
docker.WithAuthorizer(docker.NewDockerAuthorizer(authOpts...)),
docker.WithPlainHTTP(func(domain string) (bool, error) {
// Should be used for testing **only**
return slices.Contains(insecureRegistries, domain), nil

View file

@ -19,6 +19,7 @@ package oci
import (
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
@ -27,14 +28,20 @@ import (
)
// recordingRoundTripper counts RoundTrip invocations on a delegate so tests
// can verify a supplied transport is actually used by the resolver.
// can verify a supplied transport is actually used by the resolver. It also
// tracks token-endpoint calls separately so tests can assert the authorizer's
// token fetch goes through the same transport.
type recordingRoundTripper struct {
delegate http.RoundTripper
calls atomic.Int32
delegate http.RoundTripper
calls atomic.Int32
authCalls atomic.Int32
}
func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.calls.Add(1)
if strings.HasSuffix(req.URL.Path, "/token") {
r.authCalls.Add(1)
}
return r.delegate.RoundTrip(req)
}
@ -64,6 +71,37 @@ func TestNewResolver_UsesProvidedTransport(t *testing.T) {
"resolver did not invoke the supplied transport — wiring is broken")
}
// TestNewResolver_AuthorizerUsesProvidedTransport guards that the docker
// authorizer's token fetch goes through the supplied transport, not
// http.DefaultClient.
func TestNewResolver_AuthorizerUsesProvidedTransport(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/token") {
_, _ = w.Write([]byte(`{"token":"fake","access_token":"fake","expires_in":300}`))
return
}
if r.Header.Get("Authorization") == "" {
w.Header().Set("Www-Authenticate", `Bearer realm="`+server.URL+`/token",service="test"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
// Authenticated retry — fail fast, we only care the auth dance went
// through the supplied transport.
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(server.Close)
host := server.Listener.Addr().String()
rec := &recordingRoundTripper{delegate: &http.Transport{}}
resolver := NewResolver(&configfile.ConfigFile{}, rec, host)
_, _, _ = resolver.Resolve(t.Context(), host+"/test/image:latest")
assert.Assert(t, rec.authCalls.Load() > 0,
"authorizer token fetch did not go through the supplied transport (bypassed via http.DefaultClient)")
}
func TestNewResolver_NilTransportIsValid(t *testing.T) {
resolver := NewResolver(&configfile.ConfigFile{}, nil)
assert.Assert(t, resolver != nil, "NewResolver must return a non-nil resolver when transport is nil")