From 7b8b61bda1cef0211db51b44ef293b069d9a9ae8 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Wed, 1 Apr 2015 15:39:37 -0700 Subject: [PATCH] Add .docker/config.json and support for HTTP Headers This PR does the following: - migrated ~/.dockerfg to ~/.docker/config.json. The data is migrated but the old file remains in case its needed - moves the auth json in that fie into an "auth" property so we can add new top-level properties w/o messing with the auth stuff - adds support for an HttpHeaders property in ~/.docker/config.json which adds these http headers to all msgs from the cli In a follow-on PR I'll move the config file process out from under "registry" since it not specific to that any more. I didn't do it here because I wanted the diff to be smaller so people can make sure I didn't break/miss any auth code during my edits. Signed-off-by: Doug Davis --- docs/auth.go | 105 ++++++++++++++++++++++-------- docs/auth_test.go | 26 ++++---- docs/config_file_test.go | 135 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 38 deletions(-) create mode 100644 docs/config_file_test.go diff --git a/docs/auth.go b/docs/auth.go index 51b781dd..bccf58fc 100644 --- a/docs/auth.go +++ b/docs/auth.go @@ -8,24 +8,27 @@ import ( "io/ioutil" "net/http" "os" - "path" + "path/filepath" "strings" "sync" "time" "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/requestdecorator" ) const ( // Where we store the config file - CONFIGFILE = ".dockercfg" + CONFIGFILE = "config.json" + OLD_CONFIGFILE = ".dockercfg" ) var ( ErrConfigFileMissing = errors.New("The Auth config file is missing") ) +// Registry Auth Info type AuthConfig struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` @@ -34,9 +37,11 @@ type AuthConfig struct { ServerAddress string `json:"serveraddress,omitempty"` } +// ~/.docker/config.json file info type ConfigFile struct { - Configs map[string]AuthConfig `json:"configs,omitempty"` - rootPath string + AuthConfigs map[string]AuthConfig `json:"auths"` + HttpHeaders map[string]string `json:"HttpHeaders,omitempty"` + filename string // Note: not serialized - for internal use only } type RequestAuthorization struct { @@ -147,18 +152,58 @@ func decodeAuth(authStr string) (string, string, error) { // load up the auth config information and return values // FIXME: use the internal golang config parser -func LoadConfig(rootPath string) (*ConfigFile, error) { - configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath} - confFile := path.Join(rootPath, CONFIGFILE) +func LoadConfig(configDir string) (*ConfigFile, error) { + if configDir == "" { + configDir = filepath.Join(homedir.Get(), ".docker") + } + + configFile := ConfigFile{ + AuthConfigs: make(map[string]AuthConfig), + filename: filepath.Join(configDir, CONFIGFILE), + } + + // Try happy path first - latest config file + if _, err := os.Stat(configFile.filename); err == nil { + file, err := os.Open(configFile.filename) + if err != nil { + return &configFile, err + } + defer file.Close() + + if err := json.NewDecoder(file).Decode(&configFile); err != nil { + return &configFile, err + } + + for addr, ac := range configFile.AuthConfigs { + ac.Username, ac.Password, err = decodeAuth(ac.Auth) + if err != nil { + return &configFile, err + } + ac.Auth = "" + ac.ServerAddress = addr + configFile.AuthConfigs[addr] = ac + } + + return &configFile, nil + } else if !os.IsNotExist(err) { + // if file is there but we can't stat it for any reason other + // than it doesn't exist then stop + return &configFile, err + } + + // Can't find latest config file so check for the old one + confFile := filepath.Join(homedir.Get(), OLD_CONFIGFILE) + if _, err := os.Stat(confFile); err != nil { return &configFile, nil //missing file is not an error } + b, err := ioutil.ReadFile(confFile) if err != nil { return &configFile, err } - if err := json.Unmarshal(b, &configFile.Configs); err != nil { + if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { arr := strings.Split(string(b), "\n") if len(arr) < 2 { return &configFile, fmt.Errorf("The Auth config file is empty") @@ -179,48 +224,52 @@ func LoadConfig(rootPath string) (*ConfigFile, error) { authConfig.Email = origEmail[1] authConfig.ServerAddress = IndexServerAddress() // *TODO: Switch to using IndexServerName() instead? - configFile.Configs[IndexServerAddress()] = authConfig + configFile.AuthConfigs[IndexServerAddress()] = authConfig } else { - for k, authConfig := range configFile.Configs { + for k, authConfig := range configFile.AuthConfigs { authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) if err != nil { return &configFile, err } authConfig.Auth = "" authConfig.ServerAddress = k - configFile.Configs[k] = authConfig + configFile.AuthConfigs[k] = authConfig } } return &configFile, nil } -// save the auth config -func SaveConfig(configFile *ConfigFile) error { - confFile := path.Join(configFile.rootPath, CONFIGFILE) - if len(configFile.Configs) == 0 { - os.Remove(confFile) - return nil - } - - configs := make(map[string]AuthConfig, len(configFile.Configs)) - for k, authConfig := range configFile.Configs { +func (configFile *ConfigFile) Save() error { + // Encode sensitive data into a new/temp struct + tmpAuthConfigs := make(map[string]AuthConfig, len(configFile.AuthConfigs)) + for k, authConfig := range configFile.AuthConfigs { authCopy := authConfig authCopy.Auth = encodeAuth(&authCopy) authCopy.Username = "" authCopy.Password = "" authCopy.ServerAddress = "" - configs[k] = authCopy + tmpAuthConfigs[k] = authCopy } - b, err := json.MarshalIndent(configs, "", "\t") + saveAuthConfigs := configFile.AuthConfigs + configFile.AuthConfigs = tmpAuthConfigs + defer func() { configFile.AuthConfigs = saveAuthConfigs }() + + data, err := json.MarshalIndent(configFile, "", "\t") if err != nil { return err } - err = ioutil.WriteFile(confFile, b, 0600) + + if err := os.MkdirAll(filepath.Dir(configFile.filename), 0600); err != nil { + return err + } + + err = ioutil.WriteFile(configFile.filename, data, 0600) if err != nil { return err } + return nil } @@ -431,7 +480,7 @@ func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, regis func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { configKey := index.GetAuthConfigKey() // First try the happy case - if c, found := config.Configs[configKey]; found || index.Official { + if c, found := config.AuthConfigs[configKey]; found || index.Official { return c } @@ -450,7 +499,7 @@ func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { // Maybe they have a legacy config file, we will iterate the keys converting // them to the new format and testing - for registry, config := range config.Configs { + for registry, config := range config.AuthConfigs { if configKey == convertToHostname(registry) { return config } @@ -459,3 +508,7 @@ func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { // When all else fails, return an empty auth config return AuthConfig{} } + +func (config *ConfigFile) Filename() string { + return config.filename +} diff --git a/docs/auth_test.go b/docs/auth_test.go index 9cc299aa..b07aa7db 100644 --- a/docs/auth_test.go +++ b/docs/auth_test.go @@ -3,6 +3,7 @@ package registry import ( "io/ioutil" "os" + "path/filepath" "testing" ) @@ -31,13 +32,14 @@ func setupTempConfigFile() (*ConfigFile, error) { if err != nil { return nil, err } + root = filepath.Join(root, CONFIGFILE) configFile := &ConfigFile{ - rootPath: root, - Configs: make(map[string]AuthConfig), + AuthConfigs: make(map[string]AuthConfig), + filename: root, } for _, registry := range []string{"testIndex", IndexServerAddress()} { - configFile.Configs[registry] = AuthConfig{ + configFile.AuthConfigs[registry] = AuthConfig{ Username: "docker-user", Password: "docker-pass", Email: "docker@docker.io", @@ -52,14 +54,14 @@ func TestSameAuthDataPostSave(t *testing.T) { if err != nil { t.Fatal(err) } - defer os.RemoveAll(configFile.rootPath) + defer os.RemoveAll(configFile.filename) - err = SaveConfig(configFile) + err = configFile.Save() if err != nil { t.Fatal(err) } - authConfig := configFile.Configs["testIndex"] + authConfig := configFile.AuthConfigs["testIndex"] if authConfig.Username != "docker-user" { t.Fail() } @@ -79,9 +81,9 @@ func TestResolveAuthConfigIndexServer(t *testing.T) { if err != nil { t.Fatal(err) } - defer os.RemoveAll(configFile.rootPath) + defer os.RemoveAll(configFile.filename) - indexConfig := configFile.Configs[IndexServerAddress()] + indexConfig := configFile.AuthConfigs[IndexServerAddress()] officialIndex := &IndexInfo{ Official: true, @@ -102,7 +104,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) { if err != nil { t.Fatal(err) } - defer os.RemoveAll(configFile.rootPath) + defer os.RemoveAll(configFile.filename) registryAuth := AuthConfig{ Username: "foo-user", @@ -119,7 +121,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) { Password: "baz-pass", Email: "baz@example.com", } - configFile.Configs[IndexServerAddress()] = officialAuth + configFile.AuthConfigs[IndexServerAddress()] = officialAuth expectedAuths := map[string]AuthConfig{ "registry.example.com": registryAuth, @@ -157,12 +159,12 @@ func TestResolveAuthConfigFullURL(t *testing.T) { Name: configKey, } for _, registry := range registries { - configFile.Configs[registry] = configured + configFile.AuthConfigs[registry] = configured resolved := configFile.ResolveAuthConfig(index) if resolved.Email != configured.Email { t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email) } - delete(configFile.Configs, registry) + delete(configFile.AuthConfigs, registry) resolved = configFile.ResolveAuthConfig(index) if resolved.Email == configured.Email { t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email) diff --git a/docs/config_file_test.go b/docs/config_file_test.go new file mode 100644 index 00000000..9abb8ee9 --- /dev/null +++ b/docs/config_file_test.go @@ -0,0 +1,135 @@ +package registry + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/docker/docker/pkg/homedir" +) + +func TestMissingFile(t *testing.T) { + tmpHome, _ := ioutil.TempDir("", "config-test") + + config, err := LoadConfig(tmpHome) + if err != nil { + t.Fatalf("Failed loading on missing file: %q", err) + } + + // Now save it and make sure it shows up in new form + err = config.Save() + if err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE)) + if !strings.Contains(string(buf), `"auths":`) { + t.Fatalf("Should have save in new form: %s", string(buf)) + } +} + +func TestEmptyFile(t *testing.T) { + tmpHome, _ := ioutil.TempDir("", "config-test") + fn := filepath.Join(tmpHome, CONFIGFILE) + ioutil.WriteFile(fn, []byte(""), 0600) + + _, err := LoadConfig(tmpHome) + if err == nil { + t.Fatalf("Was supposed to fail") + } +} + +func TestEmptyJson(t *testing.T) { + tmpHome, _ := ioutil.TempDir("", "config-test") + fn := filepath.Join(tmpHome, CONFIGFILE) + ioutil.WriteFile(fn, []byte("{}"), 0600) + + config, err := LoadConfig(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + // Now save it and make sure it shows up in new form + err = config.Save() + if err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE)) + if !strings.Contains(string(buf), `"auths":`) { + t.Fatalf("Should have save in new form: %s", string(buf)) + } +} + +func TestOldJson(t *testing.T) { + if runtime.GOOS == "windows" { + return + } + + tmpHome, _ := ioutil.TempDir("", "config-test") + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, OLD_CONFIGFILE) + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + ioutil.WriteFile(fn, []byte(js), 0600) + + config, err := LoadConfig(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Email != "user@example.com" || ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + err = config.Save() + if err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE)) + if !strings.Contains(string(buf), `"auths":`) || + !strings.Contains(string(buf), "user@example.com") { + t.Fatalf("Should have save in new form: %s", string(buf)) + } +} + +func TestNewJson(t *testing.T) { + tmpHome, _ := ioutil.TempDir("", "config-test") + fn := filepath.Join(tmpHome, CONFIGFILE) + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }` + ioutil.WriteFile(fn, []byte(js), 0600) + + config, err := LoadConfig(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Email != "user@example.com" || ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + err = config.Save() + if err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE)) + if !strings.Contains(string(buf), `"auths":`) || + !strings.Contains(string(buf), "user@example.com") { + t.Fatalf("Should have save in new form: %s", string(buf)) + } +}