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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/distribution/manifest/manifestlist"
|
"github.com/docker/distribution/manifest/manifestlist"
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"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")
|
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"]
|
image := vars["name"]
|
||||||
|
|
||||||
// TODO why is reference.ParseAnyReference() / reference.ParseNormalizedNamed() not using the reference.ErrTagInvalidFormat (and so on) errors?
|
// 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))
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
blobsrvc := distrepo.Blobs(ctx)
|
blobsrvc := distrepo.Blobs(ctx)
|
||||||
|
|
||||||
|
var distributionInspect registry.DistributionInspect
|
||||||
if canonicalRef, ok := namedRef.(reference.Canonical); !ok {
|
if canonicalRef, ok := namedRef.(reference.Canonical); !ok {
|
||||||
namedRef = reference.TagNameOnly(namedRef)
|
namedRef = reference.TagNameOnly(namedRef)
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,6 @@ package image // import "github.com/docker/docker/api/server/router/image"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -64,16 +62,9 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authEncoded := r.Header.Get(registry.AuthHeader)
|
// For a pull it is not an error if no auth was given. Ignore invalid
|
||||||
authConfig := ®istry.AuthConfig{}
|
// AuthConfig to increase compatibility with the existing API.
|
||||||
if authEncoded != "" {
|
authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
|
||||||
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{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
progressErr = s.backend.PullImage(ctx, image, tag, platform, metaHeaders, authConfig, output)
|
progressErr = s.backend.PullImage(ctx, image, tag, platform, metaHeaders, authConfig, output)
|
||||||
} else { // import
|
} else { // import
|
||||||
src := r.Form.Get("fromSrc")
|
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 {
|
if err := httputils.ParseForm(r); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
authConfig := ®istry.AuthConfig{}
|
|
||||||
|
|
||||||
authEncoded := r.Header.Get(registry.AuthHeader)
|
var authConfig *registry.AuthConfig
|
||||||
if authEncoded != "" {
|
if authEncoded := r.Header.Get(registry.AuthHeader); authEncoded != "" {
|
||||||
// the new format is to handle the authConfig as a header
|
// the new format is to handle the authConfig as a header. Ignore invalid
|
||||||
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
// AuthConfig to increase compatibility with the existing API.
|
||||||
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
authConfig, _ = registry.DecodeAuthConfig(authEncoded)
|
||||||
// to increase compatibility to existing api it is defaulting to be empty
|
|
||||||
authConfig = ®istry.AuthConfig{}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// the old format is supported for compatibility if there was no authConfig header
|
// the old format is supported for compatibility if there was no authConfig header
|
||||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
var err error
|
||||||
return errors.Wrap(errdefs.InvalidParameter(err), "Bad parameters and missing X-Registry-Auth")
|
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)
|
output := ioutils.NewWriteFlusher(w)
|
||||||
defer output.Close()
|
defer output.Close()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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() {
|
if !output.Flushed() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -359,20 +347,8 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
|
||||||
if err := httputils.ParseForm(r); err != nil {
|
if err := httputils.ParseForm(r); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
|
||||||
config *registry.AuthConfig
|
|
||||||
authEncoded = r.Header.Get(registry.AuthHeader)
|
|
||||||
headers = map[string][]string{}
|
|
||||||
)
|
|
||||||
|
|
||||||
if authEncoded != "" {
|
var headers = map[string][]string{}
|
||||||
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{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k, v := range r.Header {
|
for k, v := range r.Header {
|
||||||
if strings.HasPrefix(k, "X-Meta-") {
|
if strings.HasPrefix(k, "X-Meta-") {
|
||||||
headers[k] = v
|
headers[k] = v
|
||||||
|
@ -392,7 +368,10 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package plugin // import "github.com/docker/docker/api/server/router/plugin"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -19,7 +17,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfig) {
|
func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfig) {
|
||||||
|
|
||||||
metaHeaders := map[string][]string{}
|
metaHeaders := map[string][]string{}
|
||||||
for k, v := range headers {
|
for k, v := range headers {
|
||||||
if strings.HasPrefix(k, "X-Meta-") {
|
if strings.HasPrefix(k, "X-Meta-") {
|
||||||
|
@ -27,16 +24,8 @@ func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get X-Registry-Auth
|
// Ignore invalid AuthConfig to increase compatibility with the existing API.
|
||||||
authEncoded := headers.Get(registry.AuthHeader)
|
authConfig, _ := registry.DecodeAuthConfig(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{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return metaHeaders, authConfig
|
return metaHeaders, authConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
package registry // import "github.com/docker/docker/api/types/registry"
|
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
|
// AuthHeader is the name of the header used to send encoded registry
|
||||||
// authorization credentials for registry operations (push/pull).
|
// 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 is a bearer token to be sent to a registry
|
||||||
RegistryToken string `json:"registrytoken,omitempty"`
|
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