Skip to content

Commit dc75e9e

Browse files
committed
cli/config: do not discard permission errors when loading config-file
When attempting to load a config-file that exists, but is not accessible for the current user, we should not discard the error. This patch makes sure that the error is returned by Load(), but does not yet change LoadDefaultConfigFile, as this requires a change in signature. Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent c80adf4 commit dc75e9e

File tree

2 files changed

+51
-16
lines changed

2 files changed

+51
-16
lines changed

‎cli/config/config.go‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func Path(p ...string) (string, error) {
7575
}
7676

7777
// LoadFromReader is a convenience function that creates a ConfigFile object from
78-
// a reader
78+
// a reader. It returns an error if configData is malformed.
7979
func LoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) {
8080
configFile := configfile.ConfigFile{
8181
AuthConfigs: make(map[string]types.AuthConfig),
@@ -88,6 +88,10 @@ func LoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) {
8888
// If no directory is given, it uses the default [Dir]. A [*configfile.ConfigFile]
8989
// is returned containing the contents of the configuration file, or a default
9090
// struct if no configfile exists in the given location.
91+
//
92+
// Load returns an error if a configuration file exists in the given location,
93+
// but cannot be read, or is malformed. Consumers must handle errors to prevent
94+
// overwriting an existing configuration file.
9195
func Load(configDir string) (*configfile.ConfigFile, error) {
9296
if configDir == "" {
9397
configDir = Dir()
@@ -102,19 +106,17 @@ func load(configDir string) (*configfile.ConfigFile, error) {
102106
file, err := os.Open(filename)
103107
if err != nil {
104108
if os.IsNotExist(err) {
105-
//
106-
// if file is there but we can't stat it for any reason other
107-
// than it doesn't exist then stop
109+
// It is OK for no configuration file to be present, in which
110+
// case we return a default struct.
108111
return configFile, nil
109112
}
110-
// if file is there but we can't stat it for any reason other
111-
// than it doesn't exist then stop
112-
return configFile, nil
113+
// Any other error happening when failing to read the file must be returned.
114+
return configFile, errors.Wrap(err, "loading config file")
113115
}
114116
defer file.Close()
115117
err = configFile.LoadFromReader(file)
116118
if err != nil {
117-
err = errors.Wrap(err, filename)
119+
err = errors.Wrapf(err, "loading config file: %s: ", filename)
118120
}
119121
return configFile, err
120122
}
@@ -133,7 +135,8 @@ func load(configDir string) (*configfile.ConfigFile, error) {
133135
func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile {
134136
configFile, err := load(Dir())
135137
if err != nil {
136-
_, _ = fmt.Fprintf(stderr, "WARNING: Error loading config file: %v\n", err)
138+
// FIXME(thaJeztah): we should not proceed here to prevent overwriting existing (but malformed) config files; see https://github.com/docker/cli/issues/5075
139+
_, _ = fmt.Fprintln(stderr, "WARNING: Error", err)
137140
}
138141
if !configFile.ContainsAuth() {
139142
configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore)

‎cli/config/config_test.go‎

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"runtime"
89
"strings"
910
"testing"
1011

1112
"github.com/docker/cli/cli/config/configfile"
1213
"github.com/docker/cli/cli/config/credentials"
1314
"gotest.tools/v3/assert"
1415
is "gotest.tools/v3/assert/cmp"
16+
"gotest.tools/v3/skip"
1517
)
1618

1719
func setupConfigDir(t *testing.T) string {
@@ -69,6 +71,19 @@ func TestLoadDanglingSymlink(t *testing.T) {
6971
assert.Equal(t, fi.Mode()&os.ModeSymlink, os.ModeSymlink, "expected %v to be a symlink", cfgFile)
7072
}
7173

74+
func TestLoadNoPermissions(t *testing.T) {
75+
if runtime.GOOS != "windows" {
76+
skip.If(t, os.Getuid() == 0, "cannot test permission denied when running as root")
77+
}
78+
cfgDir := t.TempDir()
79+
cfgFile := filepath.Join(cfgDir, ConfigFileName)
80+
err := os.WriteFile(cfgFile, []byte(`{}`), os.FileMode(0o000))
81+
assert.NilError(t, err)
82+
83+
_, err = Load(cfgDir)
84+
assert.ErrorIs(t, err, os.ErrPermission)
85+
}
86+
7287
func TestSaveFileToDirs(t *testing.T) {
7388
tmpHome := filepath.Join(t.TempDir(), ".docker")
7489
config, err := Load(tmpHome)
@@ -345,14 +360,31 @@ func TestLoadDefaultConfigFile(t *testing.T) {
345360
err := os.WriteFile(filename, content, 0o644)
346361
assert.NilError(t, err)
347362

348-
configFile := LoadDefaultConfigFile(buffer)
349-
credStore := credentials.DetectDefaultStore("")
350-
expected := configfile.New(filename)
351-
expected.CredentialsStore = credStore
352-
expected.PsFormat = "format"
363+
t.Run("success", func(t *testing.T) {
364+
configFile := LoadDefaultConfigFile(buffer)
365+
credStore := credentials.DetectDefaultStore("")
366+
expected := configfile.New(filename)
367+
expected.CredentialsStore = credStore
368+
expected.PsFormat = "format"
353369

354-
assert.Check(t, is.DeepEqual(expected, configFile))
355-
assert.Check(t, is.Equal(buffer.String(), ""))
370+
assert.Check(t, is.DeepEqual(expected, configFile))
371+
assert.Check(t, is.Equal(buffer.String(), ""))
372+
})
373+
374+
t.Run("permission error", func(t *testing.T) {
375+
if runtime.GOOS != "windows" {
376+
skip.If(t, os.Getuid() == 0, "cannot test permission denied when running as root")
377+
}
378+
err = os.Chmod(filename, 0o000)
379+
assert.NilError(t, err)
380+
381+
buffer.Reset()
382+
_ = LoadDefaultConfigFile(buffer)
383+
warnings := buffer.String()
384+
385+
assert.Check(t, is.Contains(warnings, "WARNING:"))
386+
assert.Check(t, is.Contains(warnings, os.ErrPermission.Error()))
387+
})
356388
}
357389

358390
func TestConfigPath(t *testing.T) {

0 commit comments

Comments
 (0)