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

Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com>
This commit is contained in:
6543 2024-08-31 13:46:50 +02:00 committed by GitHub
parent 99d169bd3f
commit fb6068d836
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 140 additions and 2 deletions

View file

@ -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. 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) ![plugins filter](./secrets-plugins-filter.png)
## Adding Secrets ## Adding Secrets

View file

@ -4,6 +4,7 @@ Some versions need some changes to the server configuration or the pipeline conf
## `next` ## `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` - 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. - 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) - 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)

View file

@ -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) 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) return fmt.Errorf("secret %q is not allowed to be used with image %q by step %q", s.Name, container.Image, container.Name)
} }

View file

@ -14,7 +14,11 @@
package utils package utils
import "github.com/distribution/reference" import (
"strings"
"github.com/distribution/reference"
)
// trimImage returns the short image name without tag. // trimImage returns the short image name without tag.
func trimImage(name string) string { func trimImage(name string) string {
@ -57,6 +61,29 @@ func MatchImage(from string, to ...string) bool {
return false 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 // MatchHostname returns true if the image hostname
// matches the specified hostname. // matches the specified hostname.
func MatchHostname(image, hostname string) bool { func MatchHostname(image, hostname string) bool {

View file

@ -113,6 +113,10 @@ func Test_expandImage(t *testing.T) {
from: "gcr.io/golang:1.0.0", from: "gcr.io/golang:1.0.0",
want: "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 // error cases, return input unmodified
{ {
from: "foo/bar?baz:boo", 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) { func Test_matchImage(t *testing.T) {
testdata := []struct { testdata := []struct {
from, to string 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) { func Test_matchHostname(t *testing.T) {
testdata := []struct { testdata := []struct {
image, hostname string image, hostname string