// Copyright 2018 Google LLC All Rights Reserved. // // 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 authn import ( "os" "path/filepath" "sync" "time" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/types" "github.com/google/go-containerregistry/pkg/name" "github.com/mitchellh/go-homedir" ) // Resource represents a registry or repository that can be authenticated against. type Resource interface { // String returns the full string representation of the target, e.g. // gcr.io/my-project or just gcr.io. String() string // RegistryStr returns just the registry portion of the target, e.g. for // gcr.io/my-project, this should just return gcr.io. This is needed to // pull out an appropriate hostname. RegistryStr() string } // Keychain is an interface for resolving an image reference to a credential. type Keychain interface { // Resolve looks up the most appropriate credential for the specified target. Resolve(Resource) (Authenticator, error) } // defaultKeychain implements Keychain with the semantics of the standard Docker // credential keychain. type defaultKeychain struct { mu sync.Mutex } var ( // DefaultKeychain implements Keychain by interpreting the docker config file. DefaultKeychain = &defaultKeychain{} ) const ( // DefaultAuthKey is the key used for dockerhub in config files, which // is hardcoded for historical reasons. DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/" ) // Resolve implements Keychain. func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) { dk.mu.Lock() defer dk.mu.Unlock() // Podman users may have their container registry auth configured in a // different location, that Docker packages aren't aware of. // If the Docker config file isn't found, we'll fallback to look where // Podman configures it, and parse that as a Docker auth config instead. // First, check $HOME/.docker/config.json foundDockerConfig := false home, err := homedir.Dir() if err == nil { foundDockerConfig = fileExists(filepath.Join(home, ".docker/config.json")) } // If $HOME/.docker/config.json isn't found, check $DOCKER_CONFIG (if set) if !foundDockerConfig && os.Getenv("DOCKER_CONFIG") != "" { foundDockerConfig = fileExists(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json")) } // If either of those locations are found, load it using Docker's // config.Load, which may fail if the config can't be parsed. // // If neither was found, look for Podman's auth at // $XDG_RUNTIME_DIR/containers/auth.json and attempt to load it as a // Docker config. // // If neither are found, fallback to Anonymous. var cf *configfile.ConfigFile if foundDockerConfig { cf, err = config.Load(os.Getenv("DOCKER_CONFIG")) if err != nil { return nil, err } } else { f, err := os.Open(filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers/auth.json")) if err != nil { return Anonymous, nil } defer f.Close() cf, err = config.LoadFromReader(f) if err != nil { return nil, err } } // See: // https://github.com/google/ko/issues/90 // https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404 var cfg, empty types.AuthConfig for _, key := range []string{ target.String(), target.RegistryStr(), } { if key == name.DefaultRegistry { key = DefaultAuthKey } cfg, err = cf.GetAuthConfig(key) if err != nil { return nil, err } // cf.GetAuthConfig automatically sets the ServerAddress attribute. Since // we don't make use of it, clear the value for a proper "is-empty" test. // See: https://github.com/google/go-containerregistry/issues/1510 cfg.ServerAddress = "" if cfg != empty { break } } if cfg == empty { return Anonymous, nil } return FromConfig(AuthConfig{ Username: cfg.Username, Password: cfg.Password, Auth: cfg.Auth, IdentityToken: cfg.IdentityToken, RegistryToken: cfg.RegistryToken, }), nil } // fileExists returns true if the given path exists and is not a directory. func fileExists(path string) bool { fi, err := os.Stat(path) return err == nil && !fi.IsDir() } // Helper is a subset of the Docker credential helper credentials.Helper // interface used by NewKeychainFromHelper. // // See: // https://pkg.go.dev/github.com/docker/docker-credential-helpers/credentials#Helper type Helper interface { Get(serverURL string) (string, string, error) } // NewKeychainFromHelper returns a Keychain based on a Docker credential helper // implementation that can Get username and password credentials for a given // server URL. func NewKeychainFromHelper(h Helper) Keychain { return wrapper{h} } type wrapper struct{ h Helper } func (w wrapper) Resolve(r Resource) (Authenticator, error) { u, p, err := w.h.Get(r.RegistryStr()) if err != nil { return Anonymous, nil } // If the secret being stored is an identity token, the Username should be set to // ref: https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol if u == "" { return FromConfig(AuthConfig{Username: u, IdentityToken: p}), nil } return FromConfig(AuthConfig{Username: u, Password: p}), nil } func RefreshingKeychain(inner Keychain, duration time.Duration) Keychain { return &refreshingKeychain{ keychain: inner, duration: duration, } } type refreshingKeychain struct { keychain Keychain duration time.Duration clock func() time.Time } func (r *refreshingKeychain) Resolve(target Resource) (Authenticator, error) { last := time.Now() auth, err := r.keychain.Resolve(target) if err != nil || auth == Anonymous { return auth, err } return &refreshing{ target: target, keychain: r.keychain, last: last, cached: auth, duration: r.duration, clock: r.clock, }, nil } type refreshing struct { sync.Mutex target Resource keychain Keychain duration time.Duration last time.Time cached Authenticator // for testing clock func() time.Time } func (r *refreshing) Authorization() (*AuthConfig, error) { r.Lock() defer r.Unlock() if r.cached == nil || r.expired() { r.last = r.now() auth, err := r.keychain.Resolve(r.target) if err != nil { return nil, err } r.cached = auth } return r.cached.Authorization() } func (r *refreshing) now() time.Time { if r.clock == nil { return time.Now() } return r.clock() } func (r *refreshing) expired() bool { return r.now().Sub(r.last) > r.duration }