Add platform filtering support to mapping.yml (#167)

* chore!: rename package config -> mapping
* feat: add platform filtering support to mapping.yml
This commit is contained in:
James Carnegie
2024-09-18 21:11:55 +01:00
committed by GitHub
parent 05caa959c4
commit 4a70e5ae36
26 changed files with 421 additions and 253 deletions

View File

@@ -1,65 +0,0 @@
package policy
import (
"fmt"
"github.com/docker/attest/config"
)
type matchType string
const (
matchTypePolicy matchType = "policy"
matchTypeMatchNoPolicy matchType = "match_no_policy"
matchTypeNoMatch matchType = "no_match"
)
type policyMatch struct {
matchType matchType
policy *config.PolicyMapping
rule *config.PolicyRule
matchedName string
}
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
if mappings == nil {
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
}
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
for _, rule := range mappings.Rules {
if rule.Pattern.MatchString(imageName) {
switch {
case rule.PolicyID == "" && rule.Replacement == "":
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
case rule.PolicyID != "" && rule.Replacement != "":
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
case rule.PolicyID != "":
policy := mappings.Policies[rule.PolicyID]
if policy != nil {
return &policyMatch{
matchType: matchTypePolicy,
policy: policy,
rule: rule,
matchedName: imageName,
}, nil
}
return &policyMatch{
matchType: matchTypeMatchNoPolicy,
rule: rule,
matchedName: imageName,
}, nil
case rule.Replacement != "":
if matched[rule] {
return nil, fmt.Errorf("rewrite loop detected")
}
matched[rule] = true
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
return findPolicyMatchImpl(imageName, mappings, matched)
}
}
}
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}

View File

@@ -1,112 +0,0 @@
package policy
import (
"path/filepath"
"testing"
"github.com/docker/attest/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindPolicyMatch(t *testing.T) {
testCases := []struct {
name string
imageName string
mappingDir string
expectError bool
expectLoadingError bool
expectedMatchType matchType
expectedPolicyID string
expectedImageName string
}{
{
name: "alpine",
mappingDir: "doi",
imageName: "docker.io/library/alpine",
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "no match",
mappingDir: "doi",
imageName: "docker.io/something/else",
expectedMatchType: matchTypeNoMatch,
expectedImageName: "docker.io/something/else",
},
{
name: "match, no policy",
mappingDir: "local",
imageName: "docker.io/library/alpine",
expectedMatchType: matchTypeMatchNoPolicy,
expectedImageName: "docker.io/library/alpine",
},
{
name: "simple rewrite",
mappingDir: "simple-rewrite",
imageName: "mycoolmirror.org/library/alpine",
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "rewrite no match",
mappingDir: "rewrite-to-no-match",
imageName: "mycoolmirror.org/library/alpine",
expectedMatchType: matchTypeNoMatch,
expectedImageName: "badredirect.org/alpine",
},
{
name: "rewrite to match, no policy",
mappingDir: "rewrite-to-local",
imageName: "mycoolmirror.org/library/alpine",
expectedMatchType: matchTypeMatchNoPolicy,
expectedImageName: "docker.io/library/alpine",
},
{
name: "multiple rewrites",
mappingDir: "rewrite-multiple",
imageName: "myevencoolermirror.org/library/alpine",
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "rewrite loop",
mappingDir: "rewrite-loop",
imageName: "yin/alpine",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mappings", tc.mappingDir))
require.NoError(t, err)
match, err := findPolicyMatch(tc.imageName, mappings)
if tc.expectError {
require.Error(t, err)
// TODO: check error matches expected error message
return
}
require.NoError(t, err)
assert.Equal(t, tc.expectedMatchType, match.matchType)
if match.matchType == matchTypePolicy {
if assert.NotNil(t, match.policy) {
assert.Equal(t, tc.expectedPolicyID, match.policy.ID)
}
}
assert.Equal(t, tc.expectedImageName, match.matchedName)
})
}
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/attest/attestation"
"github.com/docker/attest/config"
"github.com/docker/attest/mapping"
"github.com/docker/attest/oci"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/package-url/packageurl-go"
@@ -22,9 +22,9 @@ func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsRes
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
}
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) {
if mapping.Attestations != nil {
if mapping.Attestations.Style == config.AttestationStyleAttached {
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, policyMapping *mapping.PolicyMapping) (attestation.Resolver, error) {
if policyMapping.Attestations != nil {
if policyMapping.Attestations.Style == mapping.AttestationStyleAttached {
switch resolver := resolver.(type) {
case *oci.RegistryImageDetailsResolver:
return attestation.NewRegistryResolver(resolver)
@@ -34,8 +34,8 @@ func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *confi
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
}
}
if mapping.Attestations.Repo != "" {
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
if policyMapping.Attestations.Repo != "" {
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(policyMapping.Attestations.Repo))
}
}
return attestation.NewReferrersResolver(resolver)

View File

@@ -8,8 +8,8 @@ import (
"testing"
"github.com/docker/attest/attestation"
"github.com/docker/attest/config"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/mapping"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -45,7 +45,8 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
defaultResolver := attestation.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
}
defaultPlatform, err := v1.ParsePlatform("linux/amd64")
require.NoError(t, err)
testCases := []struct {
policyPath string
expectSuccess bool
@@ -87,7 +88,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
imageName, err := tc.resolver.ImageName(ctx)
require.NoError(t, err)
resolver := policy.NewResolver(nil, tc.opts)
policy, err := resolver.ResolvePolicy(ctx, imageName)
policy, err := resolver.ResolvePolicy(ctx, imageName, defaultPlatform)
if tc.resolveErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.resolveErrorStr)
@@ -108,7 +109,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
}
func TestLoadingMappings(t *testing.T) {
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "policies", "allow"))
policyMappings, err := mapping.LoadLocalMappings(filepath.Join("testdata", "policies", "allow"))
require.NoError(t, err)
assert.Equal(t, len(policyMappings.Rules), 3)
for _, mirror := range policyMappings.Rules {
@@ -125,32 +126,32 @@ func TestCreateAttestationResolver(t *testing.T) {
layoutResolver := &attestation.LayoutResolver{}
registryResolver := &oci.RegistryImageDetailsResolver{}
nilRepoReferrers := &config.PolicyMapping{
Attestations: &config.AttestationConfig{
Style: config.AttestationStyleReferrers,
nilRepoReferrers := &mapping.PolicyMapping{
Attestations: &mapping.AttestationConfig{
Style: mapping.AttestationStyleReferrers,
},
}
referrers := &config.PolicyMapping{
Attestations: &config.AttestationConfig{
referrers := &mapping.PolicyMapping{
Attestations: &mapping.AttestationConfig{
Repo: "localhost:5000/repo",
Style: config.AttestationStyleReferrers,
Style: mapping.AttestationStyleReferrers,
},
}
attached := &config.PolicyMapping{
Attestations: &config.AttestationConfig{
Style: config.AttestationStyleAttached,
attached := &mapping.PolicyMapping{
Attestations: &mapping.AttestationConfig{
Style: mapping.AttestationStyleAttached,
},
}
testCases := []struct {
name string
resolver oci.ImageDetailsResolver
mapping *config.PolicyMapping
mapping *mapping.PolicyMapping
errorStr string
}{
{name: "referrers", resolver: layoutResolver, mapping: referrers},
{name: "referrers (no mapped repo)", resolver: layoutResolver, mapping: nilRepoReferrers},
{name: "referrers (no mapping)", resolver: layoutResolver, mapping: &config.PolicyMapping{Attestations: nil}},
{name: "referrers (no mapping)", resolver: layoutResolver, mapping: &mapping.PolicyMapping{Attestations: nil}},
{name: "attached (registry)", resolver: registryResolver, mapping: attached},
{name: "attached (layout)", resolver: layoutResolver, mapping: attached},
{name: "attached (unsupported)", resolver: mockResolver, mapping: attached, errorStr: "unsupported image details resolver type"},
@@ -169,11 +170,11 @@ func TestCreateAttestationResolver(t *testing.T) {
}
switch resolver.(type) {
case *attestation.ReferrersResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
assert.Equal(t, tc.mapping.Attestations.Style, mapping.AttestationStyleReferrers)
case *attestation.RegistryResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
assert.Equal(t, tc.mapping.Attestations.Style, mapping.AttestationStyleAttached)
case *attestation.LayoutResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
assert.Equal(t, tc.mapping.Attestations.Style, mapping.AttestationStyleAttached)
}
})
}

View File

@@ -8,9 +8,10 @@ import (
"path/filepath"
"github.com/distribution/reference"
"github.com/docker/attest/config"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/mapping"
"github.com/docker/attest/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
type Resolver struct {
@@ -25,7 +26,7 @@ func NewResolver(tufClient tuf.Downloader, opts *Options) *Resolver {
}
}
func (r *Resolver) ResolvePolicy(_ context.Context, imageName string) (*Policy, error) {
func (r *Resolver) ResolvePolicy(_ context.Context, imageName string, platform *v1.Platform) (*Policy, error) {
p, err := r.resolvePolicyByID()
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
@@ -37,45 +38,45 @@ func (r *Resolver) ResolvePolicy(_ context.Context, imageName string) (*Policy,
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
localMappings, err := mapping.LoadLocalMappings(r.opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
match, err := findPolicyMatch(imageName, localMappings)
match, err := localMappings.FindPolicyMatch(imageName, platform)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return r.resolveLocalPolicy(match.policy, imageName, match.matchedName)
if match.MatchType == mapping.MatchTypePolicy {
return r.resolveLocalPolicy(match.Policy, imageName, match.MatchedName)
}
if !r.opts.DisableTUF {
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
tufMappings, err := mapping.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
// it's a mirror of a tuf policy
if match.matchType == matchTypeMatchNoPolicy {
if match.MatchType == mapping.MatchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID {
return r.resolveTUFPolicy(mapping, imageName, match.matchedName)
if mapping.ID == match.Rule.PolicyID {
return r.resolveTUFPolicy(mapping, imageName, match.MatchedName)
}
}
}
// try to resolve a tuf policy directly
match, err = findPolicyMatch(imageName, tufMappings)
match, err = tufMappings.FindPolicyMatch(imageName, platform)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return r.resolveTUFPolicy(match.policy, imageName, match.matchedName)
if match.MatchType == mapping.MatchTypePolicy {
return r.resolveTUFPolicy(match.Policy, imageName, match.MatchedName)
}
}
return nil, nil
}
func (r *Resolver) resolveLocalPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
func (r *Resolver) resolveLocalPolicy(mapping *mapping.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
if r.opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
@@ -118,7 +119,7 @@ func (r *Resolver) resolveLocalPolicy(mapping *config.PolicyMapping, imageName s
return policy, nil
}
func (r *Resolver) resolveTUFPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
func (r *Resolver) resolveTUFPolicy(mapping *mapping.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
var URI string
var digest map[string]string
files := make([]*File, 0, len(mapping.Files))
@@ -159,7 +160,7 @@ func (r *Resolver) resolveTUFPolicy(mapping *config.PolicyMapping, imageName str
func (r *Resolver) resolvePolicyByID() (*Policy, error) {
if r.opts.PolicyID != "" {
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
localMappings, err := mapping.LoadLocalMappings(r.opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
@@ -171,7 +172,7 @@ func (r *Resolver) resolvePolicyByID() (*Policy, error) {
}
if !r.opts.DisableTUF {
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
tufMappings, err := mapping.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/docker/attest/internal/test"
"github.com/docker/attest/policy"
"github.com/docker/attest/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -17,7 +18,8 @@ func TestResolvePolicy(t *testing.T) {
noLocalPolicyPath := "testdata/policies/no-policy"
testPolicyID := "docker-official-images"
testImageName := "localhost:5001/test/repo:tag"
defaultPlatform, err := v1.ParsePlatform("linux/amd64")
require.NoError(t, err)
testCases := []struct {
name string
policyPath string
@@ -52,7 +54,7 @@ func TestResolvePolicy(t *testing.T) {
opts.DisableTUF = tc.DisableTUF
opts.LocalTargetsDir = tempDir
resolver := policy.NewResolver(tufClient, opts)
policy, err := resolver.ResolvePolicy(context.Background(), testImageName)
policy, err := resolver.ResolvePolicy(context.Background(), testImageName, defaultPlatform)
require.NoError(t, err)
assert.NotNil(t, policy)
if tc.DisableTUF || tc.localOverridesTUF {

View File

@@ -1,10 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images

View File

@@ -1,10 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: local-policy
description: Local Policy
files:
- path: local-policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images # note this policy does not exist in this file

View File

@@ -1,13 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"
policy-id: docker-official-images # invalid to specify both rewrite and policy-id

View File

@@ -1,14 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^yin/(.*)$"
rewrite: "yang/$1"
- pattern: "^yang/(.*)$"
rewrite: "yin/$1"

View File

@@ -1,14 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"
- pattern: "^myevencoolermirror[.]org/library/(.*)$"
rewrite: "mycoolmirror.org/library/$1"

View File

@@ -1,12 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: local-policy
description: Local Policy
files:
- path: local-policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images # note this policy does not exist in this file
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"

View File

@@ -1,12 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "badredirect.org/$1" # no matching rule for this rewrite

View File

@@ -1,12 +0,0 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"

View File

@@ -2,7 +2,7 @@ package policy
import (
"github.com/docker/attest/attestation"
"github.com/docker/attest/config"
"github.com/docker/attest/mapping"
"github.com/docker/attest/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
@@ -34,7 +34,7 @@ type Options struct {
LocalPolicyDir string
PolicyID string
ReferrersRepo string
AttestationStyle config.AttestationStyle
AttestationStyle mapping.AttestationStyle
Debug bool
AttestationVerifier attestation.Verifier
}
@@ -42,7 +42,7 @@ type Options struct {
type Policy struct {
InputFiles []*File
Query string
Mapping *config.PolicyMapping
Mapping *mapping.PolicyMapping
ResolvedName string
URI string
Digest map[string]string