mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
api: add registry.DecodeAuthConfig, registry.DecodeAuthConfigBody
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
e3a7a1c6ae
commit
7819811835
5 changed files with 140 additions and 72 deletions
|
@ -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 = ®istry.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 = ®istry.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)
|
||||
|
||||
|
|
|
@ -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 := ®istry.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 = ®istry.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 := ®istry.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 = ®istry.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 = ®istry.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,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 := ®istry.AuthConfig{}
|
||||
if authEncoded != "" {
|
||||
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
||||
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
||||
authConfig = ®istry.AuthConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore invalid AuthConfig to increase compatibility with the existing API.
|
||||
authConfig, _ := registry.DecodeAuthConfig(headers.Get(registry.AuthHeader))
|
||||
return metaHeaders, authConfig
|
||||
}
|
||||
|
||||
|
|
|
@ -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
api/types/registry/authconfig_test.go
Normal file
53
api/types/registry/authconfig_test.go
Normal file
|
@ -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)
|
||||
}
|
Loading…
Add table
Reference in a new issue