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 }