lib/backend/registrybackend/security/security.go (171 lines of code) (raw):
// Copyright (c) 2016-2019 Uber Technologies, Inc.
//
// 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 security
import (
"fmt"
"net/http"
"net/url"
"sync"
"github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
"github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api"
"github.com/uber/kraken/utils/httputil"
"github.com/uber/kraken/utils/log"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker-credential-helpers/client"
"github.com/docker/engine-api/types"
)
const (
basePingQuery = "http://%s/v2/"
registryVersionHeader = "Docker-Distribution-Api-Version"
tokenUsername = "<token>"
credentialHelperPrefix = "docker-credential-"
)
var v2Version = auth.APIVersion{
Type: "registry",
Version: "2.0",
}
// Config contains tls and basic auth configuration.
type Config struct {
TLS httputil.TLSConfig `yaml:"tls"`
BasicAuth *types.AuthConfig `yaml:"basic"`
RemoteCredentialsStore string `yaml:"credsStore"`
EnableHTTPFallback bool `yaml:"enableHTTPFallback"`
}
// Authenticator creates send options to authenticate requests to registry
// backends.
type Authenticator interface {
// Authenticate returns a send option to authenticate to the registry,
// scoped to the given image repository.
Authenticate(repo string) ([]httputil.SendOption, error)
}
type authenticator struct {
address string
config Config
roundTripper http.RoundTripper
credentialStore auth.CredentialStore
challengeManager challenge.Manager
tokenHandlers sync.Map
}
// NewAuthenticator returns a new authenticator for the given docker registry
// address, TLS, and credentials configuration. It supports both basic auth and
// token based authentication challenges. If TLS is disabled, no authentication
// is attempted.
func NewAuthenticator(address string, config Config) (Authenticator, error) {
rt := http.DefaultTransport.(*http.Transport).Clone()
tlsClientConfig, err := config.TLS.BuildClient()
if err != nil {
return nil, fmt.Errorf("build tls config for %q: %s", address, err)
}
rt.TLSClientConfig = tlsClientConfig
return &authenticator{
address: address,
config: config,
roundTripper: rt,
credentialStore: newCredentialStore(address, config),
challengeManager: challenge.NewSimpleManager(),
}, nil
}
func (a *authenticator) Authenticate(repo string) ([]httputil.SendOption, error) {
config := a.config
var opts []httputil.SendOption
if config.TLS.Client.Disabled {
opts = append(opts, httputil.SendNoop())
return opts, nil
}
if config.EnableHTTPFallback {
opts = append(opts, httputil.EnableHTTPFallback())
} else {
opts = append(opts, httputil.DisableHTTPFallback())
}
if !a.shouldAuth() {
opts = append(opts, httputil.SendTLSTransport(a.roundTripper))
return opts, nil
}
if err := a.updateChallenge(); err != nil {
return nil, fmt.Errorf("could not update auth challenge: %s", err)
}
opts = append(opts, httputil.SendTLSTransport(a.transport(repo)))
return opts, nil
}
func (a *authenticator) shouldAuth() bool {
return a.config.BasicAuth != nil || a.config.RemoteCredentialsStore != ""
}
func (a *authenticator) transport(repo string) http.RoundTripper {
basicHandler := auth.NewBasicHandler(a.credentialStore)
bearerHandler, _ := a.tokenHandlers.LoadOrStore(repo, auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
Transport: a.roundTripper,
Credentials: a.credentialStore,
Scopes: []auth.Scope{
auth.RepositoryScope{
Repository: repo,
Actions: []string{"pull", "push"},
},
},
ClientID: "docker",
}))
return transport.NewTransport(a.roundTripper, auth.NewAuthorizer(a.challengeManager, basicHandler, bearerHandler.(auth.AuthenticationHandler)))
}
func (a *authenticator) updateChallenge() error {
resp, err := httputil.Send(
"GET",
fmt.Sprintf(basePingQuery, a.address),
httputil.SendTLSTransport(a.roundTripper),
httputil.SendAcceptedCodes(http.StatusOK, http.StatusUnauthorized),
)
if err != nil {
return err
}
versions := auth.APIVersions(resp, registryVersionHeader)
for _, version := range versions {
if version == v2Version {
if err := a.challengeManager.AddResponse(resp); err != nil {
return fmt.Errorf("add response: %s", err)
}
return nil
}
}
return fmt.Errorf("registry is not v2")
}
type credentialStore struct {
address string
config Config
}
func newCredentialStore(address string, config Config) *credentialStore {
return &credentialStore{
address: address,
config: config,
}
}
func (c credentialStore) Basic(*url.URL) (string, string) {
if username, password := c.credentialsFromHelper(); username != "" && username != tokenUsername {
return username, password
}
basic := c.config.BasicAuth
if basic == nil {
return "", ""
}
return basic.Username, basic.Password
}
func (c credentialStore) RefreshToken(*url.URL, string) string {
if username, token := c.credentialsFromHelper(); username == tokenUsername {
return token
}
basic := c.config.BasicAuth
if basic == nil {
return ""
}
return basic.IdentityToken
}
func (c credentialStore) credentialsFromHelper() (string, string) {
switch c.config.RemoteCredentialsStore {
case "":
// No credential helper configured, caller will use static credentials
// from configuration.
return "", ""
case "ecr-login":
client := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}
username, password, err := client.Get(c.address)
if err != nil {
log.Errorf("get credentials from helper ECR for %q: %s", c.address, err)
}
return username, password
default:
helper := credentialHelperPrefix + c.config.RemoteCredentialsStore
creds, err := client.Get(client.NewShellProgramFunc(helper), c.address)
if err != nil {
log.Errorf("get credentials from helper %s for %q: %s", c.config.RemoteCredentialsStore, c.address, err)
return "", ""
}
return creds.Username, creds.Secret
}
}
func (c credentialStore) SetRefreshToken(*url.URL, string, string) {}