From 07c4b4124b46be30ea3ac7d114c44c4f911ca182 Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Thu, 18 Aug 2016 14:23:10 -0700 Subject: [PATCH] Add registry-specific credential helper support Signed-off-by: Jake Sanders --- cli/command/cli.go | 49 ++++++++++++-- cli/command/image/build.go | 4 +- cli/command/registry.go | 4 +- cli/command/registry/login.go | 2 +- cli/command/registry/logout.go | 2 +- cliconfig/config_test.go | 94 +++++++++++++++++++++++---- cliconfig/configfile/file.go | 4 +- cliconfig/credentials/native_store.go | 4 +- 8 files changed, 139 insertions(+), 24 deletions(-) diff --git a/cli/command/cli.go b/cli/command/cli.go index 99ea6331af..6d1dd7472e 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -10,6 +10,7 @@ import ( "runtime" "github.com/docker/docker/api" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" @@ -86,15 +87,55 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { return cli.configFile } +// GetAllCredentials returns all of the credentials stored in all of the +// configured credential stores. +func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) { + auths := make(map[string]types.AuthConfig) + for registry := range cli.configFile.CredentialHelpers { + helper := cli.CredentialsStore(registry) + newAuths, err := helper.GetAll() + if err != nil { + return nil, err + } + addAll(auths, newAuths) + } + defaultStore := cli.CredentialsStore("") + newAuths, err := defaultStore.GetAll() + if err != nil { + return nil, err + } + addAll(auths, newAuths) + return auths, nil +} + +func addAll(to, from map[string]types.AuthConfig) { + for reg, ac := range from { + to[reg] = ac + } +} + // CredentialsStore returns a new credentials store based -// on the settings provided in the configuration file. -func (cli *DockerCli) CredentialsStore() credentials.Store { - if cli.configFile.CredentialsStore != "" { - return credentials.NewNativeStore(cli.configFile) +// on the settings provided in the configuration file. Empty string returns +// the default credential store. +func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store { + if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" { + return credentials.NewNativeStore(cli.configFile, helper) } return credentials.NewFileStore(cli.configFile) } +// getConfiguredCredentialStore returns the credential helper configured for the +// given registry, the default credsStore, or the empty string if neither are +// configured. +func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string { + if c.CredentialHelpers != nil && serverAddress != "" { + if helper, exists := c.CredentialHelpers[serverAddress]; exists { + return helper + } + } + return c.CredentialsStore +} + // Initialize the dockerCli runs initialization that must happen after command // line flags are parsed. func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { diff --git a/cli/command/image/build.go b/cli/command/image/build.go index ebec87d641..78cc41494f 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -280,7 +280,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } } - authConfig, _ := dockerCli.CredentialsStore().GetAll() + authConfigs, _ := dockerCli.GetAllCredentials() buildOptions := types.ImageBuildOptions{ Memory: memory, MemorySwap: memorySwap, @@ -301,7 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { ShmSize: shmSize, Ulimits: options.ulimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), - AuthConfigs: authConfig, + AuthConfigs: authConfigs, Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, SecurityOpt: options.securityOpt, diff --git a/cli/command/registry.go b/cli/command/registry.go index b70d6f444c..65f6b3309e 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -67,7 +67,7 @@ func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes configKey = ElectAuthServer(ctx, cli) } - a, _ := cli.CredentialsStore().Get(configKey) + a, _ := cli.CredentialsStore(configKey).Get(configKey) return a } @@ -82,7 +82,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD serverAddress = registry.ConvertToHostname(serverAddress) } - authconfig, err := cli.CredentialsStore().Get(serverAddress) + authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) if err != nil { return authconfig, err } diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index f161f2d403..bdcc9a103b 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -69,7 +69,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { authConfig.Password = "" authConfig.IdentityToken = response.IdentityToken } - if err := dockerCli.CredentialsStore().Store(authConfig); err != nil { + if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { return fmt.Errorf("Error saving credentials: %v", err) } diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 8e820dcc8c..877e60e8cc 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -68,7 +68,7 @@ func runLogout(dockerCli *command.DockerCli, serverAddress string) error { fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) for _, r := range regsToLogout { - if err := dockerCli.CredentialsStore().Erase(r); err != nil { + if err := dockerCli.CredentialsStore(r).Erase(r); err != nil { fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) } } diff --git a/cliconfig/config_test.go b/cliconfig/config_test.go index 6eff26e481..d8a099ab58 100644 --- a/cliconfig/config_test.go +++ b/cliconfig/config_test.go @@ -86,7 +86,7 @@ func TestEmptyFile(t *testing.T) { } } -func TestEmptyJson(t *testing.T) { +func TestEmptyJSON(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") if err != nil { t.Fatal(err) @@ -193,7 +193,7 @@ func TestOldValidAuth(t *testing.T) { } } -func TestOldJsonInvalid(t *testing.T) { +func TestOldJSONInvalid(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") if err != nil { t.Fatal(err) @@ -219,7 +219,7 @@ func TestOldJsonInvalid(t *testing.T) { } } -func TestOldJson(t *testing.T) { +func TestOldJSON(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") if err != nil { t.Fatal(err) @@ -265,7 +265,7 @@ func TestOldJson(t *testing.T) { } } -func TestNewJson(t *testing.T) { +func TestNewJSON(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") if err != nil { t.Fatal(err) @@ -304,7 +304,7 @@ func TestNewJson(t *testing.T) { } } -func TestNewJsonNoEmail(t *testing.T) { +func TestNewJSONNoEmail(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") if err != nil { t.Fatal(err) @@ -343,7 +343,7 @@ func TestNewJsonNoEmail(t *testing.T) { } } -func TestJsonWithPsFormat(t *testing.T) { +func TestJSONWithPsFormat(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") if err != nil { t.Fatal(err) @@ -376,6 +376,78 @@ func TestJsonWithPsFormat(t *testing.T) { } } +func TestJSONWithCredentialStore(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "credsStore": "crazy-secure-storage" +}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.CredentialsStore != "crazy-secure-storage" { + t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + if !strings.Contains(configStr, `"credsStore":`) || + !strings.Contains(configStr, "crazy-secure-storage") { + t.Fatalf("Should have save in new form: %s", configStr) + } +} + +func TestJSONWithCredentialHelpers(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "credHelpers": { "images.io": "images-io", "containers.com": "crazy-secure-storage" } +}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.CredentialHelpers == nil { + t.Fatal("config.CredentialHelpers was nil") + } else if config.CredentialHelpers["images.io"] != "images-io" || + config.CredentialHelpers["containers.com"] != "crazy-secure-storage" { + t.Fatalf("Credential helpers not deserialized properly: %v\n", config.CredentialHelpers) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + if !strings.Contains(configStr, `"credHelpers":`) || + !strings.Contains(configStr, "images.io") || + !strings.Contains(configStr, "images-io") || + !strings.Contains(configStr, "containers.com") || + !strings.Contains(configStr, "crazy-secure-storage") { + t.Fatalf("Should have save in new form: %s", configStr) + } +} + // Save it and make sure it shows up in new form func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string { if err := config.Save(); err != nil { @@ -420,7 +492,7 @@ func TestConfigFile(t *testing.T) { } } -func TestJsonReaderNoFile(t *testing.T) { +func TestJSONReaderNoFile(t *testing.T) { js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }` config, err := LoadFromReader(strings.NewReader(js)) @@ -435,7 +507,7 @@ func TestJsonReaderNoFile(t *testing.T) { } -func TestOldJsonReaderNoFile(t *testing.T) { +func TestOldJSONReaderNoFile(t *testing.T) { js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` config, err := LegacyLoadFromReader(strings.NewReader(js)) @@ -449,7 +521,7 @@ func TestOldJsonReaderNoFile(t *testing.T) { } } -func TestJsonWithPsFormatNoFile(t *testing.T) { +func TestJSONWithPsFormatNoFile(t *testing.T) { js := `{ "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" @@ -465,7 +537,7 @@ func TestJsonWithPsFormatNoFile(t *testing.T) { } -func TestJsonSaveWithNoFile(t *testing.T) { +func TestJSONSaveWithNoFile(t *testing.T) { js := `{ "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } }, "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" @@ -507,7 +579,7 @@ func TestJsonSaveWithNoFile(t *testing.T) { } } -func TestLegacyJsonSaveWithNoFile(t *testing.T) { +func TestLegacyJSONSaveWithNoFile(t *testing.T) { js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` config, err := LegacyLoadFromReader(strings.NewReader(js)) diff --git a/cliconfig/configfile/file.go b/cliconfig/configfile/file.go index d7df229d29..39097133a4 100644 --- a/cliconfig/configfile/file.go +++ b/cliconfig/configfile/file.go @@ -31,6 +31,7 @@ type ConfigFile struct { 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"` } @@ -96,7 +97,8 @@ func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { // in this file or not. func (configFile *ConfigFile) ContainsAuth() bool { return configFile.CredentialsStore != "" || - (configFile.AuthConfigs != nil && len(configFile.AuthConfigs) > 0) + len(configFile.CredentialHelpers) > 0 || + len(configFile.AuthConfigs) > 0 } // SaveToWriter encodes and writes out all the authorization information to diff --git a/cliconfig/credentials/native_store.go b/cliconfig/credentials/native_store.go index 19a2beffc9..dec2dbcb82 100644 --- a/cliconfig/credentials/native_store.go +++ b/cliconfig/credentials/native_store.go @@ -22,8 +22,8 @@ type nativeStore struct { // NewNativeStore creates a new native store that // uses a remote helper program to manage credentials. -func NewNativeStore(file *configfile.ConfigFile) Store { - name := remoteCredentialsPrefix + file.CredentialsStore +func NewNativeStore(file *configfile.ConfigFile, helperSuffix string) Store { + name := remoteCredentialsPrefix + helperSuffix return &nativeStore{ programFunc: client.NewShellProgramFunc(name), fileStore: NewFileStore(file),