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 #520

Merged
merged 1 commit into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 188 additions & 8 deletions docker/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
)
Expand All @@ -29,7 +30,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 @@ -38,16 +54,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 @@ -63,32 +109,59 @@ func NewAuthConfigurationsFromFile(path string) (*AuthConfigurations, error) {
func cfgPaths(dockerConfigEnv string, homeEnv string) []string {
var paths []string
if dockerConfigEnv != "" {
paths = append(paths, path.Join(dockerConfigEnv, "plaintext-passwords.json"))
paths = append(paths, path.Join(dockerConfigEnv, "config.json"))
}
if homeEnv != "" {
paths = append(paths, path.Join(homeEnv, ".docker", "plaintext-passwords.json"))
paths = append(paths, path.Join(homeEnv, ".docker", "config.json"))
paths = append(paths, path.Join(homeEnv, ".dockercfg"))
}
return paths
}

// 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 err error
var auths *AuthConfigurations
var result *AuthConfigurations

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

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)
}
}
return auths, err

if result != nil {
return result, nil

}
return nil, err
}

// NewAuthConfigurations returns AuthConfigurations from a JSON encoded string in the
Expand Down Expand Up @@ -138,8 +211,16 @@ func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) {
c.Configs[reg] = AuthConfiguration{
IdentityToken: conf.IdentityToken,
}
case conf.RegistryToken != "":
c.Configs[reg] = AuthConfiguration{
RegistryToken: conf.RegistryToken,
}
case conf.Auth != "":
// 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, err
}
Expand Down Expand Up @@ -191,3 +272,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
}
Loading
Loading