diff --git a/executor/linux/build.go b/executor/linux/build.go index f57656a9..e4193e05 100644 --- a/executor/linux/build.go +++ b/executor/linux/build.go @@ -14,7 +14,6 @@ import ( "golang.org/x/sync/errgroup" "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" "github.com/go-vela/worker/internal/build" context2 "github.com/go-vela/worker/internal/context" "github.com/go-vela/worker/internal/image" @@ -45,74 +44,6 @@ func (c *client) CreateBuild(ctx context.Context) error { return fmt.Errorf("unable to upload build state: %w", c.err) } - // before setting up the build, enforce repo.trusted is set for pipelines containing privileged images - // this configuration is set as an executor flag - if c.enforceTrustedRepos { - // check if pipeline steps contain privileged images - // assume no privileged images are in use - containsPrivilegedImages := false - - // group steps services and stages together - containers := c.pipeline.Steps - - containers = append(containers, c.pipeline.Services...) - for _, stage := range c.pipeline.Stages { - containers = append(containers, stage.Steps...) - } - - for _, container := range containers { - // TODO: remove hardcoded reference - if container.Image == "#init" { - continue - } - - for _, pattern := range c.privilegedImages { - privileged, err := image.IsPrivilegedImage(container.Image, pattern) - if err != nil { - return fmt.Errorf("could not verify if image %s is privileged", container.Image) - } - - if privileged { - containsPrivilegedImages = true - } - } - } - - // check if this build should be denied - if (containsPrivilegedImages) && !(c.repo != nil && c.repo.GetTrusted()) { - // deny the build, clean build/steps, and return error - // populate the build error - e := "build denied, repo must be trusted in order to run privileged images" - c.build.SetError(e) - // set the build status to error - c.build.SetStatus(constants.StatusError) - - steps := c.pipeline.Steps - for _, stage := range c.pipeline.Stages { - steps = append(containers, stage.Steps...) - } - - // update all preconfigured steps to the correct status - for _, s := range steps { - // extract step - step := library.StepFromBuildContainer(c.build, s) - // status to use for preconfigured steps that are not ran - status := constants.StatusKilled - // set step status - step.SetStatus(status) - // send API call to update the step - //nolint:contextcheck // ignore passing context - _, _, err := c.Vela.Step.Update(c.repo.GetOrg(), c.repo.GetName(), c.build.GetNumber(), step) - if err != nil { - // only log any step update errors to allow the return err to run - c.Logger.Errorf("unable to update step %s to status %s: %s", s.Name, status, err.Error()) - } - } - - return fmt.Errorf("build containing privileged images %s/%d denied, repo is not trusted", c.repo.GetFullName(), c.build.GetNumber()) - } - } - // setup the runtime build c.err = c.Runtime.SetupBuild(ctx, c.pipeline) if c.err != nil { @@ -262,7 +193,7 @@ func (c *client) PlanBuild(ctx context.Context) error { // AssembleBuild prepares the containers within a build for execution. // -//nolint:funlen // ignore function length due to comments and logging messages +//nolint:gocyclo,funlen // ignore cyclomatic complexity and function length due to comments and logging messages func (c *client) AssembleBuild(ctx context.Context) error { // defer taking a snapshot of the build // @@ -419,6 +350,88 @@ func (c *client) AssembleBuild(ctx context.Context) error { // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.AppendData _log.AppendData(image) } + // enforce repo.trusted is set for pipelines containing privileged images + // if not enforced, allow all that exist in the list of runtime privileged images + // this configuration is set as an executor flag + if c.enforceTrustedRepos { + // group steps services stages and secret origins together + containers := c.pipeline.Steps + + containers = append(containers, c.pipeline.Services...) + + for _, stage := range c.pipeline.Stages { + containers = append(containers, stage.Steps...) + } + + for _, secret := range c.pipeline.Secrets { + containers = append(containers, secret.Origin) + } + + // assume no privileged images are in use + containsPrivilegedImages := false + + // verify all pipeline containers + for _, container := range containers { + // TODO: remove hardcoded reference + if container.Image == "#init" { + continue + } + + // skip over non-plugin secrets origins + if container.Empty() { + continue + } + + c.Logger.Infof("verifying privileges for container %s", container.Name) + + // update the init log with image info + // + // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.AppendData + _log.AppendData([]byte(fmt.Sprintf("Verifying privileges for image %s...\n", container.Image))) + + for _, pattern := range c.privilegedImages { + // check if image matches privileged pattern + privileged, err := image.IsPrivilegedImage(container.Image, pattern) + if err != nil { + // wrap the error + c.err = fmt.Errorf("unable to verify privileges for image %s: %w", container.Image, err) + + // update the init log with image info + // + // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.AppendData + _log.AppendData([]byte(fmt.Sprintf("ERROR: %s\n", c.err.Error()))) + + // return error and destroy the build + // ignore checking more images + return c.err + } + + if privileged { + // pipeline contains at least one privileged image + containsPrivilegedImages = privileged + } + } + + // update the init log with image info + // + // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.AppendData + _log.AppendData([]byte(fmt.Sprintf("Privileges verified for image %s\n", container.Image))) + } + + // ensure pipelines containing privileged images are only permitted to run by trusted repos + if (containsPrivilegedImages) && !(c.repo != nil && c.repo.GetTrusted()) { + // update error + c.err = fmt.Errorf("unable to assemble build. pipeline contains privileged images and repo is not trusted") + + // update the init log with image info + // + // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.AppendData + _log.AppendData([]byte(fmt.Sprintf("ERROR: %s\n", c.err.Error()))) + + // return error and destroy the build + return c.err + } + } // inspect the runtime build (eg a kubernetes pod) for the pipeline buildOutput, err := c.Runtime.InspectBuild(ctx, c.pipeline) diff --git a/executor/linux/build_test.go b/executor/linux/build_test.go index 97a99136..ade7766d 100644 --- a/executor/linux/build_test.go +++ b/executor/linux/build_test.go @@ -130,11 +130,16 @@ func TestLinux_CreateBuild(t *testing.T) { } } -func TestLinux_CreateBuild_EnforceTrustedRepos(t *testing.T) { +func TestLinux_AssembleBuild_EnforceTrustedRepos(t *testing.T) { // setup types compiler, _ := native.New(cli.NewContext(nil, flag.NewFlagSet("test", 0), nil)) _build := testBuild() + + // setting mock build for testing dynamic environment tags + _buildWithMessageAlpine := testBuild() + _buildWithMessageAlpine.SetMessage("alpine") + // test repo is not trusted by default _untrustedRepo := testRepo() _user := testUser() @@ -145,7 +150,6 @@ func TestLinux_CreateBuild_EnforceTrustedRepos(t *testing.T) { _privilegedImagesServicesPipeline := []string{"postgres"} // to be matched with the image used by testdata/build/stages/basic.yml _privilegedImagesStagesPipeline := []string{"alpine"} - // create trusted repo _trustedRepo := testRepo() _trustedRepo.SetTrusted(true) @@ -245,7 +249,78 @@ func TestLinux_CreateBuild_EnforceTrustedRepos(t *testing.T) { privilegedImages: []string{}, // this matches the image from test.pipeline enforceTrustedRepos: false, }, - + { + name: "enforce trusted repos enabled: privileged steps pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: privileged steps pipeline with untrusted repo and dynamic image:tag", + failure: true, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: non-privileged steps pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: non-privileged steps pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos disabled: privileged steps pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: privileged steps pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: non-privileged steps pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: non-privileged steps pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/steps/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, { name: "enforce trusted repos enabled: privileged services pipeline with trusted repo", failure: false, @@ -318,6 +393,78 @@ func TestLinux_CreateBuild_EnforceTrustedRepos(t *testing.T) { privilegedImages: []string{}, // this matches the image from test.pipeline enforceTrustedRepos: false, }, + { + name: "enforce trusted repos enabled: privileged services pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: privileged services pipeline with untrusted repo and dynamic image:tag", + failure: true, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: non-privileged services pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: non-privileged services pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos disabled: privileged services pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: privileged services pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: non-privileged services pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: non-privileged services pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/services/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, { name: "enforce trusted repos enabled: privileged stages pipeline with trusted repo", failure: false, @@ -390,6 +537,78 @@ func TestLinux_CreateBuild_EnforceTrustedRepos(t *testing.T) { privilegedImages: []string{}, // this matches the image from test.pipeline enforceTrustedRepos: false, }, + { + name: "enforce trusted repos enabled: privileged stages pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: privileged stages pipeline with untrusted repo and dynamic image:tag", + failure: true, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: non-privileged stages pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos enabled: non-privileged stages pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: true, + }, + { + name: "enforce trusted repos disabled: privileged stages pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: privileged stages pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: non-privileged stages pipeline with trusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _trustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, + { + name: "enforce trusted repos disabled: non-privileged stages pipeline with untrusted repo and dynamic image:tag", + failure: false, + build: _buildWithMessageAlpine, + repo: _untrustedRepo, + pipeline: "testdata/build/stages/img_environmentdynamic.yml", + privilegedImages: []string{}, // this matches the image from test.pipeline + enforceTrustedRepos: false, + }, { name: "enforce trusted repos enabled: privileged steps pipeline with trusted repo and init step name", failure: false, @@ -637,17 +856,26 @@ func TestLinux_CreateBuild_EnforceTrustedRepos(t *testing.T) { } err = _engine.CreateBuild(context.Background()) + if err != nil { + t.Errorf("CreateBuild returned err: %v", err) + } + + // override mock handler PUT build update + // used for dynamic substitute testing + _engine.build.SetMessage(test.build.GetMessage()) + + err = _engine.AssembleBuild(context.Background()) if test.failure { if err == nil { - t.Errorf("CreateBuild should have returned err") + t.Errorf("AssembleBuild should have returned err") } return // continue to next test } if err != nil { - t.Errorf("CreateBuild returned err: %v", err) + t.Errorf("AssembleBuild returned err: %v", err) } }) } diff --git a/executor/linux/testdata/build/services/img_environmentdynamic.yml b/executor/linux/testdata/build/services/img_environmentdynamic.yml new file mode 100644 index 00000000..92cf36cb --- /dev/null +++ b/executor/linux/testdata/build/services/img_environmentdynamic.yml @@ -0,0 +1,22 @@ +--- +version: "1" + +environment: + DYNAMIC_IMAGE: "postgres" + DYNAMIC_TAG: "latest" + +services: + - name: test + environment: + FOO: bar + image: "${DYNAMIC_IMAGE}:${DYNAMIC_TAG}" + pull: on_start + +steps: + - name: test + commands: + - echo ${FOO} + environment: + FOO: bar + image: alpine:latest + pull: true \ No newline at end of file diff --git a/executor/linux/testdata/build/stages/img_environmentdynamic.yml b/executor/linux/testdata/build/stages/img_environmentdynamic.yml new file mode 100644 index 00000000..6306a5f9 --- /dev/null +++ b/executor/linux/testdata/build/stages/img_environmentdynamic.yml @@ -0,0 +1,17 @@ +--- +version: "1" + +environment: + DYNAMIC_IMAGE: "${VELA_BUILD_MESSAGE}" + DYNAMIC_TAG: "${VELA_BUILD_NUMBER}" + +stages: + test: + steps: + - name: test + commands: + - echo ${FOO} + environment: + FOO: bar + image: "${DYNAMIC_IMAGE}:${DYNAMIC_TAG}" + pull: on_start diff --git a/executor/linux/testdata/build/steps/img_environmentdynamic.yml b/executor/linux/testdata/build/steps/img_environmentdynamic.yml new file mode 100644 index 00000000..3d947ab5 --- /dev/null +++ b/executor/linux/testdata/build/steps/img_environmentdynamic.yml @@ -0,0 +1,15 @@ +--- +version: "1" + +environment: + DYNAMIC_IMAGE: "${VELA_BUILD_MESSAGE}" + DYNAMIC_TAG: "${VELA_BUILD_NUMBER}" + +steps: + - name: test + commands: + - echo ${FOO} + environment: + FOO: bar + image: "${DYNAMIC_IMAGE}:${DYNAMIC_TAG}" + pull: on_start