Allow alter trusted clone plugins and filter them via tag (#4074)

This commit is contained in:
6543 2024-09-01 20:41:10 +02:00 committed by GitHub
parent 8e0af15e85
commit 3c8204a0e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 151 additions and 58 deletions

View file

@ -41,6 +41,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix"
pipelineLog "go.woodpecker-ci.org/woodpecker/v2/pipeline/log"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
)
@ -185,7 +186,10 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
}
// lint the yaml file
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{{
err = linter.New(
linter.WithTrusted(true),
linter.WithTrustedClonePlugins(constant.TrustedClonePlugins),
).Lint([]*linter.WorkflowConfig{{
File: path.Base(file),
RawConfig: confStr,
Workflow: conf,

View file

@ -27,6 +27,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
// Command exports the info command.
@ -35,6 +36,14 @@ var Command = &cli.Command{
Usage: "lint a pipeline configuration file",
ArgsUsage: "[path/to/.woodpecker.yaml]",
Action: lint,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
Name: "plugins-trusted-clone",
Usage: "Plugins witch are trusted to handle the netrc info in clone steps",
Value: constant.TrustedClonePlugins,
},
},
}
func lint(ctx context.Context, c *cli.Command) error {
@ -69,7 +78,7 @@ func lintDir(ctx context.Context, c *cli.Command, dir string) error {
return nil
}
func lintFile(_ context.Context, _ *cli.Command, file string) error {
func lintFile(_ context.Context, c *cli.Command, file string) error {
fi, err := os.Open(file)
if err != nil {
return err
@ -83,7 +92,7 @@ func lintFile(_ context.Context, _ *cli.Command, file string) error {
rawConfig := string(buf)
c, err := yaml.ParseString(rawConfig)
parsedConfig, err := yaml.ParseString(rawConfig)
if err != nil {
return err
}
@ -91,11 +100,14 @@ func lintFile(_ context.Context, _ *cli.Command, file string) error {
config := &linter.WorkflowConfig{
File: path.Base(file),
RawConfig: rawConfig,
Workflow: c,
Workflow: parsedConfig,
}
// TODO: lint multiple files at once to allow checks for sth like "depends_on" to work
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{config})
err = linter.New(
linter.WithTrusted(true),
linter.WithTrustedClonePlugins(c.StringSlice("plugins-trusted-clone")),
).Lint([]*linter.WorkflowConfig{config})
if err != nil {
str, err := FormatLintError(config.File, err)

View file

@ -135,10 +135,11 @@ var flags = append([]cli.Flag{
Value: []string{"push", "pull_request"},
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_DEFAULT_CLONE_IMAGE"),
Name: "default-clone-image",
Sources: cli.EnvVars("WOODPECKER_DEFAULT_CLONE_PLUGIN", "WOODPECKER_DEFAULT_CLONE_IMAGE"),
Name: "default-clone-plugin",
Aliases: []string{"default-clone-image"},
Usage: "The default docker image to be used when cloning the repo",
Value: constant.DefaultCloneImage,
Value: constant.DefaultClonePlugin,
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_DEFAULT_PIPELINE_TIMEOUT"),
@ -164,6 +165,12 @@ var flags = append([]cli.Flag{
Usage: "Allow plugins to run in privileged mode, if environment variable is defined but empty there will be none",
Value: constant.PrivilegedPlugins,
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
Name: "plugins-trusted-clone",
Usage: "Plugins witch are trusted to handle the netrc info in clone steps",
Value: constant.TrustedClonePlugins,
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_VOLUME"),
Name: "volume",

View file

@ -43,7 +43,6 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
const (
@ -165,8 +164,9 @@ func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) error
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
// Cloning
server.Config.Pipeline.DefaultCloneImage = c.String("default-clone-image")
constant.TrustedCloneImages = append(constant.TrustedCloneImages, server.Config.Pipeline.DefaultCloneImage)
server.Config.Pipeline.DefaultClonePlugin = c.String("default-clone-plugin")
server.Config.Pipeline.TrustedClonePlugins = c.StringSlice("plugins-trusted-clone")
server.Config.Pipeline.TrustedClonePlugins = append(server.Config.Pipeline.TrustedClonePlugins, server.Config.Pipeline.DefaultClonePlugin)
// Execution
_events := c.StringSlice("default-cancel-previous-pipeline-events")

View file

@ -319,11 +319,13 @@ Always use authentication to clone repositories even if they are public. Needed
List of event names that will be canceled when a new pipeline for the same context (tag, branch) is created.
### `WOODPECKER_DEFAULT_CLONE_IMAGE`
### `WOODPECKER_DEFAULT_CLONE_PLUGIN`
> Default is defined in [shared/constant/constant.go](https://github.com/woodpecker-ci/woodpecker/blob/main/shared/constant/constant.go)
The default docker image to be used when cloning the repo
The default docker image to be used when cloning the repo.
It is also added to the trusted clone plugin list.
### `WOODPECKER_DEFAULT_PIPELINE_TIMEOUT`
@ -352,6 +354,15 @@ a user can log into Woodpecker, without re-authentication.
Docker images to run in privileged mode. Only change if you are sure what you do!
### WOODPECKER_PLUGINS_TRUSTED_CLONE
> Defaults are defined in [shared/constant/constant.go](https://github.com/woodpecker-ci/woodpecker/blob/main/shared/constant/constant.go)
Plugins witch are trusted to handle the netrc info in clone steps.
If a clone step use an image not in this list, the netrc will not be injected and an user has to use other methods (e.g. secrets) to clone non public repos.
You should specify the tag of your images too, as this enforces exact matches.
<!--
### `WOODPECKER_VOLUME`
> Default: empty

View file

@ -4,6 +4,8 @@ Some versions need some changes to the server configuration or the pipeline conf
## `next`
- `WOODPECKER_DEFAULT_CLONE_IMAGE` got depricated use `WOODPECKER_DEFAULT_CLONE_PLUGIN`
- Check trusted-clone-plugins by image name and tag (if tag is set)
- Remove `plugins/docker`, `plugins/gcr` and `plugins/ecr` from the default list of privileged plugins ([modify the list via config if needed](./30-administration/10-server-config.md#woodpecker_escalate)).
- Secret filters for plugins now check against tag if specified
- Removed `WOODPECKER_DEV_OAUTH_HOST` and `WOODPECKER_DEV_GITEA_OAUTH_URL` use `WOODPECKER_EXPERT_FORGE_OAUTH_HOST`

View file

@ -105,7 +105,8 @@ type Compiler struct {
registries []Registry
secrets map[string]Secret
reslimit ResourceLimit
defaultCloneImage string
defaultClonePlugin string
trustedClonePlugins []string
trustedPipeline bool
netrcOnlyTrusted bool
}
@ -116,6 +117,8 @@ func New(opts ...Option) *Compiler {
env: map[string]string{},
cloneEnv: map[string]string{},
secrets: map[string]Secret{},
defaultClonePlugin: constant.DefaultClonePlugin,
trustedClonePlugins: constant.TrustedClonePlugins,
}
for _, opt := range opts {
opt(compiler)
@ -163,20 +166,15 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
c.workspacePath = path.Clean(conf.Workspace.Path)
}
cloneImage := constant.DefaultCloneImage
if len(c.defaultCloneImage) > 0 {
cloneImage = c.defaultCloneImage
}
// add default clone step
if !c.local && len(conf.Clone.ContainerList) == 0 && !conf.SkipClone {
if !c.local && len(conf.Clone.ContainerList) == 0 && !conf.SkipClone && len(c.defaultClonePlugin) != 0 {
cloneSettings := map[string]any{"depth": "0"}
if c.metadata.Curr.Event == metadata.EventTag {
cloneSettings["tags"] = "true"
}
container := &yaml_types.Container{
Name: defaultCloneName,
Image: cloneImage,
Image: c.defaultClonePlugin,
Settings: cloneSettings,
Environment: make(map[string]any),
}
@ -208,7 +206,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
}
// only inject netrc if it's a trusted repo or a trusted plugin
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv {
step.Environment[k] = v
}
@ -265,7 +263,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
}
// inject netrc if it's a trusted repo or a trusted clone-plugin
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv {
step.Environment[k] = v
}

View file

@ -92,7 +92,7 @@ func TestCompilerCompile(t *testing.T) {
Steps: []*backend_types.Step{{
Name: "clone",
Type: backend_types.StepTypeClone,
Image: constant.DefaultCloneImage,
Image: constant.DefaultClonePlugin,
OnSuccess: true,
Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},

View file

@ -172,9 +172,15 @@ func WithResourceLimit(swap, mem, shmSize, cpuQuota, cpuShares int64, cpuSet str
}
}
func WithDefaultCloneImage(cloneImage string) Option {
func WithDefaultClonePlugin(cloneImage string) Option {
return func(compiler *Compiler) {
compiler.defaultCloneImage = cloneImage
compiler.defaultClonePlugin = cloneImage
}
}
func WithTrustedClonePlugins(images []string) Option {
return func(compiler *Compiler) {
compiler.trustedClonePlugins = images
}
}

View file

@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
func TestWithWorkspace(t *testing.T) {
@ -166,9 +167,17 @@ func TestWithEnviron(t *testing.T) {
assert.Equal(t, "true", compiler.env["SHOW"])
}
func TestWithDefaultCloneImage(t *testing.T) {
func TestDefaultClonePlugin(t *testing.T) {
compiler := New(
WithDefaultCloneImage("not-an-image"),
WithDefaultClonePlugin("not-an-image"),
)
assert.Equal(t, "not-an-image", compiler.defaultCloneImage)
assert.Equal(t, "not-an-image", compiler.defaultClonePlugin)
}
func TestWithTrustedClonePlugins(t *testing.T) {
compiler := New(WithTrustedClonePlugins([]string{"not-an-image"}))
assert.ElementsMatch(t, []string{"not-an-image"}, compiler.trustedClonePlugins)
compiler = New()
assert.ElementsMatch(t, constant.TrustedClonePlugins, compiler.trustedClonePlugins)
}

View file

@ -25,12 +25,14 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter/schema"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
// A Linter lints a pipeline configuration.
type Linter struct {
trusted bool
privilegedPlugins *[]string
trustedClonePlugins *[]string
}
// New creates a new Linter with options.
@ -73,6 +75,10 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing steps section", config.File, "steps", false))
}
if err := l.lintCloneSteps(config); err != nil {
linterErr = multierr.Append(linterErr, err)
}
if err := l.lintContainers(config, "clone"); err != nil {
linterErr = multierr.Append(linterErr, err)
}
@ -96,6 +102,29 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
return linterErr
}
func (l *Linter) lintCloneSteps(config *WorkflowConfig) error {
if len(config.Workflow.Clone.ContainerList) == 0 {
return nil
}
trustedClonePlugins := constant.TrustedClonePlugins
if l.trustedClonePlugins != nil {
trustedClonePlugins = *l.trustedClonePlugins
}
var linterErr error
for _, container := range config.Workflow.Clone.ContainerList {
if !utils.MatchImageDynamic(container.Image, trustedClonePlugins...) {
linterErr = multierr.Append(linterErr,
newLinterError(
"Specified clone image does not match allow list, netrc will not be injected",
config.File, fmt.Sprintf("clone.%s", container.Name), true),
)
}
}
return linterErr
}
func (l *Linter) lintContainers(config *WorkflowConfig, area string) error {
var linterErr error

View file

@ -173,6 +173,10 @@ func TestLintErrors(t *testing.T) {
from: "{steps: { build: { image: plugins/docker, settings: { test: 'true' } } }, when: { branch: main, event: push } } }",
want: "Cannot use once privileged plugins removed from WOODPECKER_ESCALATE, use 'woodpeckerci/plugin-docker-buildx' instead",
},
{
from: "{steps: { build: { image: golang, settings: { test: 'true' } } }, when: { branch: main, event: push }, clone: { git: { image: some-other/plugin-git:v1.1.0 } } }",
want: "Specified clone image does not match allow list, netrc will not be injected",
},
}
for _, test := range testdata {

View file

@ -30,3 +30,10 @@ func PrivilegedPlugins(plugins []string) Option {
linter.privilegedPlugins = &plugins
}
}
// WithTrustedClonePlugins adds the list of trusted clone plugins.
func WithTrustedClonePlugins(plugins []string) Option {
return func(linter *Linter) {
linter.trustedClonePlugins = &plugins
}
}

View file

@ -22,7 +22,6 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/constraint"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types/base"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)
type (
@ -125,6 +124,6 @@ func (c *Container) IsPlugin() bool {
len(c.Secrets) == 0
}
func (c *Container) IsTrustedCloneImage() bool {
return c.IsPlugin() && utils.MatchImage(c.Image, constant.TrustedCloneImages...)
func (c *Container) IsTrustedCloneImage(trustedClonePlugins []string) bool {
return c.IsPlugin() && utils.MatchImageDynamic(c.Image, trustedClonePlugins...)
}

View file

@ -64,7 +64,8 @@ var Config = struct {
Pipeline struct {
AuthenticatePublicRepos bool
DefaultCancelPreviousPipelineEvents []model.WebhookEvent
DefaultCloneImage string
DefaultClonePlugin string
TrustedClonePlugins []string
Limits model.ResourceLimit
Volumes []string
Networks []string

View file

@ -143,6 +143,7 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A
errorsAndWarnings = multierr.Append(errorsAndWarnings, linter.New(
linter.WithTrusted(b.Repo.IsTrusted),
linter.PrivilegedPlugins(server.Config.Pipeline.PrivilegedPlugins),
linter.WithTrustedClonePlugins(server.Config.Pipeline.TrustedClonePlugins),
).Lint([]*linter.WorkflowConfig{{
Workflow: parsed,
File: workflow.Name,
@ -281,7 +282,8 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
),
b.Repo.IsSCMPrivate || server.Config.Pipeline.AuthenticatePublicRepos,
),
compiler.WithDefaultCloneImage(server.Config.Pipeline.DefaultCloneImage),
compiler.WithDefaultClonePlugin(server.Config.Pipeline.DefaultClonePlugin),
compiler.WithTrustedClonePlugins(server.Config.Pipeline.TrustedClonePlugins),
compiler.WithRegistry(registries...),
compiler.WithSecret(secrets...),
compiler.WithPrefix(

View file

@ -29,12 +29,14 @@ var DefaultConfigOrder = [...]string{
}
const (
// DefaultCloneImage can be changed by 'WOODPECKER_DEFAULT_CLONE_IMAGE' at runtime.
// DefaultClonePlugin can be changed by 'WOODPECKER_DEFAULT_CLONE_PLUGIN' at runtime.
// renovate: datasource=docker depName=woodpeckerci/plugin-git
DefaultCloneImage = "docker.io/woodpeckerci/plugin-git:2.5.2"
DefaultClonePlugin = "docker.io/woodpeckerci/plugin-git:2.5.2"
)
var TrustedCloneImages = []string{
DefaultCloneImage,
// TrustedClonePlugins can be changed by 'WOODPECKER_PLUGINS_TRUSTED_CLONE' at runtime.
var TrustedClonePlugins = []string{
DefaultClonePlugin,
"docker.io/woodpeckerci/plugin-git",
"quay.io/woodpeckerci/plugin-git",
}