helpers/docker/auth/auth.go (272 lines of code) (raw):
package auth
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"regexp"
"strings"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
"github.com/docker/docker/pkg/homedir"
"gitlab.com/gitlab-org/gitlab-runner/common"
)
const (
// DefaultDockerRegistry is the name of the index
DefaultDockerRegistry = "docker.io"
authConfigSourceNameUserVariable = "$DOCKER_AUTH_CONFIG"
authConfigSourceNameJobPayload = "job payload (GitLab Registry)"
)
var (
HomeDirectory = homedir.Get()
errNoHomeDir = errors.New("no home directory found")
errPathTraversal = errors.New("path traversal is not allowed")
)
// RegistryInfo represents the source and authentication for a given registry.
type RegistryInfo struct {
Source string
AuthConfig types.AuthConfig
}
type authConfigResolver func() (string, map[string]types.AuthConfig, error)
type DebugLogger interface {
Debugln(args ...interface{})
}
// the parent directory of a path or ""
func parentPath(path string) string {
index := strings.LastIndex(path, "/")
if index == -1 {
return ""
}
return path[:index]
}
// ResolveConfigForImage returns the auth configuration for a particular image.
// Returns nil on no config found.
// See ResolveConfigs for source information.
func ResolveConfigForImage(
imageName, dockerAuthConfig, username string,
credentials []common.Credentials, logger DebugLogger,
) (*RegistryInfo, error) {
authConfigs, err := ResolveConfigs(dockerAuthConfig, username, credentials, logger)
if len(authConfigs) == 0 || err != nil {
return nil, err
}
path := dockerImageNamePath(imageName)
for p := path; p != ""; p = parentPath(p) {
info, ok := authConfigs[p]
if ok {
return &info, nil
}
}
return nil, nil
}
// ResolveConfigs returns the authentication configuration for docker registries.
// Goes through several sources in this order:
// 1. DOCKER_AUTH_CONFIG
// 2. ~/.docker/config.json or .dockercfg
// 3. Build credentials
// Returns a map of registry hostname to RegistryInfo
func ResolveConfigs(
dockerAuthConfig, username string,
credentials []common.Credentials, logger DebugLogger,
) (map[string]RegistryInfo, error) {
resolvers := []authConfigResolver{
func() (string, map[string]types.AuthConfig, error) {
return getUserConfiguration(dockerAuthConfig)
},
func() (string, map[string]types.AuthConfig, error) {
return getHomeDirConfiguration(username)
},
func() (string, map[string]types.AuthConfig, error) {
return getBuildConfiguration(credentials)
},
}
res := make(map[string]RegistryInfo)
for _, r := range resolvers {
source, configs, err := r()
if errors.Is(err, errPathTraversal) {
return nil, err
}
var hostnames []string
for registry, conf := range configs {
registryPath := convertToRegistryPath(registry)
hostnames = append(hostnames, registryPath)
if _, ok := res[registryPath]; !ok {
res[registryPath] = RegistryInfo{
Source: source,
AuthConfig: conf,
}
}
}
// Source can be blank if there is no home dir configuration
if source != "" {
logger.Debugln(fmt.Sprintf("Loaded Docker credentials, source = %q, hostnames = %v, error = %v", source, hostnames, err))
}
}
return res, nil
}
func getUserConfiguration(dockerAuthConfig string) (string, map[string]types.AuthConfig, error) {
authConfigs, err := readConfigsFromReader(bytes.NewBufferString(dockerAuthConfig))
if errors.Is(err, errPathTraversal) {
return "", nil, err
}
if authConfigs == nil {
return "", nil, nil
}
return authConfigSourceNameUserVariable, authConfigs, nil
}
func getHomeDirConfiguration(username string) (string, map[string]types.AuthConfig, error) {
sourceFile, authConfigs, err := readDockerConfigsFromHomeDir(username)
if errors.Is(err, errPathTraversal) {
return "", nil, err
}
if authConfigs == nil {
return "", nil, nil
}
return sourceFile, authConfigs, nil
}
// EncodeConfig constructs a token from an AuthConfig, suitable for
// authorizing against the Docker API with.
func EncodeConfig(authConfig *types.AuthConfig) (string, error) {
if authConfig == nil {
return "", nil
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(authConfig); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
}
func getBuildConfiguration(credentials []common.Credentials) (string, map[string]types.AuthConfig, error) {
authConfigs := make(map[string]types.AuthConfig)
for _, credentials := range credentials {
if credentials.Type != "registry" {
continue
}
authConfigs[credentials.URL] = types.AuthConfig{
Username: credentials.Username,
Password: credentials.Password,
ServerAddress: credentials.URL,
}
}
return authConfigSourceNameJobPayload, authConfigs, nil
}
// Given a docker image reference get the path to lookup the authentication credentials
func dockerImageNamePath(imageName string) string {
imageIndex := strings.LastIndex(imageName, "/")
image := imageName
if imageIndex != -1 {
image = imageName[imageIndex+1:]
}
// remove tag
image, _, _ = strings.Cut(image, ":")
path := imageName[:imageIndex+1] + image
nameParts := strings.SplitN(imageName, "/", 2)
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
// 'docker.io'
path = DefaultDockerRegistry + "/" + path
} else if nameParts[0] == "index."+DefaultDockerRegistry {
path, _ = strings.CutPrefix(path, "index.")
}
return pathWithLowerCaseHostname(path)
}
// readDockerConfigsFromHomeDir reads known docker config from home
// directory. If no username is provided it will get the home directory for the
// current user.
func readDockerConfigsFromHomeDir(userName string) (string, map[string]types.AuthConfig, error) {
homeDir := HomeDirectory
if userName != "" {
u, err := user.Lookup(userName)
if err != nil {
return "", nil, err
}
homeDir = u.HomeDir
}
if homeDir == "" {
return "", nil, errNoHomeDir
}
configFile := filepath.Join(homeDir, ".docker", "config.json")
r, err := os.Open(configFile)
if err != nil {
configFile = filepath.Join(homeDir, ".dockercfg")
r, err = os.Open(configFile)
if err != nil && !os.IsNotExist(err) {
return "", nil, err
}
}
defer r.Close()
if r == nil {
return "", make(map[string]types.AuthConfig), nil
}
authConfigs, err := readConfigsFromReader(r)
return configFile, authConfigs, err
}
func readConfigsFromReader(r io.Reader) (map[string]types.AuthConfig, error) {
config := &configfile.ConfigFile{}
if err := config.LoadFromReader(r); err != nil {
if errors.Is(err, io.EOF) {
err = nil
}
return nil, err
}
auths := make(map[string]types.AuthConfig)
addAll(auths, config.AuthConfigs)
if config.CredentialsStore != "" {
authsFromCredentialsStore, err := readConfigsFromCredentialsStore(config)
if err != nil {
return nil, err
}
addAll(auths, authsFromCredentialsStore)
}
if config.CredentialHelpers != nil {
authsFromCredentialsHelpers, err := readConfigsFromCredentialsHelper(config)
if err != nil {
return nil, err
}
addAll(auths, authsFromCredentialsHelpers)
}
return auths, nil
}
func readConfigsFromCredentialsStore(config *configfile.ConfigFile) (map[string]types.AuthConfig, error) {
if config.CredentialsStore != filepath.Base(config.CredentialsStore) {
// Fail processing if credential store attempting path traversal are detected
return nil, errPathTraversal
}
store := credentials.NewNativeStore(config, config.CredentialsStore)
newAuths, err := store.GetAll()
if err != nil {
return nil, err
}
return newAuths, nil
}
func readConfigsFromCredentialsHelper(config *configfile.ConfigFile) (map[string]types.AuthConfig, error) {
helpersAuths := make(map[string]types.AuthConfig)
for registry, helper := range config.CredentialHelpers {
if helper != filepath.Base(helper) {
// Fail processing if credential helpers attempting path traversal are detected
return nil, errPathTraversal
}
store := credentials.NewNativeStore(config, helper)
newAuths, err := store.Get(registry)
if err != nil {
return nil, err
}
helpersAuths[registry] = newAuths
}
return helpersAuths, nil
}
func addAll(to, from map[string]types.AuthConfig) {
for reg, ac := range from {
to[reg] = ac
}
}
// convert hostname part to lower case.
// Since the hostname is case insensitive we convert it to lower case
// to allow matching with case sensitive comparison
func pathWithLowerCaseHostname(path string) string {
nameParts := strings.SplitN(path, "/", 2)
hostname := strings.ToLower(nameParts[0])
if len(nameParts) == 1 {
return hostname
}
return hostname + "/" + nameParts[1]
}
// Returns the normalized path for a docker registry reference for some credentials.
func convertToRegistryPath(imageRef string) string {
protocol := regexp.MustCompile("(?i)^http[s]://")
if protocol.MatchString(imageRef) {
// old style with protocol and maybe suffix /v1/
// just the use hostname
path := protocol.ReplaceAllString(imageRef, "")
nameParts := strings.SplitN(path, "/", 2)
path = strings.ToLower(nameParts[0])
if path == "index."+DefaultDockerRegistry {
return DefaultDockerRegistry
}
return path
}
path := strings.TrimSuffix(imageRef, "/")
tagIndex := strings.LastIndex(path, ":")
pathIndex := strings.LastIndex(path, "/")
// remove image tag from path
if pathIndex != -1 && tagIndex > pathIndex {
path = path[:strings.LastIndex(path, ":")]
}
return pathWithLowerCaseHostname(path)
}