Merge pull request #2690 from dmcgowan/resolver-updates
Update Docker resolver to pass in Authorizer interface
This commit is contained in:
commit
43acab8100
@ -79,11 +79,6 @@ func GetResolver(ctx gocontext.Context, clicontext *cli.Context) (remotes.Resolv
|
|||||||
secret = rt
|
secret = rt
|
||||||
}
|
}
|
||||||
|
|
||||||
options.Credentials = func(host string) (string, string, error) {
|
|
||||||
// Only one host
|
|
||||||
return username, secret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
DialContext: (&net.Dialer{
|
DialContext: (&net.Dialer{
|
||||||
@ -104,5 +99,11 @@ func GetResolver(ctx gocontext.Context, clicontext *cli.Context) (remotes.Resolv
|
|||||||
Transport: tr,
|
Transport: tr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
credentials := func(host string) (string, string, error) {
|
||||||
|
// Only one host
|
||||||
|
return username, secret, nil
|
||||||
|
}
|
||||||
|
options.Authorizer = docker.NewAuthorizer(options.Client, credentials)
|
||||||
|
|
||||||
return docker.NewResolver(options), nil
|
return docker.NewResolver(options), nil
|
||||||
}
|
}
|
||||||
|
317
remotes/docker/authorizer.go
Normal file
317
remotes/docker/authorizer.go
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/errdefs"
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerAuthorizer struct {
|
||||||
|
credentials func(string) (string, string, error)
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
auth map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorizer creates a Docker authorizer using the provided function to
|
||||||
|
// get credentials for the token server or basic auth.
|
||||||
|
func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
return &dockerAuthorizer{
|
||||||
|
credentials: f,
|
||||||
|
client: client,
|
||||||
|
auth: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
|
||||||
|
// TODO: Lookup matching challenge and scope rather than just host
|
||||||
|
if auth := a.getAuth(req.URL.Host); auth != "" {
|
||||||
|
req.Header.Set("Authorization", auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
|
||||||
|
last := responses[len(responses)-1]
|
||||||
|
host := last.Request.URL.Host
|
||||||
|
for _, c := range parseAuthHeader(last.Header) {
|
||||||
|
if c.scheme == bearerAuth {
|
||||||
|
if err := invalidAuthorization(c, responses); err != nil {
|
||||||
|
// TODO: Clear token
|
||||||
|
a.setAuth(host, "")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(dmcg): Store challenge, not token
|
||||||
|
// Move token fetching to authorize
|
||||||
|
if err := a.setTokenAuth(ctx, host, c.parameters); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
} else if c.scheme == basicAuth {
|
||||||
|
// TODO: Resolve credentials on authorize
|
||||||
|
username, secret, err := a.credentials(host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if username != "" && secret != "" {
|
||||||
|
auth := username + ":" + secret
|
||||||
|
a.setAuth(host, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(auth))))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) getAuth(host string) string {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
return a.auth[host]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) setAuth(host string, auth string) bool {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
changed := a.auth[host] != auth
|
||||||
|
a.auth[host] = auth
|
||||||
|
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) setTokenAuth(ctx context.Context, host string, params map[string]string) error {
|
||||||
|
realm, ok := params["realm"]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("no realm specified for token auth challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
realmURL, err := url.Parse(realm)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid token auth challenge realm")
|
||||||
|
}
|
||||||
|
|
||||||
|
to := tokenOptions{
|
||||||
|
realm: realmURL.String(),
|
||||||
|
service: params["service"],
|
||||||
|
}
|
||||||
|
|
||||||
|
to.scopes = getTokenScopes(ctx, params)
|
||||||
|
if len(to.scopes) == 0 {
|
||||||
|
return errors.Errorf("no scope specified for token auth challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.credentials != nil {
|
||||||
|
to.username, to.secret, err = a.credentials(host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
if to.secret != "" {
|
||||||
|
// Credential information is provided, use oauth POST endpoint
|
||||||
|
token, err = a.fetchTokenWithOAuth(ctx, to)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to fetch oauth token")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Do request anonymously
|
||||||
|
token, err = a.fetchToken(ctx, to)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to fetch anonymous token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.setAuth(host, fmt.Sprintf("Bearer %s", token))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenOptions struct {
|
||||||
|
realm string
|
||||||
|
service string
|
||||||
|
scopes []string
|
||||||
|
username string
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
type postTokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
IssuedAt time.Time `json:"issued_at"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("scope", strings.Join(to.scopes, " "))
|
||||||
|
form.Set("service", to.service)
|
||||||
|
// TODO: Allow setting client_id
|
||||||
|
form.Set("client_id", "containerd-client")
|
||||||
|
|
||||||
|
if to.username == "" {
|
||||||
|
form.Set("grant_type", "refresh_token")
|
||||||
|
form.Set("refresh_token", to.secret)
|
||||||
|
} else {
|
||||||
|
form.Set("grant_type", "password")
|
||||||
|
form.Set("username", to.username)
|
||||||
|
form.Set("password", to.secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ctxhttp.PostForm(ctx, a.client, to.realm, form)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Registries without support for POST may return 404 for POST /v2/token.
|
||||||
|
// As of September 2017, GCR is known to return 404.
|
||||||
|
// As of February 2018, JFrog Artifactory is known to return 401.
|
||||||
|
if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
|
||||||
|
return a.fetchToken(ctx, to)
|
||||||
|
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||||
|
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
|
||||||
|
log.G(ctx).WithFields(logrus.Fields{
|
||||||
|
"status": resp.Status,
|
||||||
|
"body": string(b),
|
||||||
|
}).Debugf("token request failed")
|
||||||
|
// TODO: handle error body and write debug output
|
||||||
|
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
var tr postTokenResponse
|
||||||
|
if err = decoder.Decode(&tr); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type getTokenResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
IssuedAt time.Time `json:"issued_at"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getToken fetches a token using a GET request
|
||||||
|
func (a *dockerAuthorizer) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
|
||||||
|
req, err := http.NewRequest("GET", to.realm, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqParams := req.URL.Query()
|
||||||
|
|
||||||
|
if to.service != "" {
|
||||||
|
reqParams.Add("service", to.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range to.scopes {
|
||||||
|
reqParams.Add("scope", scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
if to.secret != "" {
|
||||||
|
req.SetBasicAuth(to.username, to.secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = reqParams.Encode()
|
||||||
|
|
||||||
|
resp, err := ctxhttp.Do(ctx, a.client, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||||
|
// TODO: handle error body and write debug output
|
||||||
|
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
var tr getTokenResponse
|
||||||
|
if err = decoder.Decode(&tr); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// `access_token` is equivalent to `token` and if both are specified
|
||||||
|
// the choice is undefined. Canonicalize `access_token` by sticking
|
||||||
|
// things in `token`.
|
||||||
|
if tr.AccessToken != "" {
|
||||||
|
tr.Token = tr.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if tr.Token == "" {
|
||||||
|
return "", ErrNoToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidAuthorization(c challenge, responses []*http.Response) error {
|
||||||
|
errStr := c.parameters["error"]
|
||||||
|
if errStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(responses)
|
||||||
|
if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameRequest(r1, r2 *http.Request) bool {
|
||||||
|
if r1.Method != r2.Method {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if *r1.URL != *r2.URL {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
@ -18,18 +18,13 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
"github.com/containerd/containerd/reference"
|
"github.com/containerd/containerd/reference"
|
||||||
@ -51,19 +46,37 @@ var (
|
|||||||
ErrInvalidAuthorization = errors.New("authorization failed")
|
ErrInvalidAuthorization = errors.New("authorization failed")
|
||||||
)
|
)
|
||||||
|
|
||||||
type dockerResolver struct {
|
// Authorizer is used to authorize HTTP requests based on 401 HTTP responses.
|
||||||
credentials func(string) (string, string, error)
|
// An Authorizer is responsible for caching tokens or credentials used by
|
||||||
host func(string) (string, error)
|
// requests.
|
||||||
plainHTTP bool
|
type Authorizer interface {
|
||||||
client *http.Client
|
// Authorize sets the appropriate `Authorization` header on the given
|
||||||
tracker StatusTracker
|
// request.
|
||||||
|
//
|
||||||
|
// If no authorization is found for the request, the request remains
|
||||||
|
// unmodified. It may also add an `Authorization` header as
|
||||||
|
// "bearer <some bearer token>"
|
||||||
|
// "basic <base64 encoded credentials>"
|
||||||
|
Authorize(context.Context, *http.Request) error
|
||||||
|
|
||||||
|
// AddResponses adds a 401 response for the authorizer to consider when
|
||||||
|
// authorizing requests. The last response should be unauthorized and
|
||||||
|
// the previous requests are used to consider redirects and retries
|
||||||
|
// that may have led to the 401.
|
||||||
|
//
|
||||||
|
// If response is not handled, returns `ErrNotImplemented`
|
||||||
|
AddResponses(context.Context, []*http.Response) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolverOptions are used to configured a new Docker register resolver
|
// ResolverOptions are used to configured a new Docker register resolver
|
||||||
type ResolverOptions struct {
|
type ResolverOptions struct {
|
||||||
|
// Authorizer is used to authorize registry requests
|
||||||
|
Authorizer Authorizer
|
||||||
|
|
||||||
// Credentials provides username and secret given a host.
|
// Credentials provides username and secret given a host.
|
||||||
// If username is empty but a secret is given, that secret
|
// If username is empty but a secret is given, that secret
|
||||||
// is interpretted as a long lived token.
|
// is interpretted as a long lived token.
|
||||||
|
// Deprecated: use Authorizer
|
||||||
Credentials func(string) (string, string, error)
|
Credentials func(string) (string, string, error)
|
||||||
|
|
||||||
// Host provides the hostname given a namespace.
|
// Host provides the hostname given a namespace.
|
||||||
@ -89,22 +102,31 @@ func DefaultHost(ns string) (string, error) {
|
|||||||
return ns, nil
|
return ns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dockerResolver struct {
|
||||||
|
auth Authorizer
|
||||||
|
host func(string) (string, error)
|
||||||
|
plainHTTP bool
|
||||||
|
client *http.Client
|
||||||
|
tracker StatusTracker
|
||||||
|
}
|
||||||
|
|
||||||
// NewResolver returns a new resolver to a Docker registry
|
// NewResolver returns a new resolver to a Docker registry
|
||||||
func NewResolver(options ResolverOptions) remotes.Resolver {
|
func NewResolver(options ResolverOptions) remotes.Resolver {
|
||||||
tracker := options.Tracker
|
if options.Tracker == nil {
|
||||||
if tracker == nil {
|
options.Tracker = NewInMemoryTracker()
|
||||||
tracker = NewInMemoryTracker()
|
|
||||||
}
|
}
|
||||||
host := options.Host
|
if options.Host == nil {
|
||||||
if host == nil {
|
options.Host = DefaultHost
|
||||||
host = DefaultHost
|
}
|
||||||
|
if options.Authorizer == nil {
|
||||||
|
options.Authorizer = NewAuthorizer(options.Client, options.Credentials)
|
||||||
}
|
}
|
||||||
return &dockerResolver{
|
return &dockerResolver{
|
||||||
credentials: options.Credentials,
|
auth: options.Authorizer,
|
||||||
host: host,
|
host: options.Host,
|
||||||
plainHTTP: options.PlainHTTP,
|
plainHTTP: options.PlainHTTP,
|
||||||
client: options.Client,
|
client: options.Client,
|
||||||
tracker: tracker,
|
tracker: options.Tracker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,17 +295,13 @@ type dockerBase struct {
|
|||||||
base url.URL
|
base url.URL
|
||||||
|
|
||||||
client *http.Client
|
client *http.Client
|
||||||
useBasic bool
|
auth Authorizer
|
||||||
username, secret string
|
|
||||||
token string
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
base url.URL
|
base url.URL
|
||||||
username, secret string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
host := refspec.Hostname()
|
host := refspec.Hostname()
|
||||||
@ -300,13 +318,6 @@ func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
|||||||
base.Scheme = "http"
|
base.Scheme = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.credentials != nil {
|
|
||||||
username, secret, err = r.credentials(base.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
||||||
base.Path = path.Join("/v2", prefix)
|
base.Path = path.Join("/v2", prefix)
|
||||||
|
|
||||||
@ -314,47 +325,33 @@ func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
|||||||
refspec: refspec,
|
refspec: refspec,
|
||||||
base: base,
|
base: base,
|
||||||
client: r.client,
|
client: r.client,
|
||||||
username: username,
|
auth: r.auth,
|
||||||
secret: secret,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) getToken() string {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
return r.token
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *dockerBase) setToken(token string) bool {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
changed := r.token != token
|
|
||||||
r.token = token
|
|
||||||
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *dockerBase) url(ps ...string) string {
|
func (r *dockerBase) url(ps ...string) string {
|
||||||
url := r.base
|
url := r.base
|
||||||
url.Path = path.Join(url.Path, path.Join(ps...))
|
url.Path = path.Join(url.Path, path.Join(ps...))
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) authorize(req *http.Request) {
|
func (r *dockerBase) authorize(ctx context.Context, req *http.Request) error {
|
||||||
token := r.getToken()
|
// Check if has header for host
|
||||||
if r.useBasic {
|
if r.auth != nil {
|
||||||
req.SetBasicAuth(r.username, r.secret)
|
if err := r.auth.Authorize(ctx, req); err != nil {
|
||||||
} else if token != "" {
|
return err
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||||
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
||||||
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request")
|
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request")
|
||||||
r.authorize(req)
|
if err := r.authorize(ctx, req); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to authorize")
|
||||||
|
}
|
||||||
resp, err := ctxhttp.Do(ctx, r.client, req)
|
resp, err := ctxhttp.Do(ctx, r.client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to do request")
|
return nil, errors.Wrap(err, "failed to do request")
|
||||||
@ -392,23 +389,14 @@ func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, respon
|
|||||||
last := responses[len(responses)-1]
|
last := responses[len(responses)-1]
|
||||||
if last.StatusCode == http.StatusUnauthorized {
|
if last.StatusCode == http.StatusUnauthorized {
|
||||||
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
||||||
for _, c := range parseAuthHeader(last.Header) {
|
if r.auth != nil {
|
||||||
if c.scheme == bearerAuth {
|
if err := r.auth.AddResponses(ctx, responses); err == nil {
|
||||||
if err := invalidAuthorization(c, responses); err != nil {
|
return copyRequest(req)
|
||||||
r.setToken("")
|
} else if !errdefs.IsNotImplemented(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.setTokenAuth(ctx, c.parameters); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return copyRequest(req)
|
|
||||||
} else if c.scheme == basicAuth {
|
|
||||||
if r.username != "" && r.secret != "" {
|
|
||||||
r.useBasic = true
|
|
||||||
}
|
|
||||||
return copyRequest(req)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
|
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
|
||||||
// Support registries which have not properly implemented the HEAD method for
|
// Support registries which have not properly implemented the HEAD method for
|
||||||
@ -424,30 +412,6 @@ func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, respon
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidAuthorization(c challenge, responses []*http.Response) error {
|
|
||||||
errStr := c.parameters["error"]
|
|
||||||
if errStr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
n := len(responses)
|
|
||||||
if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sameRequest(r1, r2 *http.Request) bool {
|
|
||||||
if r1.Method != r2.Method {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if *r1.URL != *r2.URL {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyRequest(req *http.Request) (*http.Request, error) {
|
func copyRequest(req *http.Request) (*http.Request, error) {
|
||||||
ireq := *req
|
ireq := *req
|
||||||
if ireq.GetBody != nil {
|
if ireq.GetBody != nil {
|
||||||
@ -459,167 +423,3 @@ func copyRequest(req *http.Request) (*http.Request, error) {
|
|||||||
}
|
}
|
||||||
return &ireq, nil
|
return &ireq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) error {
|
|
||||||
realm, ok := params["realm"]
|
|
||||||
if !ok {
|
|
||||||
return errors.New("no realm specified for token auth challenge")
|
|
||||||
}
|
|
||||||
|
|
||||||
realmURL, err := url.Parse(realm)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid token auth challenge realm: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
to := tokenOptions{
|
|
||||||
realm: realmURL.String(),
|
|
||||||
service: params["service"],
|
|
||||||
}
|
|
||||||
|
|
||||||
to.scopes = getTokenScopes(ctx, params)
|
|
||||||
if len(to.scopes) == 0 {
|
|
||||||
return errors.Errorf("no scope specified for token auth challenge")
|
|
||||||
}
|
|
||||||
|
|
||||||
var token string
|
|
||||||
if r.secret != "" {
|
|
||||||
// Credential information is provided, use oauth POST endpoint
|
|
||||||
token, err = r.fetchTokenWithOAuth(ctx, to)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to fetch oauth token")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Do request anonymously
|
|
||||||
token, err = r.fetchToken(ctx, to)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to fetch anonymous token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.setToken(token)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type tokenOptions struct {
|
|
||||||
realm string
|
|
||||||
service string
|
|
||||||
scopes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type postTokenResponse struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
IssuedAt time.Time `json:"issued_at"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *dockerBase) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
|
|
||||||
form := url.Values{}
|
|
||||||
form.Set("scope", strings.Join(to.scopes, " "))
|
|
||||||
form.Set("service", to.service)
|
|
||||||
// TODO: Allow setting client_id
|
|
||||||
form.Set("client_id", "containerd-dist-tool")
|
|
||||||
|
|
||||||
if r.username == "" {
|
|
||||||
form.Set("grant_type", "refresh_token")
|
|
||||||
form.Set("refresh_token", r.secret)
|
|
||||||
} else {
|
|
||||||
form.Set("grant_type", "password")
|
|
||||||
form.Set("username", r.username)
|
|
||||||
form.Set("password", r.secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Registries without support for POST may return 404 for POST /v2/token.
|
|
||||||
// As of September 2017, GCR is known to return 404.
|
|
||||||
// As of February 2018, JFrog Artifactory is known to return 401.
|
|
||||||
if (resp.StatusCode == 405 && r.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
|
|
||||||
return r.fetchToken(ctx, to)
|
|
||||||
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
|
||||||
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
|
|
||||||
log.G(ctx).WithFields(logrus.Fields{
|
|
||||||
"status": resp.Status,
|
|
||||||
"body": string(b),
|
|
||||||
}).Debugf("token request failed")
|
|
||||||
// TODO: handle error body and write debug output
|
|
||||||
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(resp.Body)
|
|
||||||
|
|
||||||
var tr postTokenResponse
|
|
||||||
if err = decoder.Decode(&tr); err != nil {
|
|
||||||
return "", fmt.Errorf("unable to decode token response: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tr.AccessToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type getTokenResponse struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
IssuedAt time.Time `json:"issued_at"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// getToken fetches a token using a GET request
|
|
||||||
func (r *dockerBase) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
|
|
||||||
req, err := http.NewRequest("GET", to.realm, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
reqParams := req.URL.Query()
|
|
||||||
|
|
||||||
if to.service != "" {
|
|
||||||
reqParams.Add("service", to.service)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, scope := range to.scopes {
|
|
||||||
reqParams.Add("scope", scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.secret != "" {
|
|
||||||
req.SetBasicAuth(r.username, r.secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.URL.RawQuery = reqParams.Encode()
|
|
||||||
|
|
||||||
resp, err := ctxhttp.Do(ctx, r.client, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
|
||||||
// TODO: handle error body and write debug output
|
|
||||||
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(resp.Body)
|
|
||||||
|
|
||||||
var tr getTokenResponse
|
|
||||||
if err = decoder.Decode(&tr); err != nil {
|
|
||||||
return "", fmt.Errorf("unable to decode token response: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// `access_token` is equivalent to `token` and if both are specified
|
|
||||||
// the choice is undefined. Canonicalize `access_token` by sticking
|
|
||||||
// things in `token`.
|
|
||||||
if tr.AccessToken != "" {
|
|
||||||
tr.Token = tr.AccessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if tr.Token == "" {
|
|
||||||
return "", ErrNoToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return tr.Token, nil
|
|
||||||
}
|
|
||||||
|
@ -69,9 +69,9 @@ func TestBasicResolver(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
base, options, close := tlsServer(wrapped)
|
base, options, close := tlsServer(wrapped)
|
||||||
options.Credentials = func(string) (string, string, error) {
|
options.Authorizer = NewAuthorizer(options.Client, func(string) (string, string, error) {
|
||||||
return "user1", "password1", nil
|
return "user1", "password1", nil
|
||||||
}
|
})
|
||||||
return base, options, close
|
return base, options, close
|
||||||
}
|
}
|
||||||
runBasicTest(t, "testname", basicAuth)
|
runBasicTest(t, "testname", basicAuth)
|
||||||
@ -215,7 +215,7 @@ func withTokenServer(th http.Handler, creds func(string) (string, string, error)
|
|||||||
})
|
})
|
||||||
|
|
||||||
base, options, close := tlsServer(wrapped)
|
base, options, close := tlsServer(wrapped)
|
||||||
options.Credentials = creds
|
options.Authorizer = NewAuthorizer(options.Client, creds)
|
||||||
options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
|
options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
|
||||||
return base, options, func() {
|
return base, options, func() {
|
||||||
s.Close()
|
s.Close()
|
||||||
|
Loading…
Reference in New Issue
Block a user