Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use creds helper #346

Draft
wants to merge 1 commit into
base: v3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
268 changes: 231 additions & 37 deletions docker/auth.go
Expand Up @@ -9,16 +9,17 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"strings"
)

// ErrCannotParseDockercfg is the error returned by NewAuthConfigurations when the dockercfg cannot be parsed.
var ErrCannotParseDockercfg = errors.New("Failed to read authentication from dockercfg")
var ErrCannotParseDockercfg = errors.New("failed to read authentication from dockercfg")

// AuthConfiguration represents authentication options to use in the PushImage
// method. It represents the authentication in the Docker index server.
Expand All @@ -27,7 +28,22 @@ type AuthConfiguration struct {
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
ServerAddress string `json:"serveraddress,omitempty"`

// IdentityToken can be supplied with the identitytoken response of the AuthCheck call
// see https://pkg.go.dev/github.com/docker/docker/api/types?tab=doc#AuthConfig
// It can be used in place of password not in conjunction with it
IdentityToken string `json:"identitytoken,omitempty"`

// RegistryToken can be supplied with the registrytoken
RegistryToken string `json:"registrytoken,omitempty"`
}

func (c AuthConfiguration) isEmpty() bool {
return c == AuthConfiguration{}
}

func (c AuthConfiguration) headerKey() string {
return "X-Registry-Auth"
}

// AuthConfigurations represents authentication options to use for the
Expand All @@ -36,16 +52,46 @@ type AuthConfigurations struct {
Configs map[string]AuthConfiguration `json:"configs"`
}

func (c AuthConfigurations) isEmpty() bool {
return len(c.Configs) == 0
}

func (AuthConfigurations) headerKey() string {
return "X-Registry-Config"
}

// merge updates the configuration. If a key is defined in both maps, the one
// in c.Configs takes precedence.
func (c *AuthConfigurations) merge(other AuthConfigurations) {
for k, v := range other.Configs {
if c.Configs == nil {
c.Configs = make(map[string]AuthConfiguration)
}
if _, ok := c.Configs[k]; !ok {
c.Configs[k] = v
}
}
}

// AuthConfigurations119 is used to serialize a set of AuthConfigurations
// for Docker API >= 1.19.
type AuthConfigurations119 map[string]AuthConfiguration

func (c AuthConfigurations119) isEmpty() bool {
return len(c) == 0
}

func (c AuthConfigurations119) headerKey() string {
return "X-Registry-Config"
}

// dockerConfig represents a registry authentation configuration from the
// .dockercfg file.
type dockerConfig struct {
Auth string `json:"auth"`
Email string `json:"email"`
IdentityToken string `json:"identitytoken"`
RegistryToken string `json:"registrytoken"`
}

// NewAuthConfigurationsFromFile returns AuthConfigurations from a path containing JSON
Expand All @@ -59,34 +105,65 @@ func NewAuthConfigurationsFromFile(path string) (*AuthConfigurations, error) {
}

func cfgPaths(dockerConfigEnv string, homeEnv string) []string {
var paths []string
if dockerConfigEnv != "" {
paths = append(paths, path.Join(dockerConfigEnv, "config.json"))
return []string{
path.Join(dockerConfigEnv, "plaintext-passwords.json"),
path.Join(dockerConfigEnv, "config.json"),
}
}
if homeEnv != "" {
paths = append(paths, path.Join(homeEnv, ".docker", "config.json"))
paths = append(paths, path.Join(homeEnv, ".dockercfg"))
return []string{
path.Join(homeEnv, ".docker", "plaintext-passwords.json"),
path.Join(homeEnv, ".docker", "config.json"),
path.Join(homeEnv, ".dockercfg"),
}
}
return paths
return nil
}

// NewAuthConfigurationsFromDockerCfg returns AuthConfigurations from
// system config files. The following files are checked in the order listed:
// - $DOCKER_CONFIG/config.json if DOCKER_CONFIG set in the environment,
// NewAuthConfigurationsFromDockerCfg returns AuthConfigurations from system
// config files. The following files are checked in the order listed:
//
// If the environment variable DOCKER_CONFIG is set to a non-empty string:
//
// - $DOCKER_CONFIG/plaintext-passwords.json
// - $DOCKER_CONFIG/config.json
//
// Otherwise, it looks for files in the $HOME directory and the legacy
// location:
//
// - $HOME/.docker/plaintext-passwords.json
// - $HOME/.docker/config.json
// - $HOME/.dockercfg
func NewAuthConfigurationsFromDockerCfg() (*AuthConfigurations, error) {
err := fmt.Errorf("No docker configuration found")
var auths *AuthConfigurations

pathsToTry := cfgPaths(os.Getenv("DOCKER_CONFIG"), os.Getenv("HOME"))
if len(pathsToTry) < 1 {
return nil, errors.New("no docker configuration found")
}
return newAuthConfigurationsFromDockerCfg(pathsToTry)
}

func newAuthConfigurationsFromDockerCfg(pathsToTry []string) (*AuthConfigurations, error) {
var result *AuthConfigurations
var auths *AuthConfigurations
var err error
for _, path := range pathsToTry {
auths, err = NewAuthConfigurationsFromFile(path)
if err == nil {
return auths, nil
if err != nil {
continue
}

if result == nil {
result = auths
} else {
result.merge(*auths)
}
}

if result != nil {
return result, nil
}
return auths, err
return result, err
}

// NewAuthConfigurations returns AuthConfigurations from a JSON encoded string in the
Expand Down Expand Up @@ -130,29 +207,47 @@ func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) {
c := &AuthConfigurations{
Configs: make(map[string]AuthConfiguration),
}

for reg, conf := range confs {
switch {
case conf.IdentityToken != "":
c.Configs[reg] = AuthConfiguration{
IdentityToken: conf.IdentityToken,
}
case conf.Auth != "":
data, err := base64.StdEncoding.DecodeString(conf.Auth)
if err != nil {
return nil, err
}
userpass := strings.SplitN(string(data), ":", 2)
if len(userpass) != 2 {
return nil, ErrCannotParseDockercfg
}
c.Configs[reg] = AuthConfiguration{
Email: conf.Email,
Username: userpass[0],
Password: userpass[1],
ServerAddress: reg,
}
if conf.Auth == "" {
continue
}

// support both padded and unpadded encoding
data, err := base64.StdEncoding.DecodeString(conf.Auth)
if err != nil {
data, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(conf.Auth)
}
if err != nil {
return nil, errors.New("error decoding plaintext credentials")
}

userpass := strings.SplitN(string(data), ":", 2)
if len(userpass) != 2 {
return nil, ErrCannotParseDockercfg
}

authConfig := AuthConfiguration{
Email: conf.Email,
Username: userpass[0],
Password: userpass[1],
ServerAddress: reg,
}

// if identitytoken provided then zero the password and set it
if conf.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = conf.IdentityToken
}

// if registrytoken provided then zero the password and set it
if conf.RegistryToken != "" {
authConfig.Password = ""
authConfig.RegistryToken = conf.RegistryToken
}
c.Configs[reg] = authConfig
}

return c, nil
}

Expand All @@ -172,7 +267,7 @@ func (c *Client) AuthCheck(conf *AuthConfiguration) (AuthStatus, error) {
if conf == nil {
return authStatus, errors.New("conf is nil")
}
resp, err := c.do("POST", "/auth", doOptions{data: conf})
resp, err := c.do(http.MethodPost, "/auth", doOptions{data: conf})
if err != nil {
return authStatus, err
}
Expand All @@ -189,3 +284,102 @@ func (c *Client) AuthCheck(conf *AuthConfiguration) (AuthStatus, error) {
}
return authStatus, nil
}

// helperCredentials represents credentials commit from an helper
type helperCredentials struct {
Username string `json:"Username,omitempty"`
Secret string `json:"Secret,omitempty"`
}

// NewAuthConfigurationsFromCredsHelpers returns AuthConfigurations from
// installed credentials helpers
func NewAuthConfigurationsFromCredsHelpers(registry string) (*AuthConfiguration, error) {
// Load docker configuration file in order to find a possible helper provider
pathsToTry := cfgPaths(os.Getenv("DOCKER_CONFIG"), os.Getenv("HOME"))
if len(pathsToTry) < 1 {
return nil, errors.New("no docker configuration found")
}

provider, err := getHelperProviderFromDockerCfg(pathsToTry, registry)
if err != nil {
return nil, err
}

c, err := getCredentialsFromHelper(provider, registry)
if err != nil {
return nil, err
}

creds := new(AuthConfiguration)
creds.Username = c.Username
creds.Password = c.Secret
return creds, nil
}

func getHelperProviderFromDockerCfg(pathsToTry []string, registry string) (string, error) {
for _, path := range pathsToTry {
content, err := ioutil.ReadFile(path)
if err != nil {
// if we can't read the file keep going
continue
}

provider, err := parseCredsDockerConfig(content, registry)
if err != nil {
continue
}
if provider != "" {
return provider, nil
}
}
return "", errors.New("no docker credentials provider found")
}

func parseCredsDockerConfig(config []byte, registry string) (string, error) {
creds := struct {
CredsStore string `json:"credsStore,omitempty"`
CredHelpers map[string]string `json:"credHelpers,omitempty"`
}{}
err := json.Unmarshal(config, &creds)
if err != nil {
return "", err
}

provider, ok := creds.CredHelpers[registry]
if ok {
return provider, nil
}
return creds.CredsStore, nil
}

// Run and parse the found credential helper
func getCredentialsFromHelper(provider string, registry string) (*helperCredentials, error) {
helpercreds, err := runDockerCredentialsHelper(provider, registry)
if err != nil {
return nil, err
}

c := new(helperCredentials)
err = json.Unmarshal(helpercreds, c)
if err != nil {
return nil, err
}

return c, nil
}

func runDockerCredentialsHelper(provider string, registry string) ([]byte, error) {
cmd := exec.Command("docker-credential-"+provider, "get")

var stdout bytes.Buffer

cmd.Stdin = bytes.NewBuffer([]byte(registry))
cmd.Stdout = &stdout

err := cmd.Run()
if err != nil {
return nil, err
}

return stdout.Bytes(), nil
}