github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/config/configfile/file.go (about)

     1  package configfile
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/khulnasoft/cli/cli/config/credentials"
    12  	"github.com/khulnasoft/cli/cli/config/types"
    13  	"github.com/pkg/errors"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  // ConfigFile ~/.docker/config.json file info
    18  type ConfigFile struct {
    19  	AuthConfigs          map[string]types.AuthConfig  `json:"auths"`
    20  	HTTPHeaders          map[string]string            `json:"HttpHeaders,omitempty"`
    21  	PsFormat             string                       `json:"psFormat,omitempty"`
    22  	ImagesFormat         string                       `json:"imagesFormat,omitempty"`
    23  	NetworksFormat       string                       `json:"networksFormat,omitempty"`
    24  	PluginsFormat        string                       `json:"pluginsFormat,omitempty"`
    25  	VolumesFormat        string                       `json:"volumesFormat,omitempty"`
    26  	StatsFormat          string                       `json:"statsFormat,omitempty"`
    27  	DetachKeys           string                       `json:"detachKeys,omitempty"`
    28  	CredentialsStore     string                       `json:"credsStore,omitempty"`
    29  	CredentialHelpers    map[string]string            `json:"credHelpers,omitempty"`
    30  	Filename             string                       `json:"-"` // Note: for internal use only
    31  	ServiceInspectFormat string                       `json:"serviceInspectFormat,omitempty"`
    32  	ServicesFormat       string                       `json:"servicesFormat,omitempty"`
    33  	TasksFormat          string                       `json:"tasksFormat,omitempty"`
    34  	SecretFormat         string                       `json:"secretFormat,omitempty"`
    35  	ConfigFormat         string                       `json:"configFormat,omitempty"`
    36  	NodesFormat          string                       `json:"nodesFormat,omitempty"`
    37  	PruneFilters         []string                     `json:"pruneFilters,omitempty"`
    38  	Proxies              map[string]ProxyConfig       `json:"proxies,omitempty"`
    39  	Experimental         string                       `json:"experimental,omitempty"`
    40  	CurrentContext       string                       `json:"currentContext,omitempty"`
    41  	CLIPluginsExtraDirs  []string                     `json:"cliPluginsExtraDirs,omitempty"`
    42  	Plugins              map[string]map[string]string `json:"plugins,omitempty"`
    43  	Aliases              map[string]string            `json:"aliases,omitempty"`
    44  	Features             map[string]string            `json:"features,omitempty"`
    45  }
    46  
    47  // ProxyConfig contains proxy configuration settings
    48  type ProxyConfig struct {
    49  	HTTPProxy  string `json:"httpProxy,omitempty"`
    50  	HTTPSProxy string `json:"httpsProxy,omitempty"`
    51  	NoProxy    string `json:"noProxy,omitempty"`
    52  	FTPProxy   string `json:"ftpProxy,omitempty"`
    53  	AllProxy   string `json:"allProxy,omitempty"`
    54  }
    55  
    56  // New initializes an empty configuration file for the given filename 'fn'
    57  func New(fn string) *ConfigFile {
    58  	return &ConfigFile{
    59  		AuthConfigs: make(map[string]types.AuthConfig),
    60  		HTTPHeaders: make(map[string]string),
    61  		Filename:    fn,
    62  		Plugins:     make(map[string]map[string]string),
    63  		Aliases:     make(map[string]string),
    64  	}
    65  }
    66  
    67  // LoadFromReader reads the configuration data given and sets up the auth config
    68  // information with given directory and populates the receiver object
    69  func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
    70  	if err := json.NewDecoder(configData).Decode(configFile); err != nil && !errors.Is(err, io.EOF) {
    71  		return err
    72  	}
    73  	var err error
    74  	for addr, ac := range configFile.AuthConfigs {
    75  		if ac.Auth != "" {
    76  			ac.Username, ac.Password, err = decodeAuth(ac.Auth)
    77  			if err != nil {
    78  				return err
    79  			}
    80  		}
    81  		ac.Auth = ""
    82  		ac.ServerAddress = addr
    83  		configFile.AuthConfigs[addr] = ac
    84  	}
    85  	return nil
    86  }
    87  
    88  // ContainsAuth returns whether there is authentication configured
    89  // in this file or not.
    90  func (configFile *ConfigFile) ContainsAuth() bool {
    91  	return configFile.CredentialsStore != "" ||
    92  		len(configFile.CredentialHelpers) > 0 ||
    93  		len(configFile.AuthConfigs) > 0
    94  }
    95  
    96  // GetAuthConfigs returns the mapping of repo to auth configuration
    97  func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig {
    98  	if configFile.AuthConfigs == nil {
    99  		configFile.AuthConfigs = make(map[string]types.AuthConfig)
   100  	}
   101  	return configFile.AuthConfigs
   102  }
   103  
   104  // SaveToWriter encodes and writes out all the authorization information to
   105  // the given writer
   106  func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error {
   107  	// Encode sensitive data into a new/temp struct
   108  	tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs))
   109  	for k, authConfig := range configFile.AuthConfigs {
   110  		authCopy := authConfig
   111  		// encode and save the authstring, while blanking out the original fields
   112  		authCopy.Auth = encodeAuth(&authCopy)
   113  		authCopy.Username = ""
   114  		authCopy.Password = ""
   115  		authCopy.ServerAddress = ""
   116  		tmpAuthConfigs[k] = authCopy
   117  	}
   118  
   119  	saveAuthConfigs := configFile.AuthConfigs
   120  	configFile.AuthConfigs = tmpAuthConfigs
   121  	defer func() { configFile.AuthConfigs = saveAuthConfigs }()
   122  
   123  	// User-Agent header is automatically set, and should not be stored in the configuration
   124  	for v := range configFile.HTTPHeaders {
   125  		if strings.EqualFold(v, "User-Agent") {
   126  			delete(configFile.HTTPHeaders, v)
   127  		}
   128  	}
   129  
   130  	data, err := json.MarshalIndent(configFile, "", "\t")
   131  	if err != nil {
   132  		return err
   133  	}
   134  	_, err = writer.Write(data)
   135  	return err
   136  }
   137  
   138  // Save encodes and writes out all the authorization information
   139  func (configFile *ConfigFile) Save() (retErr error) {
   140  	if configFile.Filename == "" {
   141  		return errors.Errorf("Can't save config with empty filename")
   142  	}
   143  
   144  	dir := filepath.Dir(configFile.Filename)
   145  	if err := os.MkdirAll(dir, 0o700); err != nil {
   146  		return err
   147  	}
   148  	temp, err := os.CreateTemp(dir, filepath.Base(configFile.Filename))
   149  	if err != nil {
   150  		return err
   151  	}
   152  	defer func() {
   153  		temp.Close()
   154  		if retErr != nil {
   155  			if err := os.Remove(temp.Name()); err != nil {
   156  				logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file")
   157  			}
   158  		}
   159  	}()
   160  
   161  	err = configFile.SaveToWriter(temp)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	if err := temp.Close(); err != nil {
   167  		return errors.Wrap(err, "error closing temp file")
   168  	}
   169  
   170  	// Handle situation where the configfile is a symlink
   171  	cfgFile := configFile.Filename
   172  	if f, err := os.Readlink(cfgFile); err == nil {
   173  		cfgFile = f
   174  	}
   175  
   176  	// Try copying the current config file (if any) ownership and permissions
   177  	copyFilePermissions(cfgFile, temp.Name())
   178  	return os.Rename(temp.Name(), cfgFile)
   179  }
   180  
   181  // ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
   182  // then checking this against any environment variables provided to the container
   183  func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string {
   184  	var cfgKey string
   185  
   186  	if _, ok := configFile.Proxies[host]; !ok {
   187  		cfgKey = "default"
   188  	} else {
   189  		cfgKey = host
   190  	}
   191  
   192  	config := configFile.Proxies[cfgKey]
   193  	permitted := map[string]*string{
   194  		"HTTP_PROXY":  &config.HTTPProxy,
   195  		"HTTPS_PROXY": &config.HTTPSProxy,
   196  		"NO_PROXY":    &config.NoProxy,
   197  		"FTP_PROXY":   &config.FTPProxy,
   198  		"ALL_PROXY":   &config.AllProxy,
   199  	}
   200  	m := runOpts
   201  	if m == nil {
   202  		m = make(map[string]*string)
   203  	}
   204  	for k := range permitted {
   205  		if *permitted[k] == "" {
   206  			continue
   207  		}
   208  		if _, ok := m[k]; !ok {
   209  			m[k] = permitted[k]
   210  		}
   211  		if _, ok := m[strings.ToLower(k)]; !ok {
   212  			m[strings.ToLower(k)] = permitted[k]
   213  		}
   214  	}
   215  	return m
   216  }
   217  
   218  // encodeAuth creates a base64 encoded string to containing authorization information
   219  func encodeAuth(authConfig *types.AuthConfig) string {
   220  	if authConfig.Username == "" && authConfig.Password == "" {
   221  		return ""
   222  	}
   223  
   224  	authStr := authConfig.Username + ":" + authConfig.Password
   225  	msg := []byte(authStr)
   226  	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
   227  	base64.StdEncoding.Encode(encoded, msg)
   228  	return string(encoded)
   229  }
   230  
   231  // decodeAuth decodes a base64 encoded string and returns username and password
   232  func decodeAuth(authStr string) (string, string, error) {
   233  	if authStr == "" {
   234  		return "", "", nil
   235  	}
   236  
   237  	decLen := base64.StdEncoding.DecodedLen(len(authStr))
   238  	decoded := make([]byte, decLen)
   239  	authByte := []byte(authStr)
   240  	n, err := base64.StdEncoding.Decode(decoded, authByte)
   241  	if err != nil {
   242  		return "", "", err
   243  	}
   244  	if n > decLen {
   245  		return "", "", errors.Errorf("Something went wrong decoding auth config")
   246  	}
   247  	userName, password, ok := strings.Cut(string(decoded), ":")
   248  	if !ok || userName == "" {
   249  		return "", "", errors.Errorf("Invalid auth configuration file")
   250  	}
   251  	return userName, strings.Trim(password, "\x00"), nil
   252  }
   253  
   254  // GetCredentialsStore returns a new credentials store from the settings in the
   255  // configuration file
   256  func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
   257  	if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
   258  		return newNativeStore(configFile, helper)
   259  	}
   260  	return credentials.NewFileStore(configFile)
   261  }
   262  
   263  // var for unit testing.
   264  var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   265  	return credentials.NewNativeStore(configFile, helperSuffix)
   266  }
   267  
   268  // GetAuthConfig for a repository from the credential store
   269  func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
   270  	return configFile.GetCredentialsStore(registryHostname).Get(registryHostname)
   271  }
   272  
   273  // getConfiguredCredentialStore returns the credential helper configured for the
   274  // given registry, the default credsStore, or the empty string if neither are
   275  // configured.
   276  func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string {
   277  	if c.CredentialHelpers != nil && registryHostname != "" {
   278  		if helper, exists := c.CredentialHelpers[registryHostname]; exists {
   279  			return helper
   280  		}
   281  	}
   282  	return c.CredentialsStore
   283  }
   284  
   285  // GetAllCredentials returns all of the credentials stored in all of the
   286  // configured credential stores.
   287  func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) {
   288  	auths := make(map[string]types.AuthConfig)
   289  	addAll := func(from map[string]types.AuthConfig) {
   290  		for reg, ac := range from {
   291  			auths[reg] = ac
   292  		}
   293  	}
   294  
   295  	defaultStore := configFile.GetCredentialsStore("")
   296  	newAuths, err := defaultStore.GetAll()
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	addAll(newAuths)
   301  
   302  	// Auth configs from a registry-specific helper should override those from the default store.
   303  	for registryHostname := range configFile.CredentialHelpers {
   304  		newAuth, err := configFile.GetAuthConfig(registryHostname)
   305  		if err != nil {
   306  			logrus.WithError(err).Warnf("Failed to get credentials for registry: %s", registryHostname)
   307  			continue
   308  		}
   309  		auths[registryHostname] = newAuth
   310  	}
   311  	return auths, nil
   312  }
   313  
   314  // GetFilename returns the file name that this config file is based on.
   315  func (configFile *ConfigFile) GetFilename() string {
   316  	return configFile.Filename
   317  }
   318  
   319  // PluginConfig retrieves the requested option for the given plugin.
   320  func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) {
   321  	if configFile.Plugins == nil {
   322  		return "", false
   323  	}
   324  	pluginConfig, ok := configFile.Plugins[pluginname]
   325  	if !ok {
   326  		return "", false
   327  	}
   328  	value, ok := pluginConfig[option]
   329  	return value, ok
   330  }
   331  
   332  // SetPluginConfig sets the option to the given value for the given
   333  // plugin. Passing a value of "" will remove the option. If removing
   334  // the final config item for a given plugin then also cleans up the
   335  // overall plugin entry.
   336  func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) {
   337  	if configFile.Plugins == nil {
   338  		configFile.Plugins = make(map[string]map[string]string)
   339  	}
   340  	pluginConfig, ok := configFile.Plugins[pluginname]
   341  	if !ok {
   342  		pluginConfig = make(map[string]string)
   343  		configFile.Plugins[pluginname] = pluginConfig
   344  	}
   345  	if value != "" {
   346  		pluginConfig[option] = value
   347  	} else {
   348  		delete(pluginConfig, option)
   349  	}
   350  	if len(pluginConfig) == 0 {
   351  		delete(configFile.Plugins, pluginname)
   352  	}
   353  }