1
0
mirror of https://github.com/moby/buildkit.git synced 2025-11-27 04:01:46 +03:00
Files
buildkit/sourcepolicy/engine.go
Brian Goff 6e89b21e21 sourcepolicy: split dest type from identifier
This makes destination more symetrical with sources.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2022-12-13 13:01:54 -08:00

220 lines
5.5 KiB
Go

package sourcepolicy
import (
"context"
"strings"
"github.com/moby/buildkit/solver/pb"
spb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/pkg/errors"
)
var (
// ErrSourceDenied is returned by the policy engine when a source is denied by the policy.
ErrSourceDenied = errors.New("source denied by policy")
// ErrTooManyOps is returned by the policy engine when there are too many converts for a single source op.
ErrTooManyOps = errors.New("too many operations")
)
// Engine is the source policy engine.
// It is responsible for evaluating a source policy against a source operation.
// Create one with `NewEngine`
//
// Rule matching is delegated to the `Matcher` interface.
// Mutations are delegated to the `Mutater` interface.
type Engine struct {
pol *spb.Policy
matcher Matcher
mutater Mutater
sources map[string]*Source
}
// NewEngine creates a new source policy engine.
func NewEngine(pol *spb.Policy, matcher Matcher, mutater Mutater) *Engine {
if matcher == nil {
matcher = DefaultMatcher
}
if mutater == nil {
mutater = DefaultMutater
}
return &Engine{
pol: pol,
matcher: matcher,
mutater: mutater,
}
}
// TODO: The key here can't be used to cache attr constraint regexes.
func (e *Engine) source(src *spb.Source) *Source {
if e.sources == nil {
e.sources = map[string]*Source{}
}
key := src.MatchType.String() + " " + src.Type + "://" + src.Identifier
if s, ok := e.sources[key]; ok {
return s
}
s := &Source{Source: src}
e.sources[key] = s
return s
}
// Evaluate evaluates a source operation against the policy.
//
// Policies are re-evaluated for each convert rule.
// Evaluate will error if the there are too many converts for a single source op to prevent infinite loops.
// This function may error out even if the op was mutated, in which case `true` will be returned along with the error.
//
// An error is returned when the source is denied by the policy.
func (e *Engine) Evaluate(ctx context.Context, op *pb.Op) (bool, error) {
if len(e.pol.Rules) == 0 {
return false, nil
}
var (
st evalState
mutated bool
)
const maxIterr = 20
for i := 0; ; i++ {
if i > maxIterr {
return mutated, errors.Wrapf(ErrTooManyOps, "too many mutations on a single source")
}
srcOp := op.GetSource()
if srcOp == nil {
return false, nil
}
if i == 0 {
ctx = bklog.WithLogger(ctx, bklog.G(ctx).WithField("orig", *srcOp).WithField("updated", op.GetSource()))
}
mut, err := e.evaluateRules(ctx, srcOp, &st)
if mut {
mutated = true
}
if err != nil {
return mutated, err
}
if !mut {
break
}
}
return mutated, nil
}
func (e *Engine) evaluateRules(ctx context.Context, srcOp *pb.SourceOp, st *evalState) (bool, error) {
ident := srcOp.GetIdentifier()
scheme, ref, found := strings.Cut(ident, "://")
if !found || ref == "" {
return false, errors.Errorf("failed to parse %q", ident)
}
ctx = bklog.WithLogger(ctx, bklog.G(ctx).WithFields(map[string]interface{}{
"scheme": scheme,
"ref": ref,
}))
for _, rule := range e.pol.Rules {
mut, err := e.evaluateRule(ctx, rule, scheme, ref, srcOp, st)
if err != nil {
return false, err
}
if mut {
return true, nil
}
}
return false, nil
}
func (e *Engine) evaluateRule(ctx context.Context, rule *spb.Rule, scheme, ref string, op *pb.SourceOp, st *evalState) (bool, error) {
origScheme := scheme
var isHTTP bool
switch scheme {
case "http", "https":
isHTTP = true
// The scheme ref is important for http/https sources
ref = scheme + "://" + ref
// Update the scheme to match the rule
// This is done so the rule can match regardless of what shceme we pulled off the URL and the rule can be written with either scheme.
switch rule.Source.Type {
case "http", "https":
scheme = rule.Source.Type
}
}
src := e.source(rule.Source)
match, err := e.matcher.Match(ctx, src, scheme, ref, op.Attrs)
if err != nil {
return false, errors.Wrap(err, "error evaluating rule")
}
bklog.G(ctx).Debug("sourcepolicy: rule match")
if !match {
return false, nil
}
switch rule.Action {
case spb.PolicyAction_ALLOW:
st.allow(ref)
return false, nil
case spb.PolicyAction_DENY:
// If this has already been allowed by a previous rule then we can ignore it.
if st.allowed[ref] {
return false, nil
}
if match {
return false, errors.Wrapf(ErrSourceDenied, "rule %s %s applies to source %s://%s", rule.Action, rule.Source.Identifier, scheme, ref)
}
return false, nil
case spb.PolicyAction_CONVERT:
if rule.Destination == nil {
return false, errors.Errorf("missing destination for convert rule")
}
// TODO: This should really go in the mutator, but there's a lot of deatail we'd need to pass through.
dest := rule.Destination.Identifier
if dest == "" {
dest = rule.Source.Identifier
}
dest, err = src.Format(ref, dest)
if err != nil {
return false, errors.Wrap(err, "error formatting destination")
}
typ := rule.Destination.Type
if typ == "" {
typ = origScheme
}
if !isHTTP {
dest = typ + "://" + dest
}
bklog.G(ctx).Debugf("sourcepolicy: converting %s to %s, pattern: %s", ref, dest, rule.Destination.Identifier)
return e.mutater.Mutate(ctx, op, dest, rule.Destination.Attrs)
default:
return false, errors.Errorf("source policy: rule %s %s: unknown type %q", rule.Action, rule.Source.Identifier, ref)
}
}
type evalState struct {
allowed map[string]bool
}
func (st *evalState) allow(ref string) {
if st.allowed == nil {
st.allowed = map[string]bool{}
}
st.allowed[ref] = true
}