github.com/uber/kraken@v0.1.4/lib/backend/registrybackend/security/security.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package security
    15  
    16  import (
    17  	"fmt"
    18  	"net/http"
    19  	"net/url"
    20  	"sync"
    21  
    22  	"github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
    23  	"github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api"
    24  	"github.com/uber/kraken/utils/httputil"
    25  	"github.com/uber/kraken/utils/log"
    26  
    27  	"github.com/docker/distribution/registry/client/auth"
    28  	"github.com/docker/distribution/registry/client/auth/challenge"
    29  	"github.com/docker/distribution/registry/client/transport"
    30  	"github.com/docker/docker-credential-helpers/client"
    31  	"github.com/docker/engine-api/types"
    32  )
    33  
    34  const (
    35  	basePingQuery          = "http://%s/v2/"
    36  	registryVersionHeader  = "Docker-Distribution-Api-Version"
    37  	tokenUsername          = "<token>"
    38  	credentialHelperPrefix = "docker-credential-"
    39  )
    40  
    41  var v2Version = auth.APIVersion{
    42  	Type:    "registry",
    43  	Version: "2.0",
    44  }
    45  
    46  // Config contains tls and basic auth configuration.
    47  type Config struct {
    48  	TLS                    httputil.TLSConfig `yaml:"tls"`
    49  	BasicAuth              *types.AuthConfig  `yaml:"basic"`
    50  	RemoteCredentialsStore string             `yaml:"credsStore"`
    51  	EnableHTTPFallback     bool               `yaml:"enableHTTPFallback"`
    52  }
    53  
    54  // Authenticator creates send options to authenticate requests to registry
    55  // backends.
    56  type Authenticator interface {
    57  	// Authenticate returns a send option to authenticate to the registry,
    58  	// scoped to the given image repository.
    59  	Authenticate(repo string) ([]httputil.SendOption, error)
    60  }
    61  
    62  type authenticator struct {
    63  	address          string
    64  	config           Config
    65  	roundTripper     http.RoundTripper
    66  	credentialStore  auth.CredentialStore
    67  	challengeManager challenge.Manager
    68  	tokenHandlers    sync.Map
    69  }
    70  
    71  // NewAuthenticator returns a new authenticator for the given docker registry
    72  // address, TLS, and credentials configuration. It supports both basic auth and
    73  // token based authentication challenges. If TLS is disabled, no authentication
    74  // is attempted.
    75  func NewAuthenticator(address string, config Config) (Authenticator, error) {
    76  	rt := http.DefaultTransport.(*http.Transport).Clone()
    77  	tlsClientConfig, err := config.TLS.BuildClient()
    78  	if err != nil {
    79  		return nil, fmt.Errorf("build tls config for %q: %s", address, err)
    80  	}
    81  	rt.TLSClientConfig = tlsClientConfig
    82  	return &authenticator{
    83  		address:          address,
    84  		config:           config,
    85  		roundTripper:     rt,
    86  		credentialStore:  newCredentialStore(address, config),
    87  		challengeManager: challenge.NewSimpleManager(),
    88  	}, nil
    89  }
    90  
    91  func (a *authenticator) Authenticate(repo string) ([]httputil.SendOption, error) {
    92  	config := a.config
    93  
    94  	var opts []httputil.SendOption
    95  	if config.TLS.Client.Disabled {
    96  		opts = append(opts, httputil.SendNoop())
    97  		return opts, nil
    98  	}
    99  
   100  	if !config.EnableHTTPFallback {
   101  		opts = append(opts, httputil.DisableHTTPFallback())
   102  	}
   103  	if !a.shouldAuth() {
   104  		opts = append(opts, httputil.SendTLSTransport(a.roundTripper))
   105  		return opts, nil
   106  	}
   107  	if err := a.updateChallenge(); err != nil {
   108  		return nil, fmt.Errorf("could not update auth challenge: %s", err)
   109  	}
   110  	opts = append(opts, httputil.SendTLSTransport(a.transport(repo)))
   111  	return opts, nil
   112  }
   113  
   114  func (a *authenticator) shouldAuth() bool {
   115  	return a.config.BasicAuth != nil || a.config.RemoteCredentialsStore != ""
   116  }
   117  
   118  func (a *authenticator) transport(repo string) http.RoundTripper {
   119  	basicHandler := auth.NewBasicHandler(a.credentialStore)
   120  	bearerHandler, _ := a.tokenHandlers.LoadOrStore(repo, auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
   121  		Transport:   a.roundTripper,
   122  		Credentials: a.credentialStore,
   123  		Scopes: []auth.Scope{
   124  			auth.RepositoryScope{
   125  				Repository: repo,
   126  				Actions:    []string{"pull", "push"},
   127  			},
   128  		},
   129  		ClientID: "docker",
   130  	}))
   131  	return transport.NewTransport(a.roundTripper, auth.NewAuthorizer(a.challengeManager, basicHandler, bearerHandler.(auth.AuthenticationHandler)))
   132  }
   133  
   134  func (a *authenticator) updateChallenge() error {
   135  	resp, err := httputil.Send(
   136  		"GET",
   137  		fmt.Sprintf(basePingQuery, a.address),
   138  		httputil.SendTLSTransport(a.roundTripper),
   139  		httputil.SendAcceptedCodes(http.StatusOK, http.StatusUnauthorized),
   140  	)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	versions := auth.APIVersions(resp, registryVersionHeader)
   145  	for _, version := range versions {
   146  		if version == v2Version {
   147  			if err := a.challengeManager.AddResponse(resp); err != nil {
   148  				return fmt.Errorf("add response: %s", err)
   149  			}
   150  			return nil
   151  		}
   152  	}
   153  	return fmt.Errorf("registry is not v2")
   154  }
   155  
   156  type credentialStore struct {
   157  	address string
   158  	config  Config
   159  }
   160  
   161  func newCredentialStore(address string, config Config) *credentialStore {
   162  	return &credentialStore{
   163  		address: address,
   164  		config:  config,
   165  	}
   166  }
   167  
   168  func (c credentialStore) Basic(*url.URL) (string, string) {
   169  	if username, password := c.credentialsFromHelper(); username != "" && username != tokenUsername {
   170  		return username, password
   171  	}
   172  	basic := c.config.BasicAuth
   173  	if basic == nil {
   174  		return "", ""
   175  	}
   176  	return basic.Username, basic.Password
   177  }
   178  
   179  func (c credentialStore) RefreshToken(*url.URL, string) string {
   180  	if username, token := c.credentialsFromHelper(); username == tokenUsername {
   181  		return token
   182  	}
   183  	basic := c.config.BasicAuth
   184  	if basic == nil {
   185  		return ""
   186  	}
   187  	return basic.IdentityToken
   188  }
   189  
   190  func (c credentialStore) credentialsFromHelper() (string, string) {
   191  	switch c.config.RemoteCredentialsStore {
   192  	case "":
   193  		// No credential helper configured, caller will use static credentials
   194  		// from configuration.
   195  		return "", ""
   196  	case "ecr-login":
   197  		client := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}
   198  		username, password, err := client.Get(c.address)
   199  		if err != nil {
   200  			log.Errorf("get credentials from helper ECR for %q: %s", c.address, err)
   201  		}
   202  		return username, password
   203  	default:
   204  		helper := credentialHelperPrefix + c.config.RemoteCredentialsStore
   205  		creds, err := client.Get(client.NewShellProgramFunc(helper), c.address)
   206  		if err != nil {
   207  			log.Errorf("get credentials from helper %s for %q: %s", c.config.RemoteCredentialsStore, c.address, err)
   208  			return "", ""
   209  		}
   210  		return creds.Username, creds.Secret
   211  	}
   212  }
   213  
   214  func (c credentialStore) SetRefreshToken(*url.URL, string, string) {}