diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 471e73f8238e..47ff8476f8cd 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -320,6 +320,7 @@ func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg st } func storeCredentials(cfg *configfile.ConfigFile, authConfig registrytypes.AuthConfig) error { + authConfig.ServerAddress = strings.ToLower(authConfig.ServerAddress) creds := cfg.GetCredentialsStore(authConfig.ServerAddress) if err := creds.Store(configtypes.AuthConfig{ Username: authConfig.Username, diff --git a/cli/command/registry/login_test.go b/cli/command/registry/login_test.go index 32d0e6f8cf7c..ab45711bcd6a 100644 --- a/cli/command/registry/login_test.go +++ b/cli/command/registry/login_test.go @@ -149,6 +149,28 @@ func TestRunLogin(t *testing.T) { }, }, }, + { + doc: "mixed-case server address updates lowercase credential key", + priorCredentials: map[string]configtypes.AuthConfig{ + "myregistry.example.com": { + Username: "my-username", + Password: "old-password", + ServerAddress: "myregistry.example.com", + }, + }, + input: loginOptions{ + serverAddress: "MyRegistry.Example.com", + user: "my-username", + password: "new-password", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "myregistry.example.com": { + Username: "my-username", + Password: "new-password", + ServerAddress: "myregistry.example.com", + }, + }, + }, { doc: "unknown user w/ prior credentials", priorCredentials: map[string]configtypes.AuthConfig{ diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 16218f67d9a7..0a04c8589380 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -3,6 +3,7 @@ package registry import ( "context" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -60,6 +61,10 @@ func runLogout(ctx context.Context, dockerCLI command.Cli, serverAddress string) // the tries below are kept for backward compatibility where a user could have // saved the registry in one of the following format. regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) + // Also try lowercase variants for backward compatibility with case-insensitive + // login normalization in getAuthConfigKey and storeCredentials. + lowercaseAddress := strings.ToLower(hostnameAddress) + regsToLogout = append(regsToLogout, lowercaseAddress, "http://"+lowercaseAddress, "https://"+lowercaseAddress) } if isDefaultRegistry { diff --git a/cli/command/registry/logout_test.go b/cli/command/registry/logout_test.go new file mode 100644 index 000000000000..a073037ace8d --- /dev/null +++ b/cli/command/registry/logout_test.go @@ -0,0 +1,48 @@ +package registry + +import ( + "context" + "path/filepath" + "testing" + + "github.com/docker/cli/cli/config/configfile" + configtypes "github.com/docker/cli/cli/config/types" + "github.com/docker/cli/internal/test" + "gotest.tools/v3/assert" +) + +func TestRunLogoutMixedCaseServerAddress(t *testing.T) { + cfg := configfile.New(filepath.Join(t.TempDir(), "config.json")) + cli := test.NewFakeCli(nil) + cli.SetConfigFile(cfg) + + const serverAddress = "myregistry.example.com" + assert.NilError(t, cfg.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{ + Username: "my-username", + Password: "my-password", + ServerAddress: serverAddress, + })) + + assert.NilError(t, runLogout(context.Background(), cli, "MyRegistry.Example.com")) + credentials, err := cfg.GetAllCredentials() + assert.NilError(t, err) + assert.DeepEqual(t, credentials, map[string]configtypes.AuthConfig{}) +} + +func TestRunLogoutUpperCaseServerAddress(t *testing.T) { + cfg := configfile.New(filepath.Join(t.TempDir(), "config.json")) + cli := test.NewFakeCli(nil) + cli.SetConfigFile(cfg) + + const serverAddress = "MYREGISTRY.EXAMPLE.COM" + assert.NilError(t, cfg.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{ + Username: "my-username", + Password: "my-password", + ServerAddress: serverAddress, + })) + + assert.NilError(t, runLogout(context.Background(), cli, serverAddress)) + credentials, err := cfg.GetAllCredentials() + assert.NilError(t, err) + assert.DeepEqual(t, credentials, map[string]configtypes.AuthConfig{}) +} diff --git a/cli/command/registry_test.go b/cli/command/registry_test.go index 751711f36391..8b9a7b5a4556 100644 --- a/cli/command/registry_test.go +++ b/cli/command/registry_test.go @@ -109,7 +109,8 @@ func TestRetrieveAuthTokenFromImage(t *testing.T) { "localhost": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}, "localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}, "registry-1.docker.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}, - "registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="} + "registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}, + "registry.example.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="} } }` cfg := configfile.ConfigFile{} @@ -157,6 +158,10 @@ func TestRetrieveAuthTokenFromImage(t *testing.T) { prefix: "registry.hub.docker.com", expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry.hub.docker.com"}, }, + { + prefix: "Registry.Example.com", + expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry.example.com"}, + }, { prefix: "[::1]", expectedAddress: "[::1]", diff --git a/cli/config/configfile/file.go b/cli/config/configfile/file.go index 26e148f05987..f109d656bbd4 100644 --- a/cli/config/configfile/file.go +++ b/cli/config/configfile/file.go @@ -42,6 +42,7 @@ const authConfigKey = "https://index.docker.io/v1/" // // [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.5.1+incompatible/registry#GetAuthConfigKey func getAuthConfigKey(domainName string) string { + domainName = strings.ToLower(domainName) if domainName == "docker.io" || domainName == "index.docker.io" { return authConfigKey } diff --git a/cli/config/configfile/file_test.go b/cli/config/configfile/file_test.go index 92df02c74352..e4ca16004a82 100644 --- a/cli/config/configfile/file_test.go +++ b/cli/config/configfile/file_test.go @@ -15,6 +15,20 @@ import ( "gotest.tools/v3/golden" ) +func TestGetAuthConfigKey(t *testing.T) { + tests := map[string]string{ + "MyRegistry.Example.com": "myregistry.example.com", + "DOCKER.IO": authConfigKey, + "Index.Docker.IO": authConfigKey, + } + + for domainName, expected := range tests { + t.Run(domainName, func(t *testing.T) { + assert.Equal(t, getAuthConfigKey(domainName), expected) + }) + } +} + func TestEncodeAuth(t *testing.T) { newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"} authStr := encodeAuth(newAuthConfig)