153 lines
4.5 KiB
Go
153 lines
4.5 KiB
Go
// 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/google/go-containerregistry/pkg/logs"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
)
|
|
|
|
// 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 registry.
|
|
Resolve(name.Registry) (Authenticator, error)
|
|
}
|
|
|
|
// defaultKeychain implements Keychain with the semantics of the standard Docker
|
|
// credential keychain.
|
|
type defaultKeychain struct{}
|
|
|
|
// configDir returns the directory containing Docker's config.json
|
|
func configDir() (string, error) {
|
|
if dc := os.Getenv("DOCKER_CONFIG"); dc != "" {
|
|
return dc, nil
|
|
}
|
|
if h := dockerUserHomeDir(); h != "" {
|
|
return filepath.Join(dockerUserHomeDir(), ".docker"), nil
|
|
}
|
|
return "", errNoHomeDir
|
|
}
|
|
|
|
var errNoHomeDir = errors.New("could not determine home directory")
|
|
|
|
// dockerUserHomeDir returns the current user's home directory, as interpreted by Docker.
|
|
func dockerUserHomeDir() string {
|
|
if runtime.GOOS == "windows" {
|
|
// Docker specifically expands "%USERPROFILE%" on Windows,
|
|
return os.Getenv("USERPROFILE")
|
|
}
|
|
// Docker defaults to "$HOME" Linux and OSX.
|
|
return os.Getenv("HOME")
|
|
}
|
|
|
|
// authEntry is a helper for JSON parsing an "auth" entry of config.json
|
|
// This is not meant for direct consumption.
|
|
type authEntry struct {
|
|
Auth string `json:"auth"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// cfg is a helper for JSON parsing Docker's config.json
|
|
// This is not meant for direct consumption.
|
|
type cfg struct {
|
|
CredHelper map[string]string `json:"credHelpers,omitempty"`
|
|
CredStore string `json:"credsStore,omitempty"`
|
|
Auths map[string]authEntry `json:"auths,omitempty"`
|
|
}
|
|
|
|
// There are a variety of ways a domain may get qualified within the Docker credential file.
|
|
// We enumerate them here as format strings.
|
|
var (
|
|
domainForms = []string{
|
|
// Allow naked domains
|
|
"%s",
|
|
// Allow scheme-prefixed.
|
|
"https://%s",
|
|
"http://%s",
|
|
// Allow scheme-prefixes with version in url path.
|
|
"https://%s/v1/",
|
|
"http://%s/v1/",
|
|
"https://%s/v2/",
|
|
"http://%s/v2/",
|
|
}
|
|
|
|
// Export an instance of the default keychain.
|
|
DefaultKeychain Keychain = &defaultKeychain{}
|
|
)
|
|
|
|
// Resolve implements Keychain.
|
|
func (dk *defaultKeychain) Resolve(reg name.Registry) (Authenticator, error) {
|
|
dir, err := configDir()
|
|
if err != nil {
|
|
logs.Warn.Printf("Unable to determine config dir: %v", err)
|
|
return Anonymous, nil
|
|
}
|
|
file := filepath.Join(dir, "config.json")
|
|
content, err := ioutil.ReadFile(file)
|
|
if err != nil {
|
|
logs.Warn.Printf("Unable to read %q: %v", file, err)
|
|
return Anonymous, nil
|
|
}
|
|
|
|
var cf cfg
|
|
if err := json.Unmarshal(content, &cf); err != nil {
|
|
logs.Warn.Printf("Unable to parse %q: %v", file, err)
|
|
return Anonymous, nil
|
|
}
|
|
|
|
// Per-registry credential helpers take precedence.
|
|
if cf.CredHelper != nil {
|
|
for _, form := range domainForms {
|
|
if entry, ok := cf.CredHelper[fmt.Sprintf(form, reg.Name())]; ok {
|
|
return &helper{name: entry, domain: reg, r: &defaultRunner{}}, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// A global credential helper is next in precedence.
|
|
if cf.CredStore != "" {
|
|
return &helper{name: cf.CredStore, domain: reg, r: &defaultRunner{}}, nil
|
|
}
|
|
|
|
// Lastly, the 'auths' section directly contains basic auth entries.
|
|
if cf.Auths != nil {
|
|
for _, form := range domainForms {
|
|
if entry, ok := cf.Auths[fmt.Sprintf(form, reg.Name())]; ok {
|
|
if entry.Auth != "" {
|
|
return &auth{entry.Auth}, nil
|
|
} else if entry.Username != "" {
|
|
return &Basic{Username: entry.Username, Password: entry.Password}, nil
|
|
} else {
|
|
// TODO(mattmoor): Support identitytoken
|
|
// TODO(mattmoor): Support registrytoken
|
|
return nil, fmt.Errorf("Unsupported entry in \"auths\" section of %q", file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback on anonymous.
|
|
return Anonymous, nil
|
|
}
|