Client credentials store.

This change implements communication with an external credentials store,
ala git-credential-helper. The client falls back the plain text store,
what we're currently using, if there is no remote store configured.

It shells out to helper program when a credential store is
configured. Those programs can be implemented with any language as long as they
follow the convention to pass arguments and information.

There is an implementation for the OS X keychain in https://github.com/calavera/docker-credential-helpers.
That package also provides basic structure to create other helpers.

Signed-off-by: David Calavera <david.calavera@gmail.com>
Upstream-commit: cf721c23e715e545eccf8484e145c2d18d6a6a23
Component: engine
This commit is contained in:
David Calavera
2016-02-29 13:01:31 -05:00
parent 1653c5add2
commit 8a7585ce8e
20 changed files with 888 additions and 51 deletions
+4
View File
@@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api"
"github.com/docker/docker/cli"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/cliconfig/credentials"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/term"
@@ -125,6 +126,9 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
if e != nil {
fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
}
if !configFile.ContainsAuth() {
credentials.DetectDefaultStore(configFile)
}
cli.configFile = configFile
host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions)
+1 -1
View File
@@ -42,7 +42,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
return err
}
authConfig := cli.resolveAuthConfig(cli.configFile.AuthConfigs, repoInfo.Index)
authConfig := cli.resolveAuthConfig(repoInfo.Index)
encodedAuth, err := encodeAuthToBase64(authConfig)
if err != nil {
return err
+42 -10
View File
@@ -9,6 +9,8 @@ import (
"strings"
Cli "github.com/docker/docker/cli"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/cliconfig/credentials"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/term"
"github.com/docker/engine-api/client"
@@ -50,18 +52,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
response, err := cli.client.RegistryLogin(authConfig)
if err != nil {
if client.IsErrUnauthorized(err) {
delete(cli.configFile.AuthConfigs, serverAddress)
if err2 := cli.configFile.Save(); err2 != nil {
fmt.Fprintf(cli.out, "WARNING: could not save config file: %v\n", err2)
if err2 := eraseCredentials(cli.configFile, authConfig.ServerAddress); err2 != nil {
fmt.Fprintf(cli.out, "WARNING: could not save credentials: %v\n", err2)
}
}
return err
}
if err := cli.configFile.Save(); err != nil {
return fmt.Errorf("Error saving config file: %v", err)
if err := storeCredentials(cli.configFile, authConfig); err != nil {
return fmt.Errorf("Error saving credentials: %v", err)
}
fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s\n", cli.configFile.Filename())
if response.Status != "" {
fmt.Fprintf(cli.out, "%s\n", response.Status)
@@ -78,10 +78,11 @@ func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) {
}
func (cli *DockerCli) configureAuth(flUser, flPassword, flEmail, serverAddress string) (types.AuthConfig, error) {
authconfig, ok := cli.configFile.AuthConfigs[serverAddress]
if !ok {
authconfig = types.AuthConfig{}
authconfig, err := getCredentials(cli.configFile, serverAddress)
if err != nil {
return authconfig, err
}
authconfig.Username = strings.TrimSpace(authconfig.Username)
if flUser = strings.TrimSpace(flUser); flUser == "" {
@@ -133,11 +134,12 @@ func (cli *DockerCli) configureAuth(flUser, flPassword, flEmail, serverAddress s
flEmail = authconfig.Email
}
}
authconfig.Username = flUser
authconfig.Password = flPassword
authconfig.Email = flEmail
authconfig.ServerAddress = serverAddress
cli.configFile.AuthConfigs[serverAddress] = authconfig
return authconfig, nil
}
@@ -150,3 +152,33 @@ func readInput(in io.Reader, out io.Writer) string {
}
return string(line)
}
// getCredentials loads the user credentials from a credentials store.
// The store is determined by the config file settings.
func getCredentials(c *cliconfig.ConfigFile, serverAddress string) (types.AuthConfig, error) {
s := loadCredentialsStore(c)
return s.Get(serverAddress)
}
// storeCredentials saves the user credentials in a credentials store.
// The store is determined by the config file settings.
func storeCredentials(c *cliconfig.ConfigFile, auth types.AuthConfig) error {
s := loadCredentialsStore(c)
return s.Store(auth)
}
// eraseCredentials removes the user credentials from a credentials store.
// The store is determined by the config file settings.
func eraseCredentials(c *cliconfig.ConfigFile, serverAddress string) error {
s := loadCredentialsStore(c)
return s.Erase(serverAddress)
}
// loadCredentialsStore initializes a new credentials store based
// in the settings provided in the configuration file.
func loadCredentialsStore(c *cliconfig.ConfigFile) credentials.Store {
if c.CredentialsStore != "" {
return credentials.NewNativeStore(c)
}
return credentials.NewFileStore(c)
}
+1 -1
View File
@@ -56,7 +56,7 @@ func (cli *DockerCli) CmdPull(args ...string) error {
return err
}
authConfig := cli.resolveAuthConfig(cli.configFile.AuthConfigs, repoInfo.Index)
authConfig := cli.resolveAuthConfig(repoInfo.Index)
requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "pull")
if isTrusted() && !ref.HasDigest() {
+1 -1
View File
@@ -44,7 +44,7 @@ func (cli *DockerCli) CmdPush(args ...string) error {
return err
}
// Resolve the Auth config relevant for this server
authConfig := cli.resolveAuthConfig(cli.configFile.AuthConfigs, repoInfo.Index)
authConfig := cli.resolveAuthConfig(repoInfo.Index)
requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "push")
if isTrusted() {
+1 -1
View File
@@ -36,7 +36,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
return err
}
authConfig := cli.resolveAuthConfig(cli.configFile.AuthConfigs, indexInfo)
authConfig := cli.resolveAuthConfig(indexInfo)
requestPrivilege := cli.registryAuthenticationPrivilegedFunc(indexInfo, "search")
encodedAuth, err := encodeAuthToBase64(authConfig)
+1 -1
View File
@@ -235,7 +235,7 @@ func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Can
}
// Resolve the Auth config relevant for this server
authConfig := cli.resolveAuthConfig(cli.configFile.AuthConfigs, repoInfo.Index)
authConfig := cli.resolveAuthConfig(repoInfo.Index)
notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
if err != nil {
+3 -30
View File
@@ -10,7 +10,6 @@ import (
gosignal "os/signal"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/Sirupsen/logrus"
@@ -185,38 +184,12 @@ func copyToFile(outfile string, r io.Reader) error {
// resolveAuthConfig is like registry.ResolveAuthConfig, but if using the
// default index, it uses the default index name for the daemon's platform,
// not the client's platform.
func (cli *DockerCli) resolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
func (cli *DockerCli) resolveAuthConfig(index *registrytypes.IndexInfo) types.AuthConfig {
configKey := index.Name
if index.Official {
configKey = cli.electAuthServer()
}
// First try the happy case
if c, found := authConfigs[configKey]; found || index.Official {
return c
}
convertToHostname := func(url string) string {
stripped := url
if strings.HasPrefix(url, "http://") {
stripped = strings.Replace(url, "http://", "", 1)
} else if strings.HasPrefix(url, "https://") {
stripped = strings.Replace(url, "https://", "", 1)
}
nameParts := strings.SplitN(stripped, "/", 2)
return nameParts[0]
}
// Maybe they have a legacy config file, we will iterate the keys converting
// them to the new format and testing
for registry, ac := range authConfigs {
if configKey == convertToHostname(registry) {
return ac
}
}
// When all else fails, return an empty auth config
return types.AuthConfig{}
a, _ := getCredentials(cli.configFile, configKey)
return a
}