Explorar el Código

api: add registry.DecodeAuthConfig, registry.DecodeAuthConfigBody

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn hace 3 años
padre
commit
7819811835

+ 5 - 18
api/server/router/distribution/distribution_routes.go

@@ -2,10 +2,8 @@ package distribution // import "github.com/docker/docker/api/server/router/distr
 
 import (
 	"context"
-	"encoding/base64"
 	"encoding/json"
 	"net/http"
-	"strings"
 
 	"github.com/docker/distribution/manifest/manifestlist"
 	"github.com/docker/distribution/manifest/schema1"
@@ -25,21 +23,6 @@ func (s *distributionRouter) getDistributionInfo(ctx context.Context, w http.Res
 
 	w.Header().Set("Content-Type", "application/json")
 
-	var (
-		config              = &registry.AuthConfig{}
-		authEncoded         = r.Header.Get(registry.AuthHeader)
-		distributionInspect registry.DistributionInspect
-	)
-
-	if authEncoded != "" {
-		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
-		if err := json.NewDecoder(authJSON).Decode(&config); err != nil {
-			// for a search it is not an error if no auth was given
-			// to increase compatibility with the existing api it is defaulting to be empty
-			config = &registry.AuthConfig{}
-		}
-	}
-
 	image := vars["name"]
 
 	// TODO why is reference.ParseAnyReference() / reference.ParseNormalizedNamed() not using the reference.ErrTagInvalidFormat (and so on) errors?
@@ -56,12 +39,16 @@ func (s *distributionRouter) getDistributionInfo(ctx context.Context, w http.Res
 		return errdefs.InvalidParameter(errors.Errorf("unknown image reference format: %s", image))
 	}
 
-	distrepo, err := s.backend.GetRepository(ctx, namedRef, config)
+	// For a search it is not an error if no auth was given. Ignore invalid
+	// AuthConfig to increase compatibility with the existing API.
+	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
+	distrepo, err := s.backend.GetRepository(ctx, namedRef, authConfig)
 	if err != nil {
 		return err
 	}
 	blobsrvc := distrepo.Blobs(ctx)
 
+	var distributionInspect registry.DistributionInspect
 	if canonicalRef, ok := namedRef.(reference.Canonical); !ok {
 		namedRef = reference.TagNameOnly(namedRef)
 

+ 20 - 41
api/server/router/image/image_routes.go

@@ -2,8 +2,6 @@ package image // import "github.com/docker/docker/api/server/router/image"
 
 import (
 	"context"
-	"encoding/base64"
-	"encoding/json"
 	"net/http"
 	"strconv"
 	"strings"
@@ -64,16 +62,9 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
 			}
 		}
 
-		authEncoded := r.Header.Get(registry.AuthHeader)
-		authConfig := &registry.AuthConfig{}
-		if authEncoded != "" {
-			authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
-			if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
-				// for a pull it is not an error if no auth was given
-				// to increase compatibility with the existing api it is defaulting to be empty
-				authConfig = &registry.AuthConfig{}
-			}
-		}
+		// For a pull it is not an error if no auth was given. Ignore invalid
+		// AuthConfig to increase compatibility with the existing API.
+		authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
 		progressErr = s.backend.PullImage(ctx, image, tag, platform, metaHeaders, authConfig, output)
 	} else { // import
 		src := r.Form.Get("fromSrc")
@@ -99,32 +90,29 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
 	if err := httputils.ParseForm(r); err != nil {
 		return err
 	}
-	authConfig := &registry.AuthConfig{}
 
-	authEncoded := r.Header.Get(registry.AuthHeader)
-	if authEncoded != "" {
-		// the new format is to handle the authConfig as a header
-		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
-		if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
-			// to increase compatibility to existing api it is defaulting to be empty
-			authConfig = &registry.AuthConfig{}
-		}
+	var authConfig *registry.AuthConfig
+	if authEncoded := r.Header.Get(registry.AuthHeader); authEncoded != "" {
+		// the new format is to handle the authConfig as a header. Ignore invalid
+		// AuthConfig to increase compatibility with the existing API.
+		authConfig, _ = registry.DecodeAuthConfig(authEncoded)
 	} else {
 		// the old format is supported for compatibility if there was no authConfig header
-		if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
-			return errors.Wrap(errdefs.InvalidParameter(err), "Bad parameters and missing X-Registry-Auth")
+		var err error
+		authConfig, err = registry.DecodeAuthConfigBody(r.Body)
+		if err != nil {
+			return errors.Wrap(err, "bad parameters and missing X-Registry-Auth")
 		}
 	}
 
-	image := vars["name"]
-	tag := r.Form.Get("tag")
-
 	output := ioutils.NewWriteFlusher(w)
 	defer output.Close()
 
 	w.Header().Set("Content-Type", "application/json")
 
-	if err := s.backend.PushImage(ctx, image, tag, metaHeaders, authConfig, output); err != nil {
+	img := vars["name"]
+	tag := r.Form.Get("tag")
+	if err := s.backend.PushImage(ctx, img, tag, metaHeaders, authConfig, output); err != nil {
 		if !output.Flushed() {
 			return err
 		}
@@ -359,20 +347,8 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
 	if err := httputils.ParseForm(r); err != nil {
 		return err
 	}
-	var (
-		config      *registry.AuthConfig
-		authEncoded = r.Header.Get(registry.AuthHeader)
-		headers     = map[string][]string{}
-	)
 
-	if authEncoded != "" {
-		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
-		if err := json.NewDecoder(authJSON).Decode(&config); err != nil {
-			// for a search it is not an error if no auth was given
-			// to increase compatibility with the existing api it is defaulting to be empty
-			config = &registry.AuthConfig{}
-		}
-	}
+	var headers = map[string][]string{}
 	for k, v := range r.Header {
 		if strings.HasPrefix(k, "X-Meta-") {
 			headers[k] = v
@@ -392,7 +368,10 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
 		return err
 	}
 
-	query, err := s.backend.SearchRegistryForImages(ctx, searchFilters, r.Form.Get("term"), limit, config, headers)
+	// For a search it is not an error if no auth was given. Ignore invalid
+	// AuthConfig to increase compatibility with the existing API.
+	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
+	query, err := s.backend.SearchRegistryForImages(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
 	if err != nil {
 		return err
 	}

+ 2 - 13
api/server/router/plugin/plugin_routes.go

@@ -2,8 +2,6 @@ package plugin // import "github.com/docker/docker/api/server/router/plugin"
 
 import (
 	"context"
-	"encoding/base64"
-	"encoding/json"
 	"net/http"
 	"strconv"
 	"strings"
@@ -19,7 +17,6 @@ import (
 )
 
 func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfig) {
-
 	metaHeaders := map[string][]string{}
 	for k, v := range headers {
 		if strings.HasPrefix(k, "X-Meta-") {
@@ -27,16 +24,8 @@ func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfi
 		}
 	}
 
-	// Get X-Registry-Auth
-	authEncoded := headers.Get(registry.AuthHeader)
-	authConfig := &registry.AuthConfig{}
-	if authEncoded != "" {
-		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
-		if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
-			authConfig = &registry.AuthConfig{}
-		}
-	}
-
+	// Ignore invalid AuthConfig to increase compatibility with the existing API.
+	authConfig, _ := registry.DecodeAuthConfig(headers.Get(registry.AuthHeader))
 	return metaHeaders, authConfig
 }
 

+ 60 - 0
api/types/registry/authconfig.go

@@ -1,4 +1,12 @@
 package registry // import "github.com/docker/docker/api/types/registry"
+import (
+	"encoding/base64"
+	"encoding/json"
+	"io"
+	"strings"
+
+	"github.com/pkg/errors"
+)
 
 // AuthHeader is the name of the header used to send encoded registry
 // authorization credentials for registry operations (push/pull).
@@ -24,3 +32,55 @@ type AuthConfig struct {
 	// RegistryToken is a bearer token to be sent to a registry
 	RegistryToken string `json:"registrytoken,omitempty"`
 }
+
+// DecodeAuthConfig decodes base64url encoded (RFC4648, section 5) JSON
+// authentication information as sent through the X-Registry-Auth header.
+//
+// This function always returns an AuthConfig, even if an error occurs. It is up
+// to the caller to decide if authentication is required, and if the error can
+// be ignored.
+//
+// For details on base64url encoding, see:
+// - RFC4648, section 5:   https://tools.ietf.org/html/rfc4648#section-5
+func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
+	if authEncoded == "" {
+		return &AuthConfig{}, nil
+	}
+
+	authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
+	return decodeAuthConfigFromReader(authJSON)
+}
+
+// DecodeAuthConfigBody decodes authentication information as sent as JSON in the
+// body of a request. This function is to provide backward compatibility with old
+// clients and API versions. Current clients and API versions expect authentication
+// to be provided through the X-Registry-Auth header.
+//
+// Like DecodeAuthConfig, this function always returns an AuthConfig, even if an
+// error occurs. It is up to the caller to decide if authentication is required,
+// and if the error can be ignored.
+func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {
+	return decodeAuthConfigFromReader(rdr)
+}
+
+func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) {
+	authConfig := &AuthConfig{}
+	if err := json.NewDecoder(rdr).Decode(authConfig); err != nil {
+		// always return an (empty) AuthConfig to increase compatibility with
+		// the existing API.
+		return &AuthConfig{}, invalid(err)
+	}
+	return authConfig, nil
+}
+
+func invalid(err error) error {
+	return errInvalidParameter{errors.Wrap(err, "invalid X-Registry-Auth header")}
+}
+
+type errInvalidParameter struct{ error }
+
+func (errInvalidParameter) InvalidParameter() {}
+
+func (e errInvalidParameter) Cause() error { return e.error }
+
+func (e errInvalidParameter) Unwrap() error { return e.error }

+ 53 - 0
api/types/registry/authconfig_test.go

@@ -0,0 +1,53 @@
+package registry // import "github.com/docker/docker/api/types/registry"
+import (
+	"io"
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+const (
+	unencoded        = `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`
+	encoded          = `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`
+	encodedNoPadding = `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ`
+)
+
+var expected = AuthConfig{
+	Username:      "testuser",
+	Password:      "testpassword",
+	ServerAddress: "example.com",
+}
+
+func TestDecodeAuthConfig(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		token, err := DecodeAuthConfig(encoded)
+		assert.NilError(t, err)
+		assert.Equal(t, *token, expected)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		token, err := DecodeAuthConfig("")
+		assert.NilError(t, err)
+		assert.Equal(t, *token, AuthConfig{})
+	})
+
+	// We currently only support base64url encoding with padding, so
+	// un-padded should produce an error.
+	//
+	// RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
+	// RFC4648, section 3.2: https://tools.ietf.org/html/rfc4648#section-3.2
+	t.Run("invalid encoding", func(t *testing.T) {
+		token, err := DecodeAuthConfig(encodedNoPadding)
+
+		assert.ErrorType(t, err, errInvalidParameter{})
+		assert.ErrorContains(t, err, "invalid X-Registry-Auth header: unexpected EOF")
+		assert.Equal(t, *token, AuthConfig{})
+	})
+}
+
+func TestDecodeAuthConfigBody(t *testing.T) {
+	token, err := DecodeAuthConfigBody(io.NopCloser(strings.NewReader(unencoded)))
+	assert.NilError(t, err)
+	assert.Equal(t, *token, expected)
+}