oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/credentials/store.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  // Package credentials supports reading, saving, and removing credentials from
    17  // Docker configuration files and external credential stores that follow
    18  // the Docker credential helper protocol.
    19  //
    20  // Reference: https://docs.docker.com/engine/reference/commandline/login/#credential-stores
    21  package credentials
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"os"
    27  	"path/filepath"
    28  
    29  	"oras.land/oras-go/v2/internal/syncutil"
    30  	"oras.land/oras-go/v2/registry/remote/auth"
    31  	"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
    32  )
    33  
    34  const (
    35  	dockerConfigDirEnv   = "DOCKER_CONFIG"
    36  	dockerConfigFileDir  = ".docker"
    37  	dockerConfigFileName = "config.json"
    38  )
    39  
    40  // Store is the interface that any credentials store must implement.
    41  type Store interface {
    42  	// Get retrieves credentials from the store for the given server address.
    43  	Get(ctx context.Context, serverAddress string) (auth.Credential, error)
    44  	// Put saves credentials into the store for the given server address.
    45  	Put(ctx context.Context, serverAddress string, cred auth.Credential) error
    46  	// Delete removes credentials from the store for the given server address.
    47  	Delete(ctx context.Context, serverAddress string) error
    48  }
    49  
    50  // DynamicStore dynamically determines which store to use based on the settings
    51  // in the config file.
    52  type DynamicStore struct {
    53  	config             *config.Config
    54  	options            StoreOptions
    55  	detectedCredsStore string
    56  	setCredsStoreOnce  syncutil.OnceOrRetry
    57  }
    58  
    59  // StoreOptions provides options for NewStore.
    60  type StoreOptions struct {
    61  	// AllowPlaintextPut allows saving credentials in plaintext in the config
    62  	// file.
    63  	//   - If AllowPlaintextPut is set to false (default value), Put() will
    64  	//     return an error when native store is not available.
    65  	//   - If AllowPlaintextPut is set to true, Put() will save credentials in
    66  	//     plaintext in the config file when native store is not available.
    67  	AllowPlaintextPut bool
    68  
    69  	// DetectDefaultNativeStore enables detecting the platform-default native
    70  	// credentials store when the config file has no authentication information.
    71  	//
    72  	// If DetectDefaultNativeStore is set to true, the store will detect and set
    73  	// the default native credentials store in the "credsStore" field of the
    74  	// config file.
    75  	//   - Windows: "wincred"
    76  	//   - Linux: "pass" or "secretservice"
    77  	//   - macOS: "osxkeychain"
    78  	//
    79  	// References:
    80  	//   - https://docs.docker.com/engine/reference/commandline/login/#credentials-store
    81  	//   - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
    82  	DetectDefaultNativeStore bool
    83  }
    84  
    85  // NewStore returns a Store based on the given configuration file.
    86  //
    87  // For Get(), Put() and Delete(), the returned Store will dynamically determine
    88  // which underlying credentials store to use for the given server address.
    89  // The underlying credentials store is determined in the following order:
    90  //  1. Native server-specific credential helper
    91  //  2. Native credentials store
    92  //  3. The plain-text config file itself
    93  //
    94  // References:
    95  //   - https://docs.docker.com/engine/reference/commandline/login/#credentials-store
    96  //   - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
    97  func NewStore(configPath string, opts StoreOptions) (*DynamicStore, error) {
    98  	cfg, err := config.Load(configPath)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	ds := &DynamicStore{
   103  		config:  cfg,
   104  		options: opts,
   105  	}
   106  	if opts.DetectDefaultNativeStore && !cfg.IsAuthConfigured() {
   107  		// no authentication configured, detect the default credentials store
   108  		ds.detectedCredsStore = getDefaultHelperSuffix()
   109  	}
   110  	return ds, nil
   111  }
   112  
   113  // NewStoreFromDocker returns a Store based on the default docker config file.
   114  //   - If the $DOCKER_CONFIG environment variable is set,
   115  //     $DOCKER_CONFIG/config.json will be used.
   116  //   - Otherwise, the default location $HOME/.docker/config.json will be used.
   117  //
   118  // NewStoreFromDocker internally calls [NewStore].
   119  //
   120  // References:
   121  //   - https://docs.docker.com/engine/reference/commandline/cli/#configuration-files
   122  //   - https://docs.docker.com/engine/reference/commandline/cli/#change-the-docker-directory
   123  func NewStoreFromDocker(opt StoreOptions) (*DynamicStore, error) {
   124  	configPath, err := getDockerConfigPath()
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	return NewStore(configPath, opt)
   129  }
   130  
   131  // Get retrieves credentials from the store for the given server address.
   132  func (ds *DynamicStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
   133  	return ds.getStore(serverAddress).Get(ctx, serverAddress)
   134  }
   135  
   136  // Put saves credentials into the store for the given server address.
   137  // Put returns ErrPlaintextPutDisabled if native store is not available and
   138  // [StoreOptions].AllowPlaintextPut is set to false.
   139  func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
   140  	if err := ds.getStore(serverAddress).Put(ctx, serverAddress, cred); err != nil {
   141  		return err
   142  	}
   143  	// save the detected creds store back to the config file on first put
   144  	return ds.setCredsStoreOnce.Do(func() error {
   145  		if ds.detectedCredsStore != "" {
   146  			if err := ds.config.SetCredentialsStore(ds.detectedCredsStore); err != nil {
   147  				return fmt.Errorf("failed to set credsStore: %w", err)
   148  			}
   149  		}
   150  		return nil
   151  	})
   152  }
   153  
   154  // Delete removes credentials from the store for the given server address.
   155  func (ds *DynamicStore) Delete(ctx context.Context, serverAddress string) error {
   156  	return ds.getStore(serverAddress).Delete(ctx, serverAddress)
   157  }
   158  
   159  // IsAuthConfigured returns whether there is authentication configured in the
   160  // config file or not.
   161  //
   162  // IsAuthConfigured returns true when:
   163  //   - The "credsStore" field is not empty
   164  //   - Or the "credHelpers" field is not empty
   165  //   - Or there is any entry in the "auths" field
   166  func (ds *DynamicStore) IsAuthConfigured() bool {
   167  	return ds.config.IsAuthConfigured()
   168  }
   169  
   170  // ConfigPath returns the path to the config file.
   171  func (ds *DynamicStore) ConfigPath() string {
   172  	return ds.config.Path()
   173  }
   174  
   175  // getHelperSuffix returns the credential helper suffix for the given server
   176  // address.
   177  func (ds *DynamicStore) getHelperSuffix(serverAddress string) string {
   178  	// 1. Look for a server-specific credential helper first
   179  	if helper := ds.config.GetCredentialHelper(serverAddress); helper != "" {
   180  		return helper
   181  	}
   182  	// 2. Then look for the configured native store
   183  	if credsStore := ds.config.CredentialsStore(); credsStore != "" {
   184  		return credsStore
   185  	}
   186  	// 3. Use the detected default store
   187  	return ds.detectedCredsStore
   188  }
   189  
   190  // getStore returns a store for the given server address.
   191  func (ds *DynamicStore) getStore(serverAddress string) Store {
   192  	if helper := ds.getHelperSuffix(serverAddress); helper != "" {
   193  		return NewNativeStore(helper)
   194  	}
   195  
   196  	fs := newFileStore(ds.config)
   197  	fs.DisablePut = !ds.options.AllowPlaintextPut
   198  	return fs
   199  }
   200  
   201  // getDockerConfigPath returns the path to the default docker config file.
   202  func getDockerConfigPath() (string, error) {
   203  	// first try the environment variable
   204  	configDir := os.Getenv(dockerConfigDirEnv)
   205  	if configDir == "" {
   206  		// then try home directory
   207  		homeDir, err := os.UserHomeDir()
   208  		if err != nil {
   209  			return "", fmt.Errorf("failed to get user home directory: %w", err)
   210  		}
   211  		configDir = filepath.Join(homeDir, dockerConfigFileDir)
   212  	}
   213  	return filepath.Join(configDir, dockerConfigFileName), nil
   214  }
   215  
   216  // storeWithFallbacks is a store that has multiple fallback stores.
   217  type storeWithFallbacks struct {
   218  	stores []Store
   219  }
   220  
   221  // NewStoreWithFallbacks returns a new store based on the given stores.
   222  //   - Get() searches the primary and the fallback stores
   223  //     for the credentials and returns when it finds the
   224  //     credentials in any of the stores.
   225  //   - Put() saves the credentials into the primary store.
   226  //   - Delete() deletes the credentials from the primary store.
   227  func NewStoreWithFallbacks(primary Store, fallbacks ...Store) Store {
   228  	if len(fallbacks) == 0 {
   229  		return primary
   230  	}
   231  	return &storeWithFallbacks{
   232  		stores: append([]Store{primary}, fallbacks...),
   233  	}
   234  }
   235  
   236  // Get retrieves credentials from the StoreWithFallbacks for the given server.
   237  // It searches the primary and the fallback stores for the credentials of serverAddress
   238  // and returns when it finds the credentials in any of the stores.
   239  func (sf *storeWithFallbacks) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
   240  	for _, s := range sf.stores {
   241  		cred, err := s.Get(ctx, serverAddress)
   242  		if err != nil {
   243  			return auth.EmptyCredential, err
   244  		}
   245  		if cred != auth.EmptyCredential {
   246  			return cred, nil
   247  		}
   248  	}
   249  	return auth.EmptyCredential, nil
   250  }
   251  
   252  // Put saves credentials into the StoreWithFallbacks. It puts
   253  // the credentials into the primary store.
   254  func (sf *storeWithFallbacks) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
   255  	return sf.stores[0].Put(ctx, serverAddress, cred)
   256  }
   257  
   258  // Delete removes credentials from the StoreWithFallbacks for the given server.
   259  // It deletes the credentials from the primary store.
   260  func (sf *storeWithFallbacks) Delete(ctx context.Context, serverAddress string) error {
   261  	return sf.stores[0].Delete(ctx, serverAddress)
   262  }