296 lines
9.1 KiB
Go
296 lines
9.1 KiB
Go
|
package config
|
||
|
|
||
|
import (
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/containers/image/types"
|
||
|
helperclient "github.com/docker/docker-credential-helpers/client"
|
||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||
|
"github.com/docker/docker/pkg/homedir"
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
type dockerAuthConfig struct {
|
||
|
Auth string `json:"auth,omitempty"`
|
||
|
}
|
||
|
|
||
|
type dockerConfigFile struct {
|
||
|
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
|
||
|
CredHelpers map[string]string `json:"credHelpers,omitempty"`
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
defaultPath = "/run/user"
|
||
|
authCfg = "containers"
|
||
|
authCfgFileName = "auth.json"
|
||
|
dockerCfg = ".docker"
|
||
|
dockerCfgFileName = "config.json"
|
||
|
dockerLegacyCfg = ".dockercfg"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// ErrNotLoggedIn is returned for users not logged into a registry
|
||
|
// that they are trying to logout of
|
||
|
ErrNotLoggedIn = errors.New("not logged in")
|
||
|
)
|
||
|
|
||
|
// SetAuthentication stores the username and password in the auth.json file
|
||
|
func SetAuthentication(ctx *types.SystemContext, registry, username, password string) error {
|
||
|
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
|
||
|
if ch, exists := auths.CredHelpers[registry]; exists {
|
||
|
return false, setAuthToCredHelper(ch, registry, username, password)
|
||
|
}
|
||
|
|
||
|
creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||
|
newCreds := dockerAuthConfig{Auth: creds}
|
||
|
auths.AuthConfigs[registry] = newCreds
|
||
|
return true, nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// GetAuthentication returns the registry credentials stored in
|
||
|
// either auth.json file or .docker/config.json
|
||
|
// If an entry is not found empty strings are returned for the username and password
|
||
|
func GetAuthentication(ctx *types.SystemContext, registry string) (string, string, error) {
|
||
|
if ctx != nil && ctx.DockerAuthConfig != nil {
|
||
|
return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil
|
||
|
}
|
||
|
|
||
|
dockerLegacyPath := filepath.Join(homedir.Get(), dockerLegacyCfg)
|
||
|
paths := [3]string{getPathToAuth(ctx), filepath.Join(homedir.Get(), dockerCfg, dockerCfgFileName), dockerLegacyPath}
|
||
|
|
||
|
for _, path := range paths {
|
||
|
legacyFormat := path == dockerLegacyPath
|
||
|
username, password, err := findAuthentication(registry, path, legacyFormat)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
if username != "" && password != "" {
|
||
|
return username, password, nil
|
||
|
}
|
||
|
}
|
||
|
return "", "", nil
|
||
|
}
|
||
|
|
||
|
// GetUserLoggedIn returns the username logged in to registry from either
|
||
|
// auth.json or XDG_RUNTIME_DIR
|
||
|
// Used to tell the user if someone is logged in to the registry when logging in
|
||
|
func GetUserLoggedIn(ctx *types.SystemContext, registry string) string {
|
||
|
path := getPathToAuth(ctx)
|
||
|
username, _, _ := findAuthentication(registry, path, false)
|
||
|
if username != "" {
|
||
|
return username
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// RemoveAuthentication deletes the credentials stored in auth.json
|
||
|
func RemoveAuthentication(ctx *types.SystemContext, registry string) error {
|
||
|
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
|
||
|
// First try cred helpers.
|
||
|
if ch, exists := auths.CredHelpers[registry]; exists {
|
||
|
return false, deleteAuthFromCredHelper(ch, registry)
|
||
|
}
|
||
|
|
||
|
if _, ok := auths.AuthConfigs[registry]; ok {
|
||
|
delete(auths.AuthConfigs, registry)
|
||
|
} else if _, ok := auths.AuthConfigs[normalizeRegistry(registry)]; ok {
|
||
|
delete(auths.AuthConfigs, normalizeRegistry(registry))
|
||
|
} else {
|
||
|
return false, ErrNotLoggedIn
|
||
|
}
|
||
|
return true, nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// RemoveAllAuthentication deletes all the credentials stored in auth.json
|
||
|
func RemoveAllAuthentication(ctx *types.SystemContext) error {
|
||
|
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
|
||
|
auths.CredHelpers = make(map[string]string)
|
||
|
auths.AuthConfigs = make(map[string]dockerAuthConfig)
|
||
|
return true, nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// getPath gets the path of the auth.json file
|
||
|
// The path can be overriden by the user if the overwrite-path flag is set
|
||
|
// If the flag is not set and XDG_RUNTIME_DIR is ser, the auth.json file is saved in XDG_RUNTIME_DIR/containers
|
||
|
// Otherwise, the auth.json file is stored in /run/user/UID/containers
|
||
|
func getPathToAuth(ctx *types.SystemContext) string {
|
||
|
if ctx != nil {
|
||
|
if ctx.AuthFilePath != "" {
|
||
|
return ctx.AuthFilePath
|
||
|
}
|
||
|
if ctx.RootForImplicitAbsolutePaths != "" {
|
||
|
return filepath.Join(ctx.RootForImplicitAbsolutePaths, defaultPath, strconv.Itoa(os.Getuid()), authCfg, authCfgFileName)
|
||
|
}
|
||
|
}
|
||
|
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||
|
if runtimeDir == "" {
|
||
|
runtimeDir = filepath.Join(defaultPath, strconv.Itoa(os.Getuid()))
|
||
|
}
|
||
|
return filepath.Join(runtimeDir, authCfg, authCfgFileName)
|
||
|
}
|
||
|
|
||
|
// readJSONFile unmarshals the authentications stored in the auth.json file and returns it
|
||
|
// or returns an empty dockerConfigFile data structure if auth.json does not exist
|
||
|
// if the file exists and is empty, readJSONFile returns an error
|
||
|
func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
|
||
|
var auths dockerConfigFile
|
||
|
|
||
|
raw, err := ioutil.ReadFile(path)
|
||
|
if os.IsNotExist(err) {
|
||
|
auths.AuthConfigs = map[string]dockerAuthConfig{}
|
||
|
return auths, nil
|
||
|
}
|
||
|
|
||
|
if legacyFormat {
|
||
|
if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil {
|
||
|
return dockerConfigFile{}, errors.Wrapf(err, "error unmarshaling JSON at %q", path)
|
||
|
}
|
||
|
return auths, nil
|
||
|
}
|
||
|
|
||
|
if err = json.Unmarshal(raw, &auths); err != nil {
|
||
|
return dockerConfigFile{}, errors.Wrapf(err, "error unmarshaling JSON at %q", path)
|
||
|
}
|
||
|
|
||
|
return auths, nil
|
||
|
}
|
||
|
|
||
|
// modifyJSON writes to auth.json if the dockerConfigFile has been updated
|
||
|
func modifyJSON(ctx *types.SystemContext, editor func(auths *dockerConfigFile) (bool, error)) error {
|
||
|
path := getPathToAuth(ctx)
|
||
|
dir := filepath.Dir(path)
|
||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||
|
if err = os.Mkdir(dir, 0700); err != nil {
|
||
|
return errors.Wrapf(err, "error creating directory %q", dir)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
auths, err := readJSONFile(path, false)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "error reading JSON file %q", path)
|
||
|
}
|
||
|
|
||
|
updated, err := editor(&auths)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "error updating %q", path)
|
||
|
}
|
||
|
if updated {
|
||
|
newData, err := json.MarshalIndent(auths, "", "\t")
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "error marshaling JSON %q", path)
|
||
|
}
|
||
|
|
||
|
if err = ioutil.WriteFile(path, newData, 0755); err != nil {
|
||
|
return errors.Wrapf(err, "error writing to file %q", path)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
|
||
|
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||
|
p := helperclient.NewShellProgramFunc(helperName)
|
||
|
creds, err := helperclient.Get(p, registry)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
return creds.Username, creds.Secret, nil
|
||
|
}
|
||
|
|
||
|
func setAuthToCredHelper(credHelper, registry, username, password string) error {
|
||
|
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||
|
p := helperclient.NewShellProgramFunc(helperName)
|
||
|
creds := &credentials.Credentials{
|
||
|
ServerURL: registry,
|
||
|
Username: username,
|
||
|
Secret: password,
|
||
|
}
|
||
|
return helperclient.Store(p, creds)
|
||
|
}
|
||
|
|
||
|
func deleteAuthFromCredHelper(credHelper, registry string) error {
|
||
|
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||
|
p := helperclient.NewShellProgramFunc(helperName)
|
||
|
return helperclient.Erase(p, registry)
|
||
|
}
|
||
|
|
||
|
// findAuthentication looks for auth of registry in path
|
||
|
func findAuthentication(registry, path string, legacyFormat bool) (string, string, error) {
|
||
|
auths, err := readJSONFile(path, legacyFormat)
|
||
|
if err != nil {
|
||
|
return "", "", errors.Wrapf(err, "error reading JSON file %q", path)
|
||
|
}
|
||
|
|
||
|
// First try cred helpers. They should always be normalized.
|
||
|
if ch, exists := auths.CredHelpers[registry]; exists {
|
||
|
return getAuthFromCredHelper(ch, registry)
|
||
|
}
|
||
|
|
||
|
// I'm feeling lucky
|
||
|
if val, exists := auths.AuthConfigs[registry]; exists {
|
||
|
return decodeDockerAuth(val.Auth)
|
||
|
}
|
||
|
|
||
|
// bad luck; let's normalize the entries first
|
||
|
registry = normalizeRegistry(registry)
|
||
|
normalizedAuths := map[string]dockerAuthConfig{}
|
||
|
for k, v := range auths.AuthConfigs {
|
||
|
normalizedAuths[normalizeRegistry(k)] = v
|
||
|
}
|
||
|
if val, exists := normalizedAuths[registry]; exists {
|
||
|
return decodeDockerAuth(val.Auth)
|
||
|
}
|
||
|
return "", "", nil
|
||
|
}
|
||
|
|
||
|
func decodeDockerAuth(s string) (string, string, error) {
|
||
|
decoded, err := base64.StdEncoding.DecodeString(s)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
parts := strings.SplitN(string(decoded), ":", 2)
|
||
|
if len(parts) != 2 {
|
||
|
// if it's invalid just skip, as docker does
|
||
|
return "", "", nil
|
||
|
}
|
||
|
user := parts[0]
|
||
|
password := strings.Trim(parts[1], "\x00")
|
||
|
return user, password, nil
|
||
|
}
|
||
|
|
||
|
// convertToHostname converts a registry url which has http|https prepended
|
||
|
// to just an hostname.
|
||
|
// Copied from github.com/docker/docker/registry/auth.go
|
||
|
func convertToHostname(url string) string {
|
||
|
stripped := url
|
||
|
if strings.HasPrefix(url, "http://") {
|
||
|
stripped = strings.TrimPrefix(url, "http://")
|
||
|
} else if strings.HasPrefix(url, "https://") {
|
||
|
stripped = strings.TrimPrefix(url, "https://")
|
||
|
}
|
||
|
|
||
|
nameParts := strings.SplitN(stripped, "/", 2)
|
||
|
|
||
|
return nameParts[0]
|
||
|
}
|
||
|
|
||
|
func normalizeRegistry(registry string) string {
|
||
|
normalized := convertToHostname(registry)
|
||
|
switch normalized {
|
||
|
case "registry-1.docker.io", "docker.io":
|
||
|
return "index.docker.io"
|
||
|
}
|
||
|
return normalized
|
||
|
}
|