diff --git a/core/container/dockercontroller/dockercontroller.go b/core/container/dockercontroller/dockercontroller.go index 6995e0ad28d..e1f4a2f376a 100644 --- a/core/container/dockercontroller/dockercontroller.go +++ b/core/container/dockercontroller/dockercontroller.go @@ -13,8 +13,11 @@ import ( "context" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "io" + "os" + "path/filepath" "regexp" "strconv" "strings" @@ -27,6 +30,7 @@ import ( "github.com/hyperledger/fabric/core/container" "github.com/hyperledger/fabric/core/container/ccintf" dcontainer "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/registry" dcli "github.com/moby/moby/client" "github.com/pkg/errors" ) @@ -119,6 +123,7 @@ func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { Tags: []string{id}, PullParent: vm.ChaincodePull, NetworkMode: vm.NetworkMode, + AuthConfigs: getAuthFromDockerConfig(), }) vm.BuildMetrics.ChaincodeImageBuildDuration.With( @@ -139,6 +144,96 @@ func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { return nil } +// dockerConfigFile is a minimal struct for unmarshalling authentication details +// from the Docker config file. +type dockerConfigFile struct { + Auths map[string]registry.AuthConfig `json:"auths,omitempty"` +} + +// getAuthFromDockerConfig gets the authentication configuration that may be +// provided in the Docker config file. +func getAuthFromDockerConfig() map[string]registry.AuthConfig { + emptyConfig := make(map[string]registry.AuthConfig) + + configFilePath := filepath.Join(getDockerConfigDir(), "config.json") + + file, err := os.Open(configFilePath) + if err != nil { + dockerLogger.Debugf("Error occurred while trying to read Docker config file from path \"%s\". Returning empty auth config.", configFilePath) + return emptyConfig + } + defer file.Close() + + var currConfig dockerConfigFile + err = json.NewDecoder(file).Decode(&currConfig) + if err != nil { + return emptyConfig + } + + mappedAuths, err := mapDockerConfigAuth(currConfig.Auths) + if err != nil { + dockerLogger.Debugf("Error while decoding Docker auth: %s. Returning empty auth config.", err.Error()) + return emptyConfig + } + + return mappedAuths +} + +func getDockerConfigDir() string { + homeDir, _ := os.UserHomeDir() + + // The value of DOCKER_CONFIG environment variable overrides the default + // config directory (~/.docker). This behaviour mimics that of Docker CLI. + configDir := os.Getenv("DOCKER_CONFIG") + if configDir == "" { + configDir = filepath.Join(homeDir, ".docker") + } + + return configDir +} + +// mapDockerConfigAuth maps the provided Docker auth configuration to the format +// expected by the Docker Engine (i.e. Username and Password based auth). +func mapDockerConfigAuth(auths map[string]registry.AuthConfig) (map[string]registry.AuthConfig, error) { + mappedAuth := make(map[string]registry.AuthConfig) + + for reg, auth := range auths { + if auth.Auth != "" { + username, password, err := decodeAuth(auth.Auth) + if err != nil { + return mappedAuth, err + } + + mappedAuth[reg] = registry.AuthConfig{ + Username: username, + Password: password, + } + } + } + + return mappedAuth, nil +} + +// decodeAuth decodes the provided Docker authentication configuration as a +// Base64 encoded string and returns the username and password from it. +func decodeAuth(auth string) (string, string, error) { + decLen := base64.StdEncoding.DecodedLen(len(auth)) + decoded := make([]byte, decLen) + + n, err := base64.StdEncoding.Decode(decoded, []byte(auth)) + if err != nil { + return "", "", err + } + if n > decLen { + return "", "", errors.New("something went wrong decoding auth config") + } + userName, password, ok := strings.Cut(string(decoded), ":") + if !ok || userName == "" { + return "", "", errors.New("invalid auth configuration file") + } + return userName, strings.Trim(password, "\x00"), nil +} + // Build is responsible for building an image if it does not already exist. func (vm *DockerVM) Build(ccid string, metadata *persistence.ChaincodePackageMetadata, codePackage io.Reader) (container.Instance, error) { imageName, err := vm.GetVMNameForDocker(ccid) diff --git a/core/container/dockercontroller/dockercontroller_test.go b/core/container/dockercontroller/dockercontroller_test.go index 58542f49e7e..8cfe24e810b 100644 --- a/core/container/dockercontroller/dockercontroller_test.go +++ b/core/container/dockercontroller/dockercontroller_test.go @@ -16,6 +16,8 @@ import ( "errors" "fmt" "io" + "os" + "path/filepath" "testing" "time" @@ -428,6 +430,44 @@ func Test_buildImageFailure(t *testing.T) { require.EqualError(t, err, "oh-bother-we-failed-badly") } +func Test_getAuthConfigFromDockerConfig(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("HOME", tempDir) + + configFile := []byte(` +{ + "auths": { + "repo.example.com": { + "auth": "dXNlcm5hbWU6cGFzc3dvcmQK" + } + } +} + `) + + rc := &mock.ReadCloser{} + rc.ReadReturns(0, io.EOF) + client := &mock.DockerClient{} + client.ImageBuildReturns(dcli.ImageBuildResult{Body: rc}, nil) + + dockerDir := filepath.Join(tempDir, ".docker") + if _, err := os.Stat(dockerDir); err != nil { + _ = os.Mkdir(dockerDir, 0o700) + } + + configFilePath := filepath.Join(dockerDir, "config.json") + err := os.WriteFile(configFilePath, configFile, 0o644) + require.NoError(t, err) + + configs := getAuthFromDockerConfig() + require.Greater(t, len(configs), 0, "len of auth configs should not be 0") + + repoAuth, ok := configs["repo.example.com"] + require.True(t, ok, "the key repo.example.com should be set") + require.Empty(t, repoAuth.Auth) + require.NotEmpty(t, repoAuth.Username) + require.NotEmpty(t, repoAuth.Password) +} + func TestBuild(t *testing.T) { buildMetrics := NewBuildMetrics(&disabled.Provider{}) md := &persistence.ChaincodePackageMetadata{