diff --git a/docs/docs/20-usage/40-secrets.md b/docs/docs/20-usage/40-secrets.md index e87a04384..aadb7734d 100644 --- a/docs/docs/20-usage/40-secrets.md +++ b/docs/docs/20-usage/40-secrets.md @@ -74,6 +74,11 @@ Please be careful when exposing secrets to pull requests. If your repository is To prevent abusing your secrets from malicious usage, you can limit a secret to a list of plugins. If enabled they are not available to any other plugin (steps without user-defined commands). If you or an attacker defines explicit commands, the secrets will not be available to the container to prevent leaking them. +:::note +If you specify a tag, the filter will respect it. +Just make sure you don't specify the same image without one, otherwise it will be ignored again. +::: + ![plugins filter](./secrets-plugins-filter.png) ## Adding Secrets diff --git a/docs/docs/91-migrations.md b/docs/docs/91-migrations.md index db82b83b8..53c1c5814 100644 --- a/docs/docs/91-migrations.md +++ b/docs/docs/91-migrations.md @@ -4,6 +4,7 @@ Some versions need some changes to the server configuration or the pipeline conf ## `next` +- 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` - Compatibility mode of deprecated `pipeline:`, `platform:` and `branches:` pipeline config options are now removed and pipeline will now fail if still in use. - Removed `steps.[name].group` in favor of `steps.[name].depends_on` (see [workflow syntax](./20-usage/20-workflow-syntax.md#depends_on) to learn how to set dependencies) diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go index 9b9c2d7d4..281e1c7fa 100644 --- a/pipeline/frontend/yaml/compiler/compiler.go +++ b/pipeline/frontend/yaml/compiler/compiler.go @@ -49,7 +49,7 @@ func (s *Secret) Available(event string, container *yaml_types.Container) error return fmt.Errorf("secret %q only allowed to be used by plugins by step %q", s.Name, container.Name) } - if onlyAllowSecretForPlugins && !utils.MatchImage(container.Image, s.AllowedPlugins...) { + if onlyAllowSecretForPlugins && !utils.MatchImageDynamic(container.Image, s.AllowedPlugins...) { return fmt.Errorf("secret %q is not allowed to be used with image %q by step %q", s.Name, container.Image, container.Name) } diff --git a/pipeline/frontend/yaml/utils/image.go b/pipeline/frontend/yaml/utils/image.go index 63054c21d..3462a58f1 100644 --- a/pipeline/frontend/yaml/utils/image.go +++ b/pipeline/frontend/yaml/utils/image.go @@ -14,7 +14,11 @@ package utils -import "github.com/distribution/reference" +import ( + "strings" + + "github.com/distribution/reference" +) // trimImage returns the short image name without tag. func trimImage(name string) string { @@ -57,6 +61,29 @@ func MatchImage(from string, to ...string) bool { return false } +// MatchImageDynamic check if image is in list based on list. +// If an list entry has a tag specified it only will match if both are the same, else the tag is ignored. +func MatchImageDynamic(from string, to ...string) bool { + fullFrom := expandImage(from) + trimFrom := trimImage(from) + for _, match := range to { + if imageHasTag(match) { + if fullFrom == expandImage(match) { + return true + } + } else { + if trimFrom == trimImage(match) { + return true + } + } + } + return false +} + +func imageHasTag(name string) bool { + return strings.Contains(name, ":") +} + // MatchHostname returns true if the image hostname // matches the specified hostname. func MatchHostname(image, hostname string) bool { diff --git a/pipeline/frontend/yaml/utils/image_test.go b/pipeline/frontend/yaml/utils/image_test.go index 17fab45d5..1d3a6de90 100644 --- a/pipeline/frontend/yaml/utils/image_test.go +++ b/pipeline/frontend/yaml/utils/image_test.go @@ -113,6 +113,10 @@ func Test_expandImage(t *testing.T) { from: "gcr.io/golang:1.0.0", want: "gcr.io/golang:1.0.0", }, + { + from: "codeberg.org/6543/hello:latest@2c98dce11f78c2b4e40f513ca82f75035eb8cfa4957a6d8eb3f917ecaf77803", + want: "codeberg.org/6543/hello:latest@2c98dce11f78c2b4e40f513ca82f75035eb8cfa4957a6d8eb3f917ecaf77803", + }, // error cases, return input unmodified { from: "foo/bar?baz:boo", @@ -124,6 +128,57 @@ func Test_expandImage(t *testing.T) { } } +func Test_imageHasTag(t *testing.T) { + testdata := []struct { + from string + want bool + }{ + { + from: "golang", + want: false, + }, + { + from: "golang:latest", + want: true, + }, + { + from: "golang:1.0.0", + want: true, + }, + { + from: "library/golang", + want: false, + }, + { + from: "library/golang:latest", + want: true, + }, + { + from: "library/golang:1.0.0", + want: true, + }, + { + from: "index.docker.io/library/golang:1.0.0", + want: true, + }, + { + from: "gcr.io/golang", + want: false, + }, + { + from: "gcr.io/golang:1.0.0", + want: true, + }, + { + from: "codeberg.org/6543/hello:latest@2c98dce11f78c2b4e40f513ca82f75035eb8cfa4957a6d8eb3f917ecaf77803", + want: true, + }, + } + for _, test := range testdata { + assert.Equal(t, test.want, imageHasTag(test.from)) + } +} + func Test_matchImage(t *testing.T) { testdata := []struct { from, to string @@ -205,6 +260,56 @@ func Test_matchImage(t *testing.T) { } } +func Test_matchImageDynamic(t *testing.T) { + testdata := []struct { + name, from string + to []string + want bool + }{ + { + name: "simple compare", + from: "golang", + to: []string{"golang"}, + want: true, + }, + { + name: "compare non-taged image whit list who tag requirement", + from: "golang", + to: []string{"golang:v3.0"}, + want: false, + }, + { + name: "compare taged image whit list who tag no requirement", + from: "golang:v3.0", + to: []string{"golang"}, + want: true, + }, + { + name: "compare taged image whit list who has image with no tag requirement", + from: "golang:1.0", + to: []string{"golang", "golang:2.0"}, + want: true, + }, + { + name: "compare taged image whit list who only has images with tag requirement", + from: "golang:1.0", + to: []string{"golang:latest", "golang:2.0"}, + want: false, + }, + { + name: "compare taged image whit list who only has images with tag requirement", + from: "golang:1.0", + to: []string{"golang:latest", "golang:1.0"}, + want: true, + }, + } + for _, test := range testdata { + if !assert.Equal(t, test.want, MatchImageDynamic(test.from, test.to...)) { + t.Logf("test data: '%s' -> '%s'", test.from, test.to) + } + } +} + func Test_matchHostname(t *testing.T) { testdata := []struct { image, hostname string