Skip to content

Commit de7eba1

Browse files
registry: normalize server address to lowercase for credential lookup
Login credentials fail when the registry hostname's case at `docker login` differs from the case used in an image reference at push/pull time (e.g. `docker login Test.Registry.org` then `docker push Test.Registry.org/img`). distribution/reference does not lowercase the domain component of a reference, so credentials ended up stored and looked up under different keys depending on casing. Normalize the lookup key to lowercase in the config file's canonical credential-lookup key (getAuthConfigKey), covering all callers of GetAuthConfig/GetCredentialsStore. Also lowercase the server address at the credential-store write path in `docker login`, since the file store's Store/Erase use an exact-match key and are not routed through the lookup normalization. `docker logout` hits the same Store/Erase path directly, so it needs the same treatment. Rather than replacing the looked-up address with its lowercase form (which would stop `docker logout` from finding credentials stored under their original case, e.g. by a version of the CLI predating this normalization), try the lowercase variants in addition to the original-case ones. Signed-off-by: HarnageaGabriel <gabriel.harnagea06@gmail.com>
1 parent 4f84911 commit de7eba1

7 files changed

Lines changed: 97 additions & 1 deletion

File tree

cli/command/registry/login.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg st
320320
}
321321

322322
func storeCredentials(cfg *configfile.ConfigFile, authConfig registrytypes.AuthConfig) error {
323+
authConfig.ServerAddress = strings.ToLower(authConfig.ServerAddress)
323324
creds := cfg.GetCredentialsStore(authConfig.ServerAddress)
324325
if err := creds.Store(configtypes.AuthConfig{
325326
Username: authConfig.Username,

cli/command/registry/login_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ func TestRunLogin(t *testing.T) {
149149
},
150150
},
151151
},
152+
{
153+
doc: "mixed-case server address updates lowercase credential key",
154+
priorCredentials: map[string]configtypes.AuthConfig{
155+
"myregistry.example.com": {
156+
Username: "my-username",
157+
Password: "old-password",
158+
ServerAddress: "myregistry.example.com",
159+
},
160+
},
161+
input: loginOptions{
162+
serverAddress: "MyRegistry.Example.com",
163+
user: "my-username",
164+
password: "new-password",
165+
},
166+
expectedCredentials: map[string]configtypes.AuthConfig{
167+
"myregistry.example.com": {
168+
Username: "my-username",
169+
Password: "new-password",
170+
ServerAddress: "myregistry.example.com",
171+
},
172+
},
173+
},
152174
{
153175
doc: "unknown user w/ prior credentials",
154176
priorCredentials: map[string]configtypes.AuthConfig{

cli/command/registry/logout.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package registry
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/docker/cli/cli"
89
"github.com/docker/cli/cli/command"
@@ -60,6 +61,10 @@ func runLogout(ctx context.Context, dockerCLI command.Cli, serverAddress string)
6061
// the tries below are kept for backward compatibility where a user could have
6162
// saved the registry in one of the following format.
6263
regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
64+
// Also try lowercase variants for backward compatibility with case-insensitive
65+
// login normalization in getAuthConfigKey and storeCredentials.
66+
lowercaseAddress := strings.ToLower(hostnameAddress)
67+
regsToLogout = append(regsToLogout, lowercaseAddress, "http://"+lowercaseAddress, "https://"+lowercaseAddress)
6368
}
6469

6570
if isDefaultRegistry {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package registry
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/docker/cli/cli/config/configfile"
9+
configtypes "github.com/docker/cli/cli/config/types"
10+
"github.com/docker/cli/internal/test"
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
func TestRunLogoutMixedCaseServerAddress(t *testing.T) {
15+
cfg := configfile.New(filepath.Join(t.TempDir(), "config.json"))
16+
cli := test.NewFakeCli(nil)
17+
cli.SetConfigFile(cfg)
18+
19+
const serverAddress = "myregistry.example.com"
20+
assert.NilError(t, cfg.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{
21+
Username: "my-username",
22+
Password: "my-password",
23+
ServerAddress: serverAddress,
24+
}))
25+
26+
assert.NilError(t, runLogout(context.Background(), cli, "MyRegistry.Example.com"))
27+
credentials, err := cfg.GetAllCredentials()
28+
assert.NilError(t, err)
29+
assert.DeepEqual(t, credentials, map[string]configtypes.AuthConfig{})
30+
}
31+
32+
func TestRunLogoutUpperCaseServerAddress(t *testing.T) {
33+
cfg := configfile.New(filepath.Join(t.TempDir(), "config.json"))
34+
cli := test.NewFakeCli(nil)
35+
cli.SetConfigFile(cfg)
36+
37+
const serverAddress = "MYREGISTRY.EXAMPLE.COM"
38+
assert.NilError(t, cfg.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{
39+
Username: "my-username",
40+
Password: "my-password",
41+
ServerAddress: serverAddress,
42+
}))
43+
44+
assert.NilError(t, runLogout(context.Background(), cli, serverAddress))
45+
credentials, err := cfg.GetAllCredentials()
46+
assert.NilError(t, err)
47+
assert.DeepEqual(t, credentials, map[string]configtypes.AuthConfig{})
48+
}

cli/command/registry_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ func TestRetrieveAuthTokenFromImage(t *testing.T) {
109109
"localhost": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
110110
"localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
111111
"registry-1.docker.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
112-
"registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
112+
"registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
113+
"test.registry.org": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
113114
}
114115
}`
115116
cfg := configfile.ConfigFile{}
@@ -157,6 +158,10 @@ func TestRetrieveAuthTokenFromImage(t *testing.T) {
157158
prefix: "registry.hub.docker.com",
158159
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry.hub.docker.com"},
159160
},
161+
{
162+
prefix: "Test.Registry.org",
163+
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "test.registry.org"},
164+
},
160165
{
161166
prefix: "[::1]",
162167
expectedAddress: "[::1]",

cli/config/configfile/file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const authConfigKey = "https://index.docker.io/v1/"
4242
//
4343
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.5.1+incompatible/registry#GetAuthConfigKey
4444
func getAuthConfigKey(domainName string) string {
45+
domainName = strings.ToLower(domainName)
4546
if domainName == "docker.io" || domainName == "index.docker.io" {
4647
return authConfigKey
4748
}

cli/config/configfile/file_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ import (
1515
"gotest.tools/v3/golden"
1616
)
1717

18+
func TestGetAuthConfigKey(t *testing.T) {
19+
tests := map[string]string{
20+
"MyRegistry.Example.com": "myregistry.example.com",
21+
"DOCKER.IO": authConfigKey,
22+
"Index.Docker.IO": authConfigKey,
23+
}
24+
25+
for domainName, expected := range tests {
26+
t.Run(domainName, func(t *testing.T) {
27+
assert.Equal(t, getAuthConfigKey(domainName), expected)
28+
})
29+
}
30+
}
31+
1832
func TestEncodeAuth(t *testing.T) {
1933
newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"}
2034
authStr := encodeAuth(newAuthConfig)

0 commit comments

Comments
 (0)