Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to filter secrets by plugins with specific tags #4069

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/docs/20-usage/40-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/docs/91-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pipeline/frontend/yaml/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
29 changes: 28 additions & 1 deletion pipeline/frontend/yaml/utils/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
105 changes: 105 additions & 0 deletions pipeline/frontend/yaml/utils/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down