github.com/sijibomii/docker@v0.0.0-20231230191044-5cf6ca554647/cliconfig/credentials/native_store.go (about)

     1  package credentials
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  
    11  	"github.com/Sirupsen/logrus"
    12  	"github.com/docker/docker/cliconfig"
    13  	"github.com/docker/engine-api/types"
    14  )
    15  
    16  const (
    17  	remoteCredentialsPrefix = "docker-credential-"
    18  	tokenUsername           = "<token>"
    19  )
    20  
    21  // Standarize the not found error, so every helper returns
    22  // the same message and docker can handle it properly.
    23  var errCredentialsNotFound = errors.New("credentials not found in native keychain")
    24  
    25  // command is an interface that remote executed commands implement.
    26  type command interface {
    27  	Output() ([]byte, error)
    28  	Input(in io.Reader)
    29  }
    30  
    31  // credentialsRequest holds information shared between docker and a remote credential store.
    32  type credentialsRequest struct {
    33  	ServerURL string
    34  	Username  string
    35  	Secret    string
    36  }
    37  
    38  // credentialsGetResponse is the information serialized from a remote store
    39  // when the plugin sends requests to get the user credentials.
    40  type credentialsGetResponse struct {
    41  	Username string
    42  	Secret   string
    43  }
    44  
    45  // nativeStore implements a credentials store
    46  // using native keychain to keep credentials secure.
    47  // It piggybacks into a file store to keep users' emails.
    48  type nativeStore struct {
    49  	commandFn func(args ...string) command
    50  	fileStore Store
    51  }
    52  
    53  // NewNativeStore creates a new native store that
    54  // uses a remote helper program to manage credentials.
    55  func NewNativeStore(file *cliconfig.ConfigFile) Store {
    56  	return &nativeStore{
    57  		commandFn: shellCommandFn(file.CredentialsStore),
    58  		fileStore: NewFileStore(file),
    59  	}
    60  }
    61  
    62  // Erase removes the given credentials from the native store.
    63  func (c *nativeStore) Erase(serverAddress string) error {
    64  	if err := c.eraseCredentialsFromStore(serverAddress); err != nil {
    65  		return err
    66  	}
    67  
    68  	// Fallback to plain text store to remove email
    69  	return c.fileStore.Erase(serverAddress)
    70  }
    71  
    72  // Get retrieves credentials for a specific server from the native store.
    73  func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
    74  	// load user email if it exist or an empty auth config.
    75  	auth, _ := c.fileStore.Get(serverAddress)
    76  
    77  	creds, err := c.getCredentialsFromStore(serverAddress)
    78  	if err != nil {
    79  		return auth, err
    80  	}
    81  	auth.Username = creds.Username
    82  	auth.IdentityToken = creds.IdentityToken
    83  	auth.Password = creds.Password
    84  
    85  	return auth, nil
    86  }
    87  
    88  // GetAll retrieves all the credentials from the native store.
    89  func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
    90  	auths, _ := c.fileStore.GetAll()
    91  
    92  	for s, ac := range auths {
    93  		creds, _ := c.getCredentialsFromStore(s)
    94  		ac.Username = creds.Username
    95  		ac.Password = creds.Password
    96  		ac.IdentityToken = creds.IdentityToken
    97  		auths[s] = ac
    98  	}
    99  
   100  	return auths, nil
   101  }
   102  
   103  // Store saves the given credentials in the file store.
   104  func (c *nativeStore) Store(authConfig types.AuthConfig) error {
   105  	if err := c.storeCredentialsInStore(authConfig); err != nil {
   106  		return err
   107  	}
   108  	authConfig.Username = ""
   109  	authConfig.Password = ""
   110  	authConfig.IdentityToken = ""
   111  
   112  	// Fallback to old credential in plain text to save only the email
   113  	return c.fileStore.Store(authConfig)
   114  }
   115  
   116  // storeCredentialsInStore executes the command to store the credentials in the native store.
   117  func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
   118  	cmd := c.commandFn("store")
   119  	creds := &credentialsRequest{
   120  		ServerURL: config.ServerAddress,
   121  		Username:  config.Username,
   122  		Secret:    config.Password,
   123  	}
   124  
   125  	if config.IdentityToken != "" {
   126  		creds.Username = tokenUsername
   127  		creds.Secret = config.IdentityToken
   128  	}
   129  
   130  	buffer := new(bytes.Buffer)
   131  	if err := json.NewEncoder(buffer).Encode(creds); err != nil {
   132  		return err
   133  	}
   134  	cmd.Input(buffer)
   135  
   136  	out, err := cmd.Output()
   137  	if err != nil {
   138  		t := strings.TrimSpace(string(out))
   139  		logrus.Debugf("error adding credentials - err: %v, out: `%s`", err, t)
   140  		return fmt.Errorf(t)
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // getCredentialsFromStore executes the command to get the credentials from the native store.
   147  func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) {
   148  	var ret types.AuthConfig
   149  
   150  	cmd := c.commandFn("get")
   151  	cmd.Input(strings.NewReader(serverAddress))
   152  
   153  	out, err := cmd.Output()
   154  	if err != nil {
   155  		t := strings.TrimSpace(string(out))
   156  
   157  		// do not return an error if the credentials are not
   158  		// in the keyckain. Let docker ask for new credentials.
   159  		if t == errCredentialsNotFound.Error() {
   160  			return ret, nil
   161  		}
   162  
   163  		logrus.Debugf("error getting credentials - err: %v, out: `%s`", err, t)
   164  		return ret, fmt.Errorf(t)
   165  	}
   166  
   167  	var resp credentialsGetResponse
   168  	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
   169  		return ret, err
   170  	}
   171  
   172  	if resp.Username == tokenUsername {
   173  		ret.IdentityToken = resp.Secret
   174  	} else {
   175  		ret.Password = resp.Secret
   176  		ret.Username = resp.Username
   177  	}
   178  
   179  	ret.ServerAddress = serverAddress
   180  	return ret, nil
   181  }
   182  
   183  // eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
   184  func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
   185  	cmd := c.commandFn("erase")
   186  	cmd.Input(strings.NewReader(serverURL))
   187  
   188  	out, err := cmd.Output()
   189  	if err != nil {
   190  		t := strings.TrimSpace(string(out))
   191  		logrus.Debugf("error erasing credentials - err: %v, out: `%s`", err, t)
   192  		return fmt.Errorf(t)
   193  	}
   194  
   195  	return nil
   196  }