197 lines
5.2 KiB
Go
197 lines
5.2 KiB
Go
package credentials
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/cliconfig/configfile"
|
|
"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 *configfile.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
|
|
}
|