Skip to content

Commit

Permalink
fix: docker auth for identity tokens (#2866)
Browse files Browse the repository at this point in the history
Fix docker authentication look up so it supports the case where the
username is blank which means that the second returned value is an
identity token not a password.
  • Loading branch information
stevenh authored Oct 31, 2024
1 parent 72a3f1f commit 47764d4
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 26 deletions.
50 changes: 31 additions & 19 deletions docker_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
// defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values.
var defaultRegistryFn = defaultRegistry

// getRegistryCredentials is a variable overwritten in tests to mock the dockercfg.GetRegistryCredentials function.
var getRegistryCredentials = dockercfg.GetRegistryCredentials

// DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
// Finally, it will use the credential helpers to extract the information from the docker config file
// for that registry, if it exists.
Expand Down Expand Up @@ -111,9 +114,28 @@ type credentials struct {

var creds = &credentialsCache{entries: map[string]credentials{}}

// Get returns the username and password for the given hostname
// AuthConfig updates the details in authConfig for the given hostname
// as determined by the details in configKey.
func (c *credentialsCache) AuthConfig(hostname, configKey string, authConfig *registry.AuthConfig) error {
u, p, err := creds.get(hostname, configKey)
if err != nil {
return err
}

if u != "" {
authConfig.Username = u
authConfig.Password = p
} else {
authConfig.IdentityToken = p
}

return nil
}

// get returns the username and password for the given hostname
// as determined by the details in configPath.
func (c *credentialsCache) Get(hostname, configKey string) (string, string, error) {
// If the username is empty, the password is an identity token.
func (c *credentialsCache) get(hostname, configKey string) (string, string, error) {
key := configKey + ":" + hostname
c.mtx.RLock()
entry, ok := c.entries[key]
Expand All @@ -124,7 +146,7 @@ func (c *credentialsCache) Get(hostname, configKey string) (string, string, erro
}

// No entry found, request and cache.
user, password, err := dockercfg.GetRegistryCredentials(hostname)
user, password, err := getRegistryCredentials(hostname)
if err != nil {
return "", "", fmt.Errorf("getting credentials for %s: %w", hostname, err)
}
Expand Down Expand Up @@ -186,14 +208,10 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
switch {
case ac.Username == "" && ac.Password == "":
// Look up credentials from the credential store.
u, p, err := creds.Get(k, key)
if err != nil {
if err := creds.AuthConfig(k, key, &ac); err != nil {
results <- authConfigResult{err: err}
return
}

ac.Username = u
ac.Password = p
case ac.Auth == "":
// Create auth from the username and password encoding.
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
Expand All @@ -203,25 +221,19 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
}(k, v)
}

// in the case where the auth field in the .docker/conf.json is empty, and the user has credential helpers registered
// the auth comes from there
// In the case where the auth field in the .docker/conf.json is empty, and the user has
// credential helpers registered the auth comes from there.
for k := range cfg.CredentialHelpers {
go func(k string) {
defer wg.Done()

u, p, err := creds.Get(k, key)
if err != nil {
var ac registry.AuthConfig
if err := creds.AuthConfig(k, key, &ac); err != nil {
results <- authConfigResult{err: err}
return
}

results <- authConfigResult{
key: k,
cfg: registry.AuthConfig{
Username: u,
Password: p,
},
}
results <- authConfigResult{key: k, cfg: ac}
}(k)
}

Expand Down
64 changes: 57 additions & 7 deletions docker_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ import (
"github.com/testcontainers/testcontainers-go/wait"
)

const exampleAuth = "https://example-auth.com"
const (
exampleAuth = "https://example-auth.com"
privateRegistry = "https://my.private.registry"
exampleRegistry = "https://example.com"
)

func Test_getDockerConfig(t *testing.T) {
expectedConfig := &dockercfg.Config{
AuthConfigs: map[string]dockercfg.AuthConfig{
core.IndexDockerIO: {},
"https://example.com": {},
"https://my.private.registry": {},
core.IndexDockerIO: {},
exampleRegistry: {},
privateRegistry: {},
},
CredentialsStore: "desktop",
}
Expand Down Expand Up @@ -378,6 +382,13 @@ func localAddress(t *testing.T) string {
//go:embed testdata/.docker/config.json
var dockerConfig string

// reset resets the credentials cache.
func (c *credentialsCache) reset() {
c.mtx.Lock()
defer c.mtx.Unlock()
c.entries = make(map[string]credentials)
}

func Test_getDockerAuthConfigs(t *testing.T) {
t.Run("HOME/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata")
Expand Down Expand Up @@ -418,6 +429,45 @@ func Test_getDockerAuthConfigs(t *testing.T) {
require.Nil(t, authConfigs)
})

t.Run("DOCKER_AUTH_CONFIG/identity-token", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-exist")

// Reset the credentials cache to ensure our mocked method is called.
creds.reset()

// Mock getRegistryCredentials to return identity-token for index.docker.io.
old := getRegistryCredentials
t.Cleanup(func() {
getRegistryCredentials = old
creds.reset() // Ensure our mocked results aren't cached.
})
getRegistryCredentials = func(hostname string) (string, string, error) {
switch hostname {
case core.IndexDockerIO:
return "", "identity-token", nil
default:
return "username", "password", nil
}
}
t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig)

authConfigs, err := getDockerAuthConfigs()
require.NoError(t, err)
require.Equal(t, map[string]registry.AuthConfig{
core.IndexDockerIO: {
IdentityToken: "identity-token",
},
privateRegistry: {
Username: "username",
Password: "password",
},
exampleRegistry: {
Username: "username",
Password: "password",
},
}, authConfigs)
})

t.Run("DOCKER_CONFIG/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", ".docker"))
Expand Down Expand Up @@ -445,9 +495,9 @@ func requireValidAuthConfig(t *testing.T) {
// We can only check the keys as the values are not deterministic as they depend
// on users environment.
expected := map[string]registry.AuthConfig{
"https://index.docker.io/v1/": {},
"https://example.com": {},
"https://my.private.registry": {},
core.IndexDockerIO: {},
exampleRegistry: {},
privateRegistry: {},
}
for k := range authConfigs {
authConfigs[k] = registry.AuthConfig{}
Expand Down

0 comments on commit 47764d4

Please sign in to comment.