remotes: add handling for missing basic auth credentials

When a credential handler is provided but no basic auth credentials
are provided, handle the error specifically rather than treating
the credentials as not implemented. This allows a clearer error to
be provided to users rather than a confusing not implemented error
or generic unauthorized error.

Add unit tests for the basic auth case.

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan 2023-10-13 11:34:00 -07:00
parent 15bf23df09
commit 51c9ffe468
No known key found for this signature in database
GPG Key ID: F58C5D0A4405ACDB
2 changed files with 103 additions and 9 deletions

View File

@ -186,15 +186,15 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
return err return err
} }
if username != "" && secret != "" { if username == "" || secret == "" {
common := auth.TokenOptions{ return fmt.Errorf("%w: no basic auth credentials", ErrInvalidAuthorization)
Username: username,
Secret: secret,
}
a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, common)
return nil
} }
a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, auth.TokenOptions{
Username: username,
Secret: secret,
})
return nil
} }
} }
return fmt.Errorf("failed to find supported auth scheme: %w", errdefs.ErrNotImplemented) return fmt.Errorf("failed to find supported auth scheme: %w", errdefs.ErrNotImplemented)

View File

@ -35,6 +35,7 @@ import (
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker/auth" "github.com/containerd/containerd/remotes/docker/auth"
remoteerrors "github.com/containerd/containerd/remotes/errors"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go" specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -215,6 +216,14 @@ func TestPostBasicAuthTokenResolver(t *testing.T) {
runBasicTest(t, "testname", withTokenServer(th, creds)) runBasicTest(t, "testname", withTokenServer(th, creds))
} }
func TestBasicAuthResolver(t *testing.T) {
creds := func(string) (string, string, error) {
return "totallyvaliduser", "totallyvalidpassword", nil
}
runBasicTest(t, "testname", withBasicAuthServer(creds))
}
func TestBadTokenResolver(t *testing.T) { func TestBadTokenResolver(t *testing.T) {
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@ -236,7 +245,7 @@ func TestBadTokenResolver(t *testing.T) {
defer close() defer close()
resolver := NewResolver(ro) resolver := NewResolver(ro)
image := fmt.Sprintf("%s/doesntmatter:sometatg", base) image := fmt.Sprintf("%s/doesntmatter:sometag", base)
_, _, err := resolver.Resolve(ctx, image) _, _, err := resolver.Resolve(ctx, image)
if err == nil { if err == nil {
@ -247,6 +256,59 @@ func TestBadTokenResolver(t *testing.T) {
} }
} }
func TestMissingBasicAuthResolver(t *testing.T) {
creds := func(string) (string, string, error) {
return "", "", nil
}
ctx := context.Background()
h := newContent(ocispec.MediaTypeImageManifest, []byte("not anything parse-able"))
base, ro, close := withBasicAuthServer(creds)(logHandler{t, h})
defer close()
resolver := NewResolver(ro)
image := fmt.Sprintf("%s/doesntmatter:sometag", base)
_, _, err := resolver.Resolve(ctx, image)
if err == nil {
t.Fatal("Expected error getting token with inssufficient scope")
}
if !errors.Is(err, ErrInvalidAuthorization) {
t.Fatal(err)
}
if !strings.Contains(err.Error(), "no basic auth credentials") {
t.Fatalf("expected \"no basic auth credentials\" message, got %s", err.Error())
}
}
func TestWrongBasicAuthResolver(t *testing.T) {
creds := func(string) (string, string, error) {
return "totallyvaliduser", "definitelythewrongpassword", nil
}
ctx := context.Background()
h := newContent(ocispec.MediaTypeImageManifest, []byte("not anything parse-able"))
base, ro, close := withBasicAuthServer(creds)(logHandler{t, h})
defer close()
resolver := NewResolver(ro)
image := fmt.Sprintf("%s/doesntmatter:sometag", base)
_, _, err := resolver.Resolve(ctx, image)
if err == nil {
t.Fatal("Expected error getting token with inssufficient scope")
}
var rerr remoteerrors.ErrUnexpectedStatus
if !errors.As(err, &rerr) {
t.Fatal(err)
}
if rerr.StatusCode != 403 {
t.Fatalf("expected 403 status code, got %d", rerr.StatusCode)
}
}
func TestHostFailureFallbackResolver(t *testing.T) { func TestHostFailureFallbackResolver(t *testing.T) {
sf := func(h http.Handler) (string, ResolverOptions, func()) { sf := func(h http.Handler) (string, ResolverOptions, func()) {
s := httptest.NewServer(h) s := httptest.NewServer(h)
@ -549,6 +611,37 @@ func withTokenServer(th http.Handler, creds func(string) (string, string, error)
} }
} }
func withBasicAuthServer(creds func(string) (string, string, error)) func(h http.Handler) (string, ResolverOptions, func()) {
return func(h http.Handler) (string, ResolverOptions, func()) {
// Wrap with basic auth
wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user, password, ok := r.BasicAuth()
if ok {
if user != "totallyvaliduser" || password != "totallyvalidpassword" {
rw.WriteHeader(http.StatusForbidden)
rw.Write([]byte(`{"errors":[{"code":"DENIED"}]}`))
return
}
} else {
authHeader := "Basic realm=\"testserver\""
rw.Header().Set("WWW-Authenticate", authHeader)
rw.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(rw, r)
})
base, options, close := tlsServer(wrapped)
options.Hosts = ConfigureDefaultRegistries(
WithClient(options.Client),
WithAuthorizer(NewDockerAuthorizer(
WithAuthCreds(creds),
)),
)
return base, options, close
}
}
func tlsServer(h http.Handler) (string, ResolverOptions, func()) { func tlsServer(h http.Handler) (string, ResolverOptions, func()) {
s := httptest.NewUnstartedServer(h) s := httptest.NewUnstartedServer(h)
s.StartTLS() s.StartTLS()
@ -564,6 +657,7 @@ func tlsServer(h http.Handler) (string, ResolverOptions, func()) {
}, },
}, },
} }
options := ResolverOptions{ options := ResolverOptions{
Hosts: ConfigureDefaultRegistries(WithClient(client)), Hosts: ConfigureDefaultRegistries(WithClient(client)),
// Set deprecated field for tests to use for configuration // Set deprecated field for tests to use for configuration