blob: 9b8997dd647768fe6a4c61b04963c46a05e28e54 [file] [log] [blame]
package credentials
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/cliconfig"
"github.com/docker/engine-api/types"
)
const (
remoteCredentialsPrefix = "docker-credential-"
tokenUsername = "<token>"
)
// Standarize the not found error, so every helper returns
// the same message and docker can handle it properly.
var errCredentialsNotFound = errors.New("credentials not found in native keychain")
// command is an interface that remote executed commands implement.
type command interface {
Output() ([]byte, error)
Input(in io.Reader)
}
// credentialsRequest holds information shared between docker and a remote credential store.
type credentialsRequest struct {
ServerURL string
Username string
Secret string
}
// credentialsGetResponse is the information serialized from a remote store
// when the plugin sends requests to get the user credentials.
type credentialsGetResponse struct {
Username string
Secret string
}
// nativeStore implements a credentials store
// using native keychain to keep credentials secure.
// It piggybacks into a file store to keep users' emails.
type nativeStore struct {
commandFn func(args ...string) command
fileStore Store
}
// NewNativeStore creates a new native store that
// uses a remote helper program to manage credentials.
func NewNativeStore(file *cliconfig.ConfigFile) Store {
return &nativeStore{
commandFn: shellCommandFn(file.CredentialsStore),
fileStore: NewFileStore(file),
}
}
// Erase removes the given credentials from the native store.
func (c *nativeStore) Erase(serverAddress string) error {
if err := c.eraseCredentialsFromStore(serverAddress); err != nil {
return err
}
// Fallback to plain text store to remove email
return c.fileStore.Erase(serverAddress)
}
// Get retrieves credentials for a specific server from the native store.
func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
// load user email if it exist or an empty auth config.
auth, _ := c.fileStore.Get(serverAddress)
creds, err := c.getCredentialsFromStore(serverAddress)
if err != nil {
return auth, err
}
auth.Username = creds.Username
auth.IdentityToken = creds.IdentityToken
auth.Password = creds.Password
return auth, nil
}
// GetAll retrieves all the credentials from the native store.
func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
auths, _ := c.fileStore.GetAll()
for s, ac := range auths {
creds, _ := c.getCredentialsFromStore(s)
ac.Username = creds.Username
ac.Password = creds.Password
ac.IdentityToken = creds.IdentityToken
auths[s] = ac
}
return auths, nil
}
// Store saves the given credentials in the file store.
func (c *nativeStore) Store(authConfig types.AuthConfig) error {
if err := c.storeCredentialsInStore(authConfig); err != nil {
return err
}
authConfig.Username = ""
authConfig.Password = ""
authConfig.IdentityToken = ""
// Fallback to old credential in plain text to save only the email
return c.fileStore.Store(authConfig)
}
// storeCredentialsInStore executes the command to store the credentials in the native store.
func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
cmd := c.commandFn("store")
creds := &credentialsRequest{
ServerURL: config.ServerAddress,
Username: config.Username,
Secret: config.Password,
}
if config.IdentityToken != "" {
creds.Username = tokenUsername
creds.Secret = config.IdentityToken
}
buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(creds); err != nil {
return err
}
cmd.Input(buffer)
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
logrus.Debugf("error adding credentials - err: %v, out: `%s`", err, t)
return fmt.Errorf(t)
}
return nil
}
// getCredentialsFromStore executes the command to get the credentials from the native store.
func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) {
var ret types.AuthConfig
cmd := c.commandFn("get")
cmd.Input(strings.NewReader(serverAddress))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
// do not return an error if the credentials are not
// in the keyckain. Let docker ask for new credentials.
if t == errCredentialsNotFound.Error() {
return ret, nil
}
logrus.Debugf("error getting credentials - err: %v, out: `%s`", err, t)
return ret, fmt.Errorf(t)
}
var resp credentialsGetResponse
if err := json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
return ret, err
}
if resp.Username == tokenUsername {
ret.IdentityToken = resp.Secret
} else {
ret.Password = resp.Secret
ret.Username = resp.Username
}
ret.ServerAddress = serverAddress
return ret, nil
}
// eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
cmd := c.commandFn("erase")
cmd.Input(strings.NewReader(serverURL))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
logrus.Debugf("error erasing credentials - err: %v, out: `%s`", err, t)
return fmt.Errorf(t)
}
return nil
}