Skip to content

Commit

Permalink
feat(cnbBuild): validate docker credentials (#4840)
Browse files Browse the repository at this point in the history
  • Loading branch information
modulo11 authored Mar 15, 2024
1 parent d54df69 commit df2e976
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 43 deletions.
9 changes: 8 additions & 1 deletion cmd/cnbBuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,19 @@ func runCnbBuild(config *cnbBuildOptions, telemetry *buildpacks.Telemetry, image
}
}

cnbRegistryAuth, err := cnbutils.GenerateCnbAuth(config.DockerConfigJSON, utils)
credentials := cnbutils.NewCredentials(utils)
cnbRegistryAuth, err := credentials.GenerateCredentials(config.DockerConfigJSON)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrap(err, "failed to generate CNB_REGISTRY_AUTH")
}

found := credentials.Validate(targetImage.ContainerRegistry.Host)
if !found {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.New(fmt.Sprintf("DockerConfigJSON does not contain credentials for target registry (%s)", targetImage.ContainerRegistry.Host))
}

if len(config.CustomTLSCertificateLinks) > 0 {
caCertificates := "/tmp/ca-certificates.crt"
_, err := utils.Copy("/etc/ssl/certs/ca-certificates.crt", caCertificates)
Expand Down
86 changes: 61 additions & 25 deletions cmd/cnbBuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,15 @@ func TestRunCnbBuild(t *testing.T) {
`

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile("project.toml", []byte(projectToml))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assertLifecycleCalls(t, runner, 2)
assert.Contains(t, runner.Calls[1].Params, fmt.Sprintf("%s/%s:%s", imageRegistry, config.ContainerImageName, config.ContainerImageTag))
assert.Contains(t, runner.Calls[1].Params, "-run-image")
Expand All @@ -160,7 +160,7 @@ func TestRunCnbBuild(t *testing.T) {
`

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile("project.toml", []byte(projectToml))
addBuilderFiles(&utils)

Expand All @@ -169,7 +169,7 @@ func TestRunCnbBuild(t *testing.T) {

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assertLifecycleCalls(t, runner, 2)
assert.Contains(t, runner.Calls[1].Params, fmt.Sprintf("%s/%s:%s", imageRegistry, "io-buildpacks-my-app", config.ContainerImageTag))
assert.Equal(t, config.ContainerRegistryURL, commonPipelineEnvironment.container.registryURL)
Expand All @@ -190,14 +190,14 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assertLifecycleCalls(t, runner, 2)
assert.Contains(t, runner.Calls[1].Params, fmt.Sprintf("%s/%s:%s", imageRegistry, config.ContainerImageName, config.ContainerImageTag))
assert.Equal(t, config.ContainerRegistryURL, commonPipelineEnvironment.container.registryURL)
Expand All @@ -215,14 +215,14 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assertLifecycleCalls(t, runner, 2)
assert.Contains(t, runner.Calls[1].Params, fmt.Sprintf("%s/%s:%s", config.ContainerRegistryURL, config.ContainerImageName, config.ContainerImageTag))
assert.Equal(t, fmt.Sprintf("https://%s", config.ContainerRegistryURL), commonPipelineEnvironment.container.registryURL)
Expand All @@ -245,14 +245,14 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Equal(t, creatorPath, runner.Calls[1].Exec)
assert.Contains(t, runner.Calls[1].Params, "/tmp/buildpacks")
assert.Contains(t, runner.Calls[1].Params, "/tmp/buildpacks/order.toml")
Expand Down Expand Up @@ -283,14 +283,14 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Equal(t, creatorPath, runner.Calls[1].Exec)
assert.Contains(t, runner.Calls[1].Params, "/tmp/buildpacks")
assert.Contains(t, runner.Calls[1].Params, "/tmp/buildpacks/order.toml")
Expand Down Expand Up @@ -319,14 +319,14 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Equal(t, creatorPath, runner.Calls[1].Exec)
assert.Contains(t, runner.Calls[1].Params, "/tmp/buildpacks")
assert.Contains(t, runner.Calls[1].Params, "/tmp/buildpacks/order.toml")
Expand Down Expand Up @@ -358,7 +358,7 @@ func TestRunCnbBuild(t *testing.T) {

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(caCertsFile, []byte("test\n"))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, client)
Expand All @@ -370,7 +370,7 @@ func TestRunCnbBuild(t *testing.T) {

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, fmt.Sprintf("SSL_CERT_FILE=%s", caCertsTmpFile))
assertLifecycleCalls(t, runner, 2)
assert.Contains(t, runner.Calls[1].Params, fmt.Sprintf("%s/%s:%s", config.ContainerRegistryURL, config.ContainerImageName, config.ContainerImageTag))
Expand All @@ -387,7 +387,7 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
Expand All @@ -412,6 +412,7 @@ func TestRunCnbBuild(t *testing.T) {
"OPTIONS_KEY": "OPTIONS_VALUE",
"OVERWRITE": "this should win",
},
DockerConfigJSON: "/path/to/config.json",
}

projectToml := `[project]
Expand All @@ -427,6 +428,7 @@ func TestRunCnbBuild(t *testing.T) {
`

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile("project.toml", []byte(projectToml))
addBuilderFiles(&utils)

Expand All @@ -453,7 +455,7 @@ func TestRunCnbBuild(t *testing.T) {
utils := newCnbBuildTestsUtils()
utils.FilesMock.CurrentDir = "/jenkins"
utils.FilesMock.AddDir("/jenkins")
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile("/workspace/pom.xml", []byte("test"))
addBuilderFiles(&utils)

Expand All @@ -478,7 +480,7 @@ func TestRunCnbBuild(t *testing.T) {
utils := newCnbBuildTestsUtils()
utils.FilesMock.CurrentDir = "/jenkins"
utils.FilesMock.AddDir("/jenkins")
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
Expand All @@ -500,13 +502,30 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":"dXNlcjpwYXNz"}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":"dXNlcjpwYXNz"}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
assert.EqualError(t, err, "failed to generate CNB_REGISTRY_AUTH: json: cannot unmarshal string into Go struct field ConfigFile.auths of type types.AuthConfig")
})

t.Run("error case: missing target registry in docker credentials", func(t *testing.T) {
t.Parallel()
config := cnbBuildOptions{
ContainerImageTag: "0.0.1",
ContainerRegistryURL: imageRegistry,
ContainerImageName: "my-image",
DockerConfigJSON: "/path/to/config.json",
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-other-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
assert.EqualError(t, err, "DockerConfigJSON does not contain credentials for target registry (some-registry)")
})

t.Run("error case: DockerConfigJSON file not there (config.json)", func(t *testing.T) {
t.Parallel()
config := cnbBuildOptions{
Expand Down Expand Up @@ -561,7 +580,7 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
Expand All @@ -583,9 +602,26 @@ func TestRunCnbBuild(t *testing.T) {
"runImage": "bar",
},
},
DockerConfigJSON: "/path/to/config.json",
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddDir("target")
utils.FilesMock.AddFile("target/project.toml", []byte(`[project]
id = "test"
name = "test"
version = "1.0.0"
[build]
include = []
exclude = ["*.tar"]
[[build.buildpacks]]
uri = "some-buildpack"`))
utils.FilesMock.AddFile("a_file", []byte(`{}`))
utils.FilesMock.AddFile("target/somelib.jar", []byte(`FFFFFF`))

addBuilderFiles(&utils)

telemetryData := &telemetry.CustomData{}
Expand All @@ -610,7 +646,7 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddDir("target")
utils.FilesMock.AddFile("target/app.jar", []byte(`FFFFFF`))
utils.FilesMock.AddFile("target/app-src.jar", []byte(`FFFFFF`))
Expand All @@ -637,7 +673,7 @@ func TestRunCnbBuild(t *testing.T) {
}

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddDir("target")
utils.FilesMock.AddFile("target/app.jar", []byte(`FFFFFF`))

Expand All @@ -648,7 +684,7 @@ func TestRunCnbBuild(t *testing.T) {

require.NoError(t, err)
runner := utils.ExecMockRunner
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"some-registry\":\"Basic dXNlcjpwYXNz\"}")
assert.Contains(t, runner.Calls[1].Params, fmt.Sprintf("%s/%s:%s", imageRegistry, config.ContainerImageName, config.ContainerImageTag))
assert.Equal(t, config.ContainerRegistryURL, commonPipelineEnvironment.container.registryURL)
assert.Equal(t, "my-image:3.1.5", commonPipelineEnvironment.container.imageNameTag)
Expand All @@ -668,7 +704,7 @@ func TestRunCnbBuild(t *testing.T) {
expectedImageCount := len(config.MultipleImages)

utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"some-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)

telemetryData := &telemetry.CustomData{}
Expand Down
46 changes: 41 additions & 5 deletions pkg/cnbutils/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,66 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"

"github.com/SAP/jenkins-library/pkg/log"
"github.com/docker/cli/cli/config/configfile"
)

func GenerateCnbAuth(config string, utils BuildUtils) (string, error) {
type Credentials struct {
utils BuildUtils
dockerConfig *configfile.ConfigFile
}

func NewCredentials(utils BuildUtils) Credentials {
return Credentials{
utils: utils,
dockerConfig: nil,
}
}

func (c *Credentials) GenerateCredentials(config string) (string, error) {
var err error
c.dockerConfig, err = c.parse(config)
if err != nil {
return "", err
}

return c.generate()
}

func (c *Credentials) Validate(target string) bool {
if c.dockerConfig == nil {
return false
}
_, ok := c.dockerConfig.AuthConfigs[target]
if !strings.HasPrefix(target, "localhost") && !ok {
return false
}
return true
}

func (c *Credentials) parse(config string) (*configfile.ConfigFile, error) {
dockerConfig := &configfile.ConfigFile{}

if config != "" {
log.Entry().Debugf("using docker config file %q", config)
dockerConfigJSON, err := utils.FileRead(config)
dockerConfigJSON, err := c.utils.FileRead(config)
if err != nil {
return "", err
return &configfile.ConfigFile{}, err
}

err = json.Unmarshal(dockerConfigJSON, dockerConfig)
if err != nil {
return "", err
return &configfile.ConfigFile{}, err
}
}
return dockerConfig, nil
}

func (c *Credentials) generate() (string, error) {
auth := map[string]string{}
for registry, value := range dockerConfig.AuthConfigs {
for registry, value := range c.dockerConfig.AuthConfigs {
if value.Auth == "" && value.Username == "" && value.Password == "" {
log.Entry().Warnf("docker config.json contains empty credentials for registry %q. Either 'auth' or 'username' and 'password' have to be provided.", registry)
continue
Expand Down
Loading

0 comments on commit df2e976

Please sign in to comment.