mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Use docker-credential-helpers client to talk with native creds stores.
Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
parent
feab8db60d
commit
ff3e187cc7
3 changed files with 51 additions and 147 deletions
|
@ -1,14 +1,8 @@
|
||||||
package credentials
|
package credentials
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"github.com/docker/docker-credential-helpers/client"
|
||||||
"encoding/json"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/cliconfig/configfile"
|
"github.com/docker/docker/cliconfig/configfile"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
)
|
)
|
||||||
|
@ -18,50 +12,27 @@ const (
|
||||||
tokenUsername = "<token>"
|
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
|
// nativeStore implements a credentials store
|
||||||
// using native keychain to keep credentials secure.
|
// using native keychain to keep credentials secure.
|
||||||
// It piggybacks into a file store to keep users' emails.
|
// It piggybacks into a file store to keep users' emails.
|
||||||
type nativeStore struct {
|
type nativeStore struct {
|
||||||
commandFn func(args ...string) command
|
programFunc client.ProgramFunc
|
||||||
fileStore Store
|
fileStore Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNativeStore creates a new native store that
|
// NewNativeStore creates a new native store that
|
||||||
// uses a remote helper program to manage credentials.
|
// uses a remote helper program to manage credentials.
|
||||||
func NewNativeStore(file *configfile.ConfigFile) Store {
|
func NewNativeStore(file *configfile.ConfigFile) Store {
|
||||||
|
name := remoteCredentialsPrefix + file.CredentialsStore
|
||||||
return &nativeStore{
|
return &nativeStore{
|
||||||
commandFn: shellCommandFn(file.CredentialsStore),
|
programFunc: client.NewShellProgramFunc(name),
|
||||||
fileStore: NewFileStore(file),
|
fileStore: NewFileStore(file),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase removes the given credentials from the native store.
|
// Erase removes the given credentials from the native store.
|
||||||
func (c *nativeStore) Erase(serverAddress string) error {
|
func (c *nativeStore) Erase(serverAddress string) error {
|
||||||
if err := c.eraseCredentialsFromStore(serverAddress); err != nil {
|
if err := client.Erase(c.programFunc, serverAddress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,8 +86,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
|
||||||
|
|
||||||
// storeCredentialsInStore executes the command to store the credentials in the native store.
|
// storeCredentialsInStore executes the command to store the credentials in the native store.
|
||||||
func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
|
func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
|
||||||
cmd := c.commandFn("store")
|
creds := &credentials.Credentials{
|
||||||
creds := &credentialsRequest{
|
|
||||||
ServerURL: config.ServerAddress,
|
ServerURL: config.ServerAddress,
|
||||||
Username: config.Username,
|
Username: config.Username,
|
||||||
Secret: config.Password,
|
Secret: config.Password,
|
||||||
|
@ -127,70 +97,30 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
|
||||||
creds.Secret = config.IdentityToken
|
creds.Secret = config.IdentityToken
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
return client.Store(c.programFunc, creds)
|
||||||
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.
|
// getCredentialsFromStore executes the command to get the credentials from the native store.
|
||||||
func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) {
|
func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) {
|
||||||
var ret types.AuthConfig
|
var ret types.AuthConfig
|
||||||
|
|
||||||
cmd := c.commandFn("get")
|
creds, err := client.Get(c.programFunc, serverAddress)
|
||||||
cmd.Input(strings.NewReader(serverAddress))
|
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t := strings.TrimSpace(string(out))
|
if credentials.IsErrCredentialsNotFound(err) {
|
||||||
|
|
||||||
// do not return an error if the credentials are not
|
// do not return an error if the credentials are not
|
||||||
// in the keyckain. Let docker ask for new credentials.
|
// in the keyckain. Let docker ask for new credentials.
|
||||||
if t == errCredentialsNotFound.Error() {
|
|
||||||
return ret, nil
|
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
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Username == tokenUsername {
|
if creds.Username == tokenUsername {
|
||||||
ret.IdentityToken = resp.Secret
|
ret.IdentityToken = creds.Secret
|
||||||
} else {
|
} else {
|
||||||
ret.Password = resp.Secret
|
ret.Password = creds.Secret
|
||||||
ret.Username = resp.Username
|
ret.Username = creds.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.ServerAddress = serverAddress
|
ret.ServerAddress = serverAddress
|
||||||
return ret, nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/client"
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +45,7 @@ func (m *mockCommand) Output() ([]byte, error) {
|
||||||
case validServerAddress:
|
case validServerAddress:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
default:
|
default:
|
||||||
return []byte("error erasing credentials"), errCommandExited
|
return []byte("program failed"), errCommandExited
|
||||||
}
|
}
|
||||||
case "get":
|
case "get":
|
||||||
switch inS {
|
switch inS {
|
||||||
|
@ -52,21 +54,21 @@ func (m *mockCommand) Output() ([]byte, error) {
|
||||||
case validServerAddress2:
|
case validServerAddress2:
|
||||||
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
||||||
case missingCredsAddress:
|
case missingCredsAddress:
|
||||||
return []byte(errCredentialsNotFound.Error()), errCommandExited
|
return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited
|
||||||
case invalidServerAddress:
|
case invalidServerAddress:
|
||||||
return []byte("error getting credentials"), errCommandExited
|
return []byte("program failed"), errCommandExited
|
||||||
}
|
}
|
||||||
case "store":
|
case "store":
|
||||||
var c credentialsRequest
|
var c credentials.Credentials
|
||||||
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte("error storing credentials"), errCommandExited
|
return []byte("program failed"), errCommandExited
|
||||||
}
|
}
|
||||||
switch c.ServerURL {
|
switch c.ServerURL {
|
||||||
case validServerAddress:
|
case validServerAddress:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
default:
|
default:
|
||||||
return []byte("error storing credentials"), errCommandExited
|
return []byte("program failed"), errCommandExited
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +80,7 @@ func (m *mockCommand) Input(in io.Reader) {
|
||||||
m.input = in
|
m.input = in
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockCommandFn(args ...string) command {
|
func mockCommandFn(args ...string) client.Program {
|
||||||
return &mockCommand{
|
return &mockCommand{
|
||||||
arg: args[0],
|
arg: args[0],
|
||||||
}
|
}
|
||||||
|
@ -89,7 +91,7 @@ func TestNativeStoreAddCredentials(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
err := s.Store(types.AuthConfig{
|
err := s.Store(types.AuthConfig{
|
||||||
|
@ -133,7 +135,7 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
err := s.Store(types.AuthConfig{
|
err := s.Store(types.AuthConfig{
|
||||||
|
@ -147,8 +149,8 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.Error() != "error storing credentials" {
|
if !strings.Contains(err.Error(), "program failed") {
|
||||||
t.Fatalf("expected `error storing credentials`, got %v", err)
|
t.Fatalf("expected `program failed`, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(f.AuthConfigs) != 0 {
|
if len(f.AuthConfigs) != 0 {
|
||||||
|
@ -165,7 +167,7 @@ func TestNativeStoreGet(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
a, err := s.Get(validServerAddress)
|
a, err := s.Get(validServerAddress)
|
||||||
|
@ -196,7 +198,7 @@ func TestNativeStoreGetIdentityToken(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
a, err := s.Get(validServerAddress2)
|
a, err := s.Get(validServerAddress2)
|
||||||
|
@ -230,7 +232,7 @@ func TestNativeStoreGetAll(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
as, err := s.GetAll()
|
as, err := s.GetAll()
|
||||||
|
@ -277,7 +279,7 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
_, err := s.Get(missingCredsAddress)
|
_, err := s.Get(missingCredsAddress)
|
||||||
|
@ -296,7 +298,7 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
_, err := s.Get(invalidServerAddress)
|
_, err := s.Get(invalidServerAddress)
|
||||||
|
@ -304,8 +306,8 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.Error() != "error getting credentials" {
|
if !strings.Contains(err.Error(), "program failed") {
|
||||||
t.Fatalf("expected `error getting credentials`, got %v", err)
|
t.Fatalf("expected `program failed`, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +320,7 @@ func TestNativeStoreErase(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
err := s.Erase(validServerAddress)
|
err := s.Erase(validServerAddress)
|
||||||
|
@ -340,7 +342,7 @@ func TestNativeStoreEraseInvalidAddress(t *testing.T) {
|
||||||
f.CredentialsStore = "mock"
|
f.CredentialsStore = "mock"
|
||||||
|
|
||||||
s := &nativeStore{
|
s := &nativeStore{
|
||||||
commandFn: mockCommandFn,
|
programFunc: mockCommandFn,
|
||||||
fileStore: NewFileStore(f),
|
fileStore: NewFileStore(f),
|
||||||
}
|
}
|
||||||
err := s.Erase(invalidServerAddress)
|
err := s.Erase(invalidServerAddress)
|
||||||
|
@ -348,7 +350,7 @@ func TestNativeStoreEraseInvalidAddress(t *testing.T) {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.Error() != "error erasing credentials" {
|
if !strings.Contains(err.Error(), "program failed") {
|
||||||
t.Fatalf("expected `error erasing credentials`, got %v", err)
|
t.Fatalf("expected `program failed`, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package credentials
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func shellCommandFn(storeName string) func(args ...string) command {
|
|
||||||
name := remoteCredentialsPrefix + storeName
|
|
||||||
return func(args ...string) command {
|
|
||||||
return &shell{cmd: exec.Command(name, args...)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shell invokes shell commands to talk with a remote credentials helper.
|
|
||||||
type shell struct {
|
|
||||||
cmd *exec.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output returns responses from the remote credentials helper.
|
|
||||||
func (s *shell) Output() ([]byte, error) {
|
|
||||||
return s.cmd.Output()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input sets the input to send to a remote credentials helper.
|
|
||||||
func (s *shell) Input(in io.Reader) {
|
|
||||||
s.cmd.Stdin = in
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue