forked from runabol/tork
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
625 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
package docker // import "https://github.com/cpuguy83/dockercfg" | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
|
||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
// Errors from credential helpers | ||
var ( | ||
errCredentialsNotFound = errors.New("credentials not found in native keychain") | ||
errCredentialsMissingServerURL = errors.New("no credentials server URL") | ||
) | ||
|
||
// This is used by the docker CLI in casses where an oauth identity token is used. | ||
// In that case the username is stored litterally as `<token>` | ||
// When fetching the credentials we check for this value to determine if | ||
const tokenUsername = "<token>" | ||
|
||
// GetRegistryCredentials gets registry credentials for the passed in registry host. | ||
// | ||
// This will use `LoadDefaultConfig` to read registry auth details from the config. | ||
// If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform. | ||
func getRegistryCredentials(configFile string, hostname string) (string, string, error) { | ||
cfg, err := loadConfig(configFile) | ||
if err != nil { | ||
if !os.IsNotExist(err) { | ||
return "", "", err | ||
} | ||
return getCredentialsFromHelper("", hostname) | ||
} | ||
return cfg.getRegistryCredentials(hostname) | ||
} | ||
|
||
// GetRegistryCredentials gets credentials, if any, for the provided hostname | ||
// | ||
// Hostnames should already be resolved using `ResolveRegistryAuth` | ||
// | ||
// If the returned username string is empty, the password is an identity token. | ||
func (c *config) getRegistryCredentials(hostname string) (string, string, error) { | ||
h, ok := c.CredentialHelpers[hostname] | ||
if ok { | ||
return getCredentialsFromHelper(h, hostname) | ||
} | ||
|
||
if c.CredentialsStore != "" { | ||
return getCredentialsFromHelper(c.CredentialsStore, hostname) | ||
} | ||
|
||
auth, ok := c.AuthConfigs[hostname] | ||
if !ok { | ||
return getCredentialsFromHelper("", hostname) | ||
} | ||
|
||
if auth.IdentityToken != "" { | ||
return "", auth.IdentityToken, nil | ||
} | ||
|
||
if auth.Username != "" && auth.Password != "" { | ||
return auth.Username, auth.Password, nil | ||
} | ||
|
||
return decodeBase64Auth(auth) | ||
} | ||
|
||
// DecodeBase64Auth decodes the legacy file-based auth storage from the docker CLI. | ||
// It takes the "Auth" filed from AuthConfig and decodes that into a username and password. | ||
// | ||
// If "Auth" is empty, an empty user/pass will be returned, but not an error. | ||
func decodeBase64Auth(auth authConfig) (string, string, error) { | ||
if auth.Auth == "" { | ||
return "", "", nil | ||
} | ||
|
||
decLen := base64.StdEncoding.DecodedLen(len(auth.Auth)) | ||
decoded := make([]byte, decLen) | ||
n, err := base64.StdEncoding.Decode(decoded, []byte(auth.Auth)) | ||
if err != nil { | ||
return "", "", fmt.Errorf("error decoding auth from file: %w", err) | ||
} | ||
|
||
if n > decLen { | ||
return "", "", fmt.Errorf("decoded value is longer than expected length, expected: %d, actual: %d", decLen, n) | ||
} | ||
|
||
split := strings.SplitN(string(decoded), ":", 2) | ||
if len(split) != 2 { | ||
return "", "", errors.New("invalid auth string") | ||
} | ||
|
||
return split[0], strings.Trim(split[1], "\x00"), nil | ||
} | ||
|
||
// GetCredentialsFromHelper attempts to lookup credentials from the passed in docker credential helper. | ||
// | ||
// The credential helpoer should just be the suffix name (no "docker-credential-"). | ||
// If the passed in helper program is empty this will look up the default helper for the platform. | ||
// | ||
// If the credentials are not found, no error is returned, only empty credentials. | ||
// | ||
// Hostnames should already be resolved using `ResolveRegistryAuth` | ||
// | ||
// If the username string is empty, the password string is an identity token. | ||
func getCredentialsFromHelper(helper, hostname string) (string, string, error) { | ||
if helper == "" { | ||
helper = getCredentialHelper() | ||
} | ||
if helper == "" { | ||
return "", "", nil | ||
} | ||
|
||
p, err := exec.LookPath("docker-credential-" + helper) | ||
if err != nil { | ||
return "", "", nil | ||
} | ||
|
||
cmd := exec.Command(p, "get") | ||
cmd.Stdin = strings.NewReader(hostname) | ||
|
||
b, err := cmd.Output() | ||
if err != nil { | ||
s := strings.TrimSpace(string(b)) | ||
|
||
switch s { | ||
case errCredentialsNotFound.Error(): | ||
return "", "", nil | ||
case errCredentialsMissingServerURL.Error(): | ||
return "", "", errors.New(s) | ||
default: | ||
} | ||
|
||
return "", "", err | ||
} | ||
|
||
var creds struct { | ||
Username string | ||
Secret string | ||
} | ||
|
||
if err := json.Unmarshal(b, &creds); err != nil { | ||
return "", "", err | ||
} | ||
|
||
// When tokenUsername is used, the output is an identity token and the username is garbage | ||
if creds.Username == tokenUsername { | ||
creds.Username = "" | ||
} | ||
|
||
return creds.Username, creds.Secret, nil | ||
} | ||
|
||
// getCredentialHelper gets the default credential helper name for the current platform. | ||
func getCredentialHelper() string { | ||
switch runtime.GOOS { | ||
case "linux": | ||
if _, err := exec.LookPath("pass"); err == nil { | ||
return "pass" | ||
} | ||
return "secretservice" | ||
case "darwin": | ||
return "osxkeychain" | ||
case "windows": | ||
return "wincred" | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
// UserHomeConfigPath returns the path to the docker config in the current user's home dir. | ||
func userHomeConfigPath() (string, error) { | ||
home, err := os.UserHomeDir() | ||
if err != nil { | ||
return "", fmt.Errorf("error looking up user home dir: %w", err) | ||
} | ||
|
||
return filepath.Join(home, ".docker", "config.json"), nil | ||
} | ||
|
||
// ConfigPath returns the path to the docker cli config. | ||
// | ||
// It will either use the DOCKER_CONFIG env var if set, or the value from `UserHomeConfigPath` | ||
// DOCKER_CONFIG would be the dir path where `config.json` is stored, this returns the path to config.json. | ||
func configPath(configFile string) (string, error) { | ||
if configFile != "" { | ||
return configFile, nil | ||
} | ||
return userHomeConfigPath() | ||
} | ||
|
||
// LoadDefaultConfig loads the docker cli config from the path returned from `ConfigPath` | ||
func loadConfig(configFile string) (config, error) { | ||
var cfg config | ||
p, err := configPath(configFile) | ||
if err != nil { | ||
return cfg, err | ||
} | ||
return cfg, fromFile(p, &cfg) | ||
} | ||
|
||
// FromFile loads config from the specified path into cfg | ||
func fromFile(configPath string, cfg *config) error { | ||
log.Debug().Msgf("Loading docker config: %s", configPath) | ||
f, err := os.Open(configPath) | ||
if err != nil { | ||
return err | ||
} | ||
if err := json.NewDecoder(f).Decode(&cfg); err != nil { | ||
return errors.Wrapf(err, "error decoding docker config") | ||
} | ||
if err := f.Close(); err != nil { | ||
return errors.Wrapf(err, "error closing docker config file") | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package docker | ||
|
||
import ( | ||
"encoding/base64" | ||
"testing" | ||
) | ||
|
||
func TestDecodeBase64Auth(t *testing.T) { | ||
for _, tc := range base64TestCases() { | ||
t.Run(tc.name, testBase64Case(tc, func() (string, string, error) { | ||
return decodeBase64Auth(tc.config) | ||
})) | ||
} | ||
} | ||
|
||
func TestGetRegistryCredentials(t *testing.T) { | ||
t.Run("from base64 auth", func(t *testing.T) { | ||
for _, tc := range base64TestCases() { | ||
t.Run(tc.name, func(T *testing.T) { | ||
config := config{ | ||
AuthConfigs: map[string]authConfig{ | ||
"some.domain": tc.config, | ||
}, | ||
} | ||
testBase64Case(tc, func() (string, string, error) { | ||
return config.getRegistryCredentials("some.domain") | ||
}) | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
type base64TestCase struct { | ||
name string | ||
config authConfig | ||
expUser string | ||
expPass string | ||
expErr bool | ||
} | ||
|
||
func base64TestCases() []base64TestCase { | ||
cases := []base64TestCase{ | ||
{name: "empty"}, | ||
{name: "not base64", expErr: true, config: authConfig{Auth: "not base64"}}, | ||
{name: "invalid format", expErr: true, config: authConfig{ | ||
Auth: base64.StdEncoding.EncodeToString([]byte("invalid format")), | ||
}}, | ||
{name: "happy case", expUser: "user", expPass: "pass", config: authConfig{ | ||
Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), | ||
}}, | ||
} | ||
|
||
return cases | ||
} | ||
|
||
type testAuthFn func() (string, string, error) | ||
|
||
func testBase64Case(tc base64TestCase, authFn testAuthFn) func(t *testing.T) { | ||
return func(t *testing.T) { | ||
u, p, err := authFn() | ||
if tc.expErr && err == nil { | ||
t.Fatal("expected error") | ||
} | ||
|
||
if u != tc.expUser || p != tc.expPass { | ||
t.Errorf("decoded username and password do not match, expected user: %s, password: %s, got user: %s, password: %s", tc.expUser, tc.expPass, u, p) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package docker // import "https://github.com/cpuguy83/dockercfg" | ||
|
||
// Config represents the on disk format of the docker CLI's config file. | ||
type config struct { | ||
AuthConfigs map[string]authConfig `json:"auths"` | ||
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` | ||
PsFormat string `json:"psFormat,omitempty"` | ||
ImagesFormat string `json:"imagesFormat,omitempty"` | ||
NetworksFormat string `json:"networksFormat,omitempty"` | ||
PluginsFormat string `json:"pluginsFormat,omitempty"` | ||
VolumesFormat string `json:"volumesFormat,omitempty"` | ||
StatsFormat string `json:"statsFormat,omitempty"` | ||
DetachKeys string `json:"detachKeys,omitempty"` | ||
CredentialsStore string `json:"credsStore,omitempty"` | ||
CredentialHelpers map[string]string `json:"credHelpers,omitempty"` | ||
Filename string `json:"-"` // Note: for internal use only | ||
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` | ||
ServicesFormat string `json:"servicesFormat,omitempty"` | ||
TasksFormat string `json:"tasksFormat,omitempty"` | ||
SecretFormat string `json:"secretFormat,omitempty"` | ||
ConfigFormat string `json:"configFormat,omitempty"` | ||
NodesFormat string `json:"nodesFormat,omitempty"` | ||
PruneFilters []string `json:"pruneFilters,omitempty"` | ||
Proxies map[string]proxyConfig `json:"proxies,omitempty"` | ||
Experimental string `json:"experimental,omitempty"` | ||
StackOrchestrator string `json:"stackOrchestrator,omitempty"` | ||
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"` | ||
CurrentContext string `json:"currentContext,omitempty"` | ||
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"` | ||
Aliases map[string]string `json:"aliases,omitempty"` | ||
} | ||
|
||
// ProxyConfig contains proxy configuration settings | ||
type proxyConfig struct { | ||
HTTPProxy string `json:"httpProxy,omitempty"` | ||
HTTPSProxy string `json:"httpsProxy,omitempty"` | ||
NoProxy string `json:"noProxy,omitempty"` | ||
FTPProxy string `json:"ftpProxy,omitempty"` | ||
} | ||
|
||
// AuthConfig contains authorization information for connecting to a Registry | ||
type authConfig struct { | ||
Username string `json:"username,omitempty"` | ||
Password string `json:"password,omitempty"` | ||
Auth string `json:"auth,omitempty"` | ||
|
||
// Email is an optional value associated with the username. | ||
// This field is deprecated and will be removed in a later | ||
// version of docker. | ||
Email string `json:"email,omitempty"` | ||
|
||
ServerAddress string `json:"serveraddress,omitempty"` | ||
|
||
// IdentityToken is used to authenticate the user and get | ||
// an access token for the registry. | ||
IdentityToken string `json:"identitytoken,omitempty"` | ||
|
||
// RegistryToken is a bearer token to be sent to a registry | ||
RegistryToken string `json:"registrytoken,omitempty"` | ||
} | ||
|
||
// KubernetesConfig contains Kubernetes orchestrator settings | ||
type KubernetesConfig struct { | ||
AllNamespaces string `json:"allNamespaces,omitempty"` | ||
} |
Oops, something went wrong.