Skip to content
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
1 change: 1 addition & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ type policyFileSpec struct {
type policyEvalOpt struct {
Strict bool
LogLevel *logrus.Level
SkipCaps bool
}

type policyOpt struct {
Expand Down
26 changes: 26 additions & 0 deletions build/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
Data: policy.DefaultPolicyData(),
}},
}
builtin.SkipCaps = true
popts = append([]policyOpt{builtin}, popts...)
}

Expand Down Expand Up @@ -742,6 +743,11 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
DefaultPlatform: defaultPlatform(bopts),
SourceResolver: sourceResolver,
})
if !popt.SkipCaps {
if err := applyPolicyCaps(ctx, p, bopts, so); err != nil {
return nil, err
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could poitentially reject existing policies that never request caps?

Maybe we can treat an undefined or empty decision during caps probing as empty caps?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the policies do not define caps then it is as good as having caps false. If one policy requires caps and others don't (explicitly or not) then caps still needed.

policies = append(policies, p)
cbs = append(cbs, p.CheckPolicy)
if popt.Strict {
Expand All @@ -750,10 +756,30 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
}
}
}
if so.ProxyNetwork {
if policyLogger != nil {
policyLogger.Log("policy enabled network proxy")
}
}
so.SourcePolicyProvider = policysession.NewPolicyProvider(policy.MultiPolicyCallback(cbs...))
return defers, nil
}

func applyPolicyCaps(ctx context.Context, p *policy.Policy, bopts gateway.BuildOpts, so *client.SolveOpt) error {
caps, err := p.CheckCaps(ctx)
if err != nil {
return errors.Wrap(err, "failed to evaluate policy caps")
}
if !caps[policy.CapExecProxy] {
return nil
}
if err := bopts.LLBCaps.Supports(pb.CapExecMetaNetworkProxy); err != nil {
return errors.New("network proxy requested by policy is not supported by the current BuildKit daemon, please upgrade to version v0.31+")
}
so.ProxyNetwork = true
return nil
}

func policyEnvFilename(inp Inputs) string {
base := filepath.Base(filepath.Clean(inp.DockerfilePath))
if base != "." && base != string(filepath.Separator) {
Expand Down
87 changes: 87 additions & 0 deletions build/opt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import (
"sync"
"testing"

"github.com/docker/buildx/policy"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/ocilayout"
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/ociindex"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/apicaps"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
Expand Down Expand Up @@ -163,6 +167,67 @@ func TestProxyArgKeyExists(t *testing.T) {
}
}

func TestApplyPolicyCapsEnablesProxyNetwork(t *testing.T) {
p := policyWithDecision(`
package docker

decision := {
"allow": false,
"caps": {"exec.proxy": true},
}
`)
var so client.SolveOpt

err := applyPolicyCaps(context.Background(), p, buildOptsWithCaps(pb.CapExecMetaNetworkProxy), &so)
require.NoError(t, err)
require.True(t, so.ProxyNetwork)
}

func TestApplyPolicyCapsOrsProxyNetwork(t *testing.T) {
falsePolicy := policyWithDecision(`
package docker

decision := {
"allow": true,
"caps": {"exec.proxy": false},
}
`)
truePolicy := policyWithDecision(`
package docker

decision := {
"allow": true,
"caps": {"exec.proxy": true},
}
`)
var so client.SolveOpt

err := applyPolicyCaps(context.Background(), falsePolicy, buildOptsWithCaps(pb.CapExecMetaNetworkProxy), &so)
require.NoError(t, err)
require.False(t, so.ProxyNetwork)

err = applyPolicyCaps(context.Background(), truePolicy, buildOptsWithCaps(pb.CapExecMetaNetworkProxy), &so)
require.NoError(t, err)
require.True(t, so.ProxyNetwork)
}

func TestApplyPolicyCapsRequiresBuildKitCap(t *testing.T) {
p := policyWithDecision(`
package docker

decision := {
"allow": true,
"caps": {"exec.proxy": true},
}
`)
var so client.SolveOpt

err := applyPolicyCaps(context.Background(), p, buildOptsWithCaps(), &so)
require.ErrorContains(t, err, "network proxy requested by policy is not supported by the current BuildKit daemon")
require.ErrorContains(t, err, "please upgrade to version v0.31+")
require.False(t, so.ProxyNetwork)
}

func TestLoadInputsOCILayoutNamedContext(t *testing.T) {
layoutPath := t.TempDir()

Expand Down Expand Up @@ -299,3 +364,25 @@ func (w *captureProgressWriter) ValidateLogSource(digest.Digest, any) bool { ret
func (w *captureProgressWriter) ClearLogSource(any) {}

var _ progress.Writer = (*captureProgressWriter)(nil)

func policyWithDecision(decision string) *policy.Policy {
return policy.NewPolicy(policy.Opt{
Files: []policy.File{{
Filename: "policy.rego",
Data: []byte(decision),
}},
})
}

func buildOptsWithCaps(caps ...apicaps.CapID) gateway.BuildOpts {
out := make([]*apicaps.PBCap, 0, len(caps))
for _, c := range caps {
out = append(out, &apicaps.PBCap{
ID: string(c),
Enabled: true,
})
}
return gateway.BuildOpts{
LLBCaps: pb.Caps.CapSet(out),
}
}
14 changes: 14 additions & 0 deletions policy/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,23 @@ func decodeDecision(decision any) *Decision {
if len(denyMsgs) == 0 {
denyMsgs = nil
}
caps := Caps{}
if v, ok := obj["caps"]; ok {
if m, ok := v.(map[string]any); ok {
for k, entry := range m {
if b, ok := entry.(bool); ok {
caps[k] = b
}
}
}
}
if len(caps) == 0 {
caps = nil
}
return &Decision{
Allow: allow,
DenyMessages: denyMsgs,
Caps: caps,
}
}

Expand Down
20 changes: 15 additions & 5 deletions policy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,24 @@ type Input struct {
type Decision struct {
Allow *bool `json:"allow,omitempty"`
DenyMessages []string `json:"deny_msg,omitempty"`
Caps Caps `json:"caps,omitempty"`
}

type Caps map[string]bool

const CapExecProxy = "exec.proxy"

var KnownCaps = map[string]struct{}{
CapExecProxy: {},
}

type Env struct {
Args map[string]*string `json:"args,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Filename string `json:"filename,omitempty"`
Target string `json:"target,omitempty"`
Depth int `json:"depth"`
Args map[string]*string `json:"args,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Filename string `json:"filename,omitempty"`
Target string `json:"target,omitempty"`
CapsRequest bool `json:"capsRequest,omitempty"`
Depth int `json:"depth"`
}

type HTTP struct {
Expand Down
Loading
Loading