diff --git a/session/group.go b/session/group.go index ab4825e22..1294cce32 100644 --- a/session/group.go +++ b/session/group.go @@ -7,6 +7,10 @@ import ( "github.com/pkg/errors" ) +// ErrNoActiveSessions is returned when a session group does not contain any +// active session IDs. +var ErrNoActiveSessions = errors.New("no active sessions") + type Group interface { SessionIterator() Iterator } @@ -69,7 +73,7 @@ func (sm *Manager) Any(ctx context.Context, g Group, f func(context.Context, str if lastErr != nil { return lastErr } - return errors.Errorf("no active sessions") + return errors.WithStack(ErrNoActiveSessions) } timeoutCtx, cancel := context.WithCancelCause(ctx) diff --git a/util/resolver/authorizer.go b/util/resolver/authorizer.go index da772460b..ced4b78fb 100644 --- a/util/resolver/authorizer.go +++ b/util/resolver/authorizer.go @@ -48,6 +48,7 @@ func newAuthHandlerNS(sm *session.Manager) *authHandlerNS { } func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manager, g session.Group) *authFetcher { + hasSession := false if g != nil { if iter := g.SessionIterator(); iter != nil { for { @@ -55,6 +56,7 @@ func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manage if id == "" { break } + hasSession = true h, ok := a.fetchers[host+"/"+id] if ok { h.lastUsed = time.Now() @@ -64,6 +66,14 @@ func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manage } } + if !hasSession { + h, ok := a.fetchers[host+"/"] + if ok { + h.lastUsed = time.Now() + return h + } + } + // link existing fetcher for k, h := range a.fetchers { parts := strings.SplitN(k, "/", 2) @@ -186,12 +196,12 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R var username, secret string sessionID, pubKey, err := sessionauth.GetTokenAuthority(ctx, host, a.sm, a.session) - if err != nil { + if err != nil && !errors.Is(err, session.ErrNoActiveSessions) { return err } if pubKey == nil { sessionID, username, secret, err = a.getCredentials(ctx, host) - if err != nil { + if err != nil && !errors.Is(err, session.ErrNoActiveSessions) { return err } } diff --git a/util/resolver/authorizer_test.go b/util/resolver/authorizer_test.go index d435209c7..3d4098613 100644 --- a/util/resolver/authorizer_test.go +++ b/util/resolver/authorizer_test.go @@ -1,8 +1,16 @@ package resolver import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "reflect" "testing" + "time" + + "github.com/moby/buildkit/session" + "github.com/stretchr/testify/require" ) func TestParseScopes(t *testing.T) { @@ -54,3 +62,59 @@ func TestParseScopes(t *testing.T) { }) } } + +func TestBearerAuthFallsBackToAnonymousTokenWithoutSession(t *testing.T) { + type tokenRequest struct { + authorization string + service string + scope string + } + tokenRequests := make(chan tokenRequest, 1) + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tokenRequests <- tokenRequest{ + authorization: r.Header.Get("Authorization"), + service: r.URL.Query().Get("service"), + scope: r.URL.Query().Get("scope"), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]any{ + "token": "anonymous-token", + "expires_in": 60, + }); err != nil { + t.Errorf("failed to write token response: %v", err) + } + })) + defer tokenServer.Close() + + sm, err := session.NewManager() + require.NoError(t, err) + + auth := newDockerAuthorizer(tokenServer.Client(), newAuthHandlerNS(sm), sm, session.NewGroup("")) + + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "https://registry.example/v2/library/alpine/manifests/latest", nil) + res := &http.Response{ + StatusCode: http.StatusUnauthorized, + Header: http.Header{}, + Request: req, + } + res.Header.Set("WWW-Authenticate", fmt.Sprintf( + `Bearer realm=%q,service="registry.example",scope="repository:library/alpine:pull"`, + tokenServer.URL+"/token", + )) + + require.NoError(t, auth.AddResponses(t.Context(), []*http.Response{res})) + + retryReq := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "https://registry.example/v2/library/alpine/manifests/latest", nil) + require.NoError(t, auth.Authorize(t.Context(), retryReq)) + require.Equal(t, "Bearer anonymous-token", retryReq.Header.Get("Authorization")) + + select { + case req := <-tokenRequests: + require.Empty(t, req.authorization) + require.Equal(t, "registry.example", req.service) + require.Equal(t, "repository:library/alpine:pull", req.scope) + case <-time.After(time.Second): + t.Fatal("expected anonymous token request") + } +}