From 6f94d59a9641914b980d24b52859fe6bc21d8b00 Mon Sep 17 00:00:00 2001 From: Jonny Stoten Date: Wed, 28 Aug 2024 11:27:00 +0100 Subject: [PATCH] refactor!: add `policy.Resolver` struct to reduce parameters (#130) * Add `policy.Resolver` struct to reduce parameters * Pass image name directly rather than resolver * Move policy match stuff to its own file --- pkg/attest/verify.go | 8 +- pkg/policy/match.go | 65 +++++ .../{policy_match_test.go => match_test.go} | 0 pkg/policy/policy.go | 238 ------------------ pkg/policy/policy_test.go | 15 +- pkg/policy/resolver.go | 194 ++++++++++++++ 6 files changed, 270 insertions(+), 250 deletions(-) create mode 100644 pkg/policy/match.go rename pkg/policy/{policy_match_test.go => match_test.go} (100%) create mode 100644 pkg/policy/resolver.go diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index 4d26851..e57c0c6 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -50,8 +50,12 @@ func (v *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *V if err != nil { return nil, fmt.Errorf("failed to create image details resolver: %w", err) } - - pctx, err := policy.ResolvePolicy(ctx, v.tufClient, detailsResolver, v.opts) + imageName, err := detailsResolver.ImageName(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve image name: %w", err) + } + policyResolver := policy.NewResolver(v.tufClient, v.opts) + pctx, err := policyResolver.ResolvePolicy(ctx, imageName) if err != nil { return nil, fmt.Errorf("failed to resolve policy: %w", err) } diff --git a/pkg/policy/match.go b/pkg/policy/match.go new file mode 100644 index 0000000..4b8be4f --- /dev/null +++ b/pkg/policy/match.go @@ -0,0 +1,65 @@ +package policy + +import ( + "fmt" + + "github.com/docker/attest/pkg/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 +} diff --git a/pkg/policy/policy_match_test.go b/pkg/policy/match_test.go similarity index 100% rename from pkg/policy/policy_match_test.go rename to pkg/policy/match_test.go diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 03d9a9b..91892a1 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -1,251 +1,13 @@ package policy import ( - "context" "fmt" - "os" - "path" - "path/filepath" - "github.com/distribution/reference" - "github.com/docker/attest/internal/util" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" - "github.com/docker/attest/pkg/tuf" ) -func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { - if opts.LocalPolicyDir == "" { - return nil, fmt.Errorf("local policy dir not set") - } - var URI string - var digest map[string]string - files := make([]*File, 0, len(mapping.Files)) - for _, f := range mapping.Files { - filename := f.Path - filePath := path.Join(opts.LocalPolicyDir, filename) - fileContents, err := os.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err) - } - files = append(files, &File{ - Path: filename, - Content: fileContents, - }) - // if the file is a policy file, store the URI and digest - if filepath.Ext(filename) == ".rego" { - // TODO: support multiple rego files, need some way to identify the main policy file - if URI != "" { - return nil, fmt.Errorf("multiple policy files found in policy mapping") - } - URI = filePath - digest = map[string]string{"sha256": util.SHA256Hex(fileContents)} - } - } - if URI == "" { - return nil, fmt.Errorf("no policy file found in policy mapping") - } - policy := &Policy{ - InputFiles: files, - Mapping: mapping, - URI: URI, - Digest: digest, - } - if imageName != matchedName { - policy.ResolvedName = matchedName - } - return policy, nil -} - -func resolveTUFPolicy(opts *Options, tufClient tuf.Downloader, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { - var URI string - var digest map[string]string - files := make([]*File, 0, len(mapping.Files)) - for _, f := range mapping.Files { - filename := f.Path - file, err := tufClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename)) - if err != nil { - return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err) - } - files = append(files, &File{ - Path: filename, - Content: file.Data, - }) - // if the file is a policy file, store the URI and digest - if filepath.Ext(filename) == ".rego" { - // TODO: support multiple rego files, need some way to identify the main policy file - if URI != "" { - return nil, fmt.Errorf("multiple policy files found in policy mapping") - } - URI = file.TargetURI - digest = map[string]string{"sha256": file.Digest} - } - } - if URI == "" { - return nil, fmt.Errorf("no policy file found in policy mapping") - } - policy := &Policy{ - InputFiles: files, - Mapping: mapping, - URI: URI, - Digest: digest, - } - if imageName != matchedName { - policy.ResolvedName = matchedName - } - return policy, nil -} - -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 -} - -func resolvePolicyByID(opts *Options, tufClient tuf.Downloader) (*Policy, error) { - if opts.PolicyID != "" { - localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir) - if err != nil { - return nil, fmt.Errorf("failed to load local policy mappings: %w", err) - } - if localMappings != nil { - policy := localMappings.Policies[opts.PolicyID] - if policy != nil { - return resolveLocalPolicy(opts, policy, "", "") - } - } - if !opts.DisableTUF { - // must check tuf - tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir) - if err != nil { - return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err) - } - policy := tufMappings.Policies[opts.PolicyID] - if policy != nil { - return resolveTUFPolicy(opts, tufClient, policy, "", "") - } - } - return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID) - } - return nil, nil -} - -func ResolvePolicy(ctx context.Context, tufClient tuf.Downloader, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) { - p, err := resolvePolicyByID(opts, tufClient) - if err != nil { - return nil, fmt.Errorf("failed to resolve policy by id: %w", err) - } - if p != nil { - return p, nil - } - imageName, err := detailsResolver.ImageName(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get image name: %w", err) - } - imageName, err = normalizeImageName(imageName) - if err != nil { - return nil, fmt.Errorf("failed to parse image name: %w", err) - } - localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir) - if err != nil { - return nil, fmt.Errorf("failed to load local policy mappings: %w", err) - } - match, err := findPolicyMatch(imageName, localMappings) - if err != nil { - return nil, err - } - if match.matchType == matchTypePolicy { - return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName) - } - if !opts.DisableTUF { - // must check tuf - tufMappings, err := config.LoadTUFMappings(tufClient, 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 { - for _, mapping := range tufMappings.Policies { - if mapping.ID == match.rule.PolicyID { - return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName) - } - } - } - // try to resolve a tuf policy directly - match, err = findPolicyMatch(imageName, tufMappings) - if err != nil { - return nil, err - } - if match.matchType == matchTypePolicy { - return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName) - } - } - - return nil, nil -} - -func normalizeImageName(imageName string) (string, error) { - named, err := reference.ParseNormalizedNamed(imageName) - if err != nil { - return "", fmt.Errorf("failed to parse image name: %w", err) - } - return named.Name(), nil -} - func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) { switch imageSource.Type { case oci.OCI: diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 9812eec..239e637 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -46,7 +46,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { expectSuccess bool isCanonical bool resolver attestation.Resolver - policy *policy.Options + opts *policy.Options policyID string resolveErrorStr string }{ @@ -71,8 +71,8 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { input.Tag = "test" } - if tc.policy == nil { - tc.policy = &policy.Options{ + if tc.opts == nil { + tc.opts = &policy.Options{ LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"), PolicyID: tc.policyID, LocalPolicyDir: tc.repo, @@ -81,13 +81,8 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { } imageName, err := tc.resolver.ImageName(ctx) require.NoError(t, err) - platform, err := tc.resolver.ImagePlatform(ctx) - require.NoError(t, err) - src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String())) - require.NoError(t, err) - resolver, err := policy.CreateImageDetailsResolver(src) - require.NoError(t, err) - policy, err := policy.ResolvePolicy(ctx, nil, resolver, tc.policy) + resolver := policy.NewResolver(nil, tc.opts) + policy, err := resolver.ResolvePolicy(ctx, imageName) if tc.resolveErrorStr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.resolveErrorStr) diff --git a/pkg/policy/resolver.go b/pkg/policy/resolver.go new file mode 100644 index 0000000..7d73e1f --- /dev/null +++ b/pkg/policy/resolver.go @@ -0,0 +1,194 @@ +package policy + +import ( + "context" + "fmt" + "os" + "path" + "path/filepath" + + "github.com/distribution/reference" + "github.com/docker/attest/internal/util" + "github.com/docker/attest/pkg/config" + "github.com/docker/attest/pkg/tuf" +) + +type Resolver struct { + tufClient tuf.Downloader + opts *Options +} + +func NewResolver(tufClient tuf.Downloader, opts *Options) *Resolver { + return &Resolver{ + tufClient: tufClient, + opts: opts, + } +} + +func (r *Resolver) ResolvePolicy(_ context.Context, imageName string) (*Policy, error) { + p, err := r.resolvePolicyByID() + if err != nil { + return nil, fmt.Errorf("failed to resolve policy by id: %w", err) + } + if p != nil { + return p, nil + } + imageName, err = normalizeImageName(imageName) + if err != nil { + return nil, fmt.Errorf("failed to parse image name: %w", err) + } + localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir) + if err != nil { + return nil, fmt.Errorf("failed to load local policy mappings: %w", err) + } + match, err := findPolicyMatch(imageName, localMappings) + if err != nil { + return nil, err + } + if match.matchType == matchTypePolicy { + return r.resolveLocalPolicy(match.policy, imageName, match.matchedName) + } + if !r.opts.DisableTUF { + tufMappings, err := config.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 { + for _, mapping := range tufMappings.Policies { + 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) + if err != nil { + return nil, err + } + if match.matchType == 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) { + if r.opts.LocalPolicyDir == "" { + return nil, fmt.Errorf("local policy dir not set") + } + var URI string + var digest map[string]string + files := make([]*File, 0, len(mapping.Files)) + for _, f := range mapping.Files { + filename := f.Path + filePath := path.Join(r.opts.LocalPolicyDir, filename) + fileContents, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err) + } + files = append(files, &File{ + Path: filename, + Content: fileContents, + }) + // if the file is a policy file, store the URI and digest + if filepath.Ext(filename) == ".rego" { + // TODO: support multiple rego files, need some way to identify the main policy file + if URI != "" { + return nil, fmt.Errorf("multiple policy files found in policy mapping") + } + URI = filePath + digest = map[string]string{"sha256": util.SHA256Hex(fileContents)} + } + } + if URI == "" { + return nil, fmt.Errorf("no policy file found in policy mapping") + } + policy := &Policy{ + InputFiles: files, + Mapping: mapping, + URI: URI, + Digest: digest, + } + if imageName != matchedName { + policy.ResolvedName = matchedName + } + return policy, nil +} + +func (r *Resolver) resolveTUFPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { + var URI string + var digest map[string]string + files := make([]*File, 0, len(mapping.Files)) + for _, f := range mapping.Files { + filename := f.Path + file, err := r.tufClient.DownloadTarget(filename, filepath.Join(r.opts.LocalTargetsDir, filename)) + if err != nil { + return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err) + } + files = append(files, &File{ + Path: filename, + Content: file.Data, + }) + // if the file is a policy file, store the URI and digest + if filepath.Ext(filename) == ".rego" { + // TODO: support multiple rego files, need some way to identify the main policy file + if URI != "" { + return nil, fmt.Errorf("multiple policy files found in policy mapping") + } + URI = file.TargetURI + digest = map[string]string{"sha256": file.Digest} + } + } + if URI == "" { + return nil, fmt.Errorf("no policy file found in policy mapping") + } + policy := &Policy{ + InputFiles: files, + Mapping: mapping, + URI: URI, + Digest: digest, + } + if imageName != matchedName { + policy.ResolvedName = matchedName + } + return policy, nil +} + +func (r *Resolver) resolvePolicyByID() (*Policy, error) { + if r.opts.PolicyID != "" { + localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir) + if err != nil { + return nil, fmt.Errorf("failed to load local policy mappings: %w", err) + } + if localMappings != nil { + policy := localMappings.Policies[r.opts.PolicyID] + if policy != nil { + return r.resolveLocalPolicy(policy, "", "") + } + } + + if !r.opts.DisableTUF { + tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir) + if err != nil { + return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err) + } + policy := tufMappings.Policies[r.opts.PolicyID] + if policy != nil { + return r.resolveTUFPolicy(policy, "", "") + } + } + return nil, fmt.Errorf("policy with id %s not found", r.opts.PolicyID) + } + return nil, nil +} + +func normalizeImageName(imageName string) (string, error) { + named, err := reference.ParseNormalizedNamed(imageName) + if err != nil { + return "", fmt.Errorf("failed to parse image name: %w", err) + } + return named.Name(), nil +}