...

Source file src/github.com/google/go-containerregistry/pkg/authn/keychain.go

Documentation: github.com/google/go-containerregistry/pkg/authn

     1  // Copyright 2018 Google LLC All Rights Reserved.
     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  
    15  package authn
    16  
    17  import (
    18  	"os"
    19  	"path/filepath"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/docker/cli/cli/config"
    24  	"github.com/docker/cli/cli/config/configfile"
    25  	"github.com/docker/cli/cli/config/types"
    26  	"github.com/google/go-containerregistry/pkg/name"
    27  	"github.com/mitchellh/go-homedir"
    28  )
    29  
    30  // Resource represents a registry or repository that can be authenticated against.
    31  type Resource interface {
    32  	// String returns the full string representation of the target, e.g.
    33  	// gcr.io/my-project or just gcr.io.
    34  	String() string
    35  
    36  	// RegistryStr returns just the registry portion of the target, e.g. for
    37  	// gcr.io/my-project, this should just return gcr.io. This is needed to
    38  	// pull out an appropriate hostname.
    39  	RegistryStr() string
    40  }
    41  
    42  // Keychain is an interface for resolving an image reference to a credential.
    43  type Keychain interface {
    44  	// Resolve looks up the most appropriate credential for the specified target.
    45  	Resolve(Resource) (Authenticator, error)
    46  }
    47  
    48  // defaultKeychain implements Keychain with the semantics of the standard Docker
    49  // credential keychain.
    50  type defaultKeychain struct {
    51  	mu sync.Mutex
    52  }
    53  
    54  var (
    55  	// DefaultKeychain implements Keychain by interpreting the docker config file.
    56  	DefaultKeychain = &defaultKeychain{}
    57  )
    58  
    59  const (
    60  	// DefaultAuthKey is the key used for dockerhub in config files, which
    61  	// is hardcoded for historical reasons.
    62  	DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
    63  )
    64  
    65  // Resolve implements Keychain.
    66  func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
    67  	dk.mu.Lock()
    68  	defer dk.mu.Unlock()
    69  
    70  	// Podman users may have their container registry auth configured in a
    71  	// different location, that Docker packages aren't aware of.
    72  	// If the Docker config file isn't found, we'll fallback to look where
    73  	// Podman configures it, and parse that as a Docker auth config instead.
    74  
    75  	// First, check $HOME/.docker/config.json
    76  	foundDockerConfig := false
    77  	home, err := homedir.Dir()
    78  	if err == nil {
    79  		foundDockerConfig = fileExists(filepath.Join(home, ".docker/config.json"))
    80  	}
    81  	// If $HOME/.docker/config.json isn't found, check $DOCKER_CONFIG (if set)
    82  	if !foundDockerConfig && os.Getenv("DOCKER_CONFIG") != "" {
    83  		foundDockerConfig = fileExists(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json"))
    84  	}
    85  	// If either of those locations are found, load it using Docker's
    86  	// config.Load, which may fail if the config can't be parsed.
    87  	//
    88  	// If neither was found, look for Podman's auth at
    89  	// $XDG_RUNTIME_DIR/containers/auth.json and attempt to load it as a
    90  	// Docker config.
    91  	//
    92  	// If neither are found, fallback to Anonymous.
    93  	var cf *configfile.ConfigFile
    94  	if foundDockerConfig {
    95  		cf, err = config.Load(os.Getenv("DOCKER_CONFIG"))
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  	} else {
   100  		f, err := os.Open(filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers/auth.json"))
   101  		if err != nil {
   102  			return Anonymous, nil
   103  		}
   104  		defer f.Close()
   105  		cf, err = config.LoadFromReader(f)
   106  		if err != nil {
   107  			return nil, err
   108  		}
   109  	}
   110  
   111  	// See:
   112  	// https://github.com/google/ko/issues/90
   113  	// https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
   114  	var cfg, empty types.AuthConfig
   115  	for _, key := range []string{
   116  		target.String(),
   117  		target.RegistryStr(),
   118  	} {
   119  		if key == name.DefaultRegistry {
   120  			key = DefaultAuthKey
   121  		}
   122  
   123  		cfg, err = cf.GetAuthConfig(key)
   124  		if err != nil {
   125  			return nil, err
   126  		}
   127  		// cf.GetAuthConfig automatically sets the ServerAddress attribute. Since
   128  		// we don't make use of it, clear the value for a proper "is-empty" test.
   129  		// See: https://github.com/google/go-containerregistry/issues/1510
   130  		cfg.ServerAddress = ""
   131  		if cfg != empty {
   132  			break
   133  		}
   134  	}
   135  	if cfg == empty {
   136  		return Anonymous, nil
   137  	}
   138  
   139  	return FromConfig(AuthConfig{
   140  		Username:      cfg.Username,
   141  		Password:      cfg.Password,
   142  		Auth:          cfg.Auth,
   143  		IdentityToken: cfg.IdentityToken,
   144  		RegistryToken: cfg.RegistryToken,
   145  	}), nil
   146  }
   147  
   148  // fileExists returns true if the given path exists and is not a directory.
   149  func fileExists(path string) bool {
   150  	fi, err := os.Stat(path)
   151  	return err == nil && !fi.IsDir()
   152  }
   153  
   154  // Helper is a subset of the Docker credential helper credentials.Helper
   155  // interface used by NewKeychainFromHelper.
   156  //
   157  // See:
   158  // https://pkg.go.dev/github.com/docker/docker-credential-helpers/credentials#Helper
   159  type Helper interface {
   160  	Get(serverURL string) (string, string, error)
   161  }
   162  
   163  // NewKeychainFromHelper returns a Keychain based on a Docker credential helper
   164  // implementation that can Get username and password credentials for a given
   165  // server URL.
   166  func NewKeychainFromHelper(h Helper) Keychain { return wrapper{h} }
   167  
   168  type wrapper struct{ h Helper }
   169  
   170  func (w wrapper) Resolve(r Resource) (Authenticator, error) {
   171  	u, p, err := w.h.Get(r.RegistryStr())
   172  	if err != nil {
   173  		return Anonymous, nil
   174  	}
   175  	// If the secret being stored is an identity token, the Username should be set to <token>
   176  	// ref: https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol
   177  	if u == "<token>" {
   178  		return FromConfig(AuthConfig{Username: u, IdentityToken: p}), nil
   179  	}
   180  	return FromConfig(AuthConfig{Username: u, Password: p}), nil
   181  }
   182  
   183  func RefreshingKeychain(inner Keychain, duration time.Duration) Keychain {
   184  	return &refreshingKeychain{
   185  		keychain: inner,
   186  		duration: duration,
   187  	}
   188  }
   189  
   190  type refreshingKeychain struct {
   191  	keychain Keychain
   192  	duration time.Duration
   193  	clock    func() time.Time
   194  }
   195  
   196  func (r *refreshingKeychain) Resolve(target Resource) (Authenticator, error) {
   197  	last := time.Now()
   198  	auth, err := r.keychain.Resolve(target)
   199  	if err != nil || auth == Anonymous {
   200  		return auth, err
   201  	}
   202  	return &refreshing{
   203  		target:   target,
   204  		keychain: r.keychain,
   205  		last:     last,
   206  		cached:   auth,
   207  		duration: r.duration,
   208  		clock:    r.clock,
   209  	}, nil
   210  }
   211  
   212  type refreshing struct {
   213  	sync.Mutex
   214  	target   Resource
   215  	keychain Keychain
   216  
   217  	duration time.Duration
   218  
   219  	last   time.Time
   220  	cached Authenticator
   221  
   222  	// for testing
   223  	clock func() time.Time
   224  }
   225  
   226  func (r *refreshing) Authorization() (*AuthConfig, error) {
   227  	r.Lock()
   228  	defer r.Unlock()
   229  	if r.cached == nil || r.expired() {
   230  		r.last = r.now()
   231  		auth, err := r.keychain.Resolve(r.target)
   232  		if err != nil {
   233  			return nil, err
   234  		}
   235  		r.cached = auth
   236  	}
   237  	return r.cached.Authorization()
   238  }
   239  
   240  func (r *refreshing) now() time.Time {
   241  	if r.clock == nil {
   242  		return time.Now()
   243  	}
   244  	return r.clock()
   245  }
   246  
   247  func (r *refreshing) expired() bool {
   248  	return r.now().Sub(r.last) > r.duration
   249  }
   250  

View as plain text