policy: enable proxy network from source policy caps
Evaluate source policy caps before solve requests so policies can enable
BuildKit proxy networking. Policy can return caps {"exec.proxy": true}
during the caps request to enable proxy network
support for the solve.
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -140,6 +140,7 @@ type policyFileSpec struct {
|
|||||||
type policyEvalOpt struct {
|
type policyEvalOpt struct {
|
||||||
Strict bool
|
Strict bool
|
||||||
LogLevel *logrus.Level
|
LogLevel *logrus.Level
|
||||||
|
SkipCaps bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type policyOpt struct {
|
type policyOpt struct {
|
||||||
|
|||||||
@@ -667,6 +667,7 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
|
|||||||
Data: policy.DefaultPolicyData(),
|
Data: policy.DefaultPolicyData(),
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
builtin.SkipCaps = true
|
||||||
popts = append([]policyOpt{builtin}, popts...)
|
popts = append([]policyOpt{builtin}, popts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,6 +743,11 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
|
|||||||
DefaultPlatform: defaultPlatform(bopts),
|
DefaultPlatform: defaultPlatform(bopts),
|
||||||
SourceResolver: sourceResolver,
|
SourceResolver: sourceResolver,
|
||||||
})
|
})
|
||||||
|
if !popt.SkipCaps {
|
||||||
|
if err := applyPolicyCaps(ctx, p, bopts, so); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
policies = append(policies, p)
|
policies = append(policies, p)
|
||||||
cbs = append(cbs, p.CheckPolicy)
|
cbs = append(cbs, p.CheckPolicy)
|
||||||
if popt.Strict {
|
if popt.Strict {
|
||||||
@@ -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...))
|
so.SourcePolicyProvider = policysession.NewPolicyProvider(policy.MultiPolicyCallback(cbs...))
|
||||||
return defers, nil
|
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 {
|
func policyEnvFilename(inp Inputs) string {
|
||||||
base := filepath.Base(filepath.Clean(inp.DockerfilePath))
|
base := filepath.Base(filepath.Clean(inp.DockerfilePath))
|
||||||
if base != "." && base != string(filepath.Separator) {
|
if base != "." && base != string(filepath.Separator) {
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/policy"
|
||||||
"github.com/docker/buildx/util/buildflags"
|
"github.com/docker/buildx/util/buildflags"
|
||||||
"github.com/docker/buildx/util/ocilayout"
|
"github.com/docker/buildx/util/ocilayout"
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/moby/buildkit/client/ociindex"
|
"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"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -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) {
|
func TestLoadInputsOCILayoutNamedContext(t *testing.T) {
|
||||||
layoutPath := t.TempDir()
|
layoutPath := t.TempDir()
|
||||||
|
|
||||||
@@ -299,3 +364,25 @@ func (w *captureProgressWriter) ValidateLogSource(digest.Digest, any) bool { ret
|
|||||||
func (w *captureProgressWriter) ClearLogSource(any) {}
|
func (w *captureProgressWriter) ClearLogSource(any) {}
|
||||||
|
|
||||||
var _ progress.Writer = (*captureProgressWriter)(nil)
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -683,9 +683,23 @@ func decodeDecision(decision any) *Decision {
|
|||||||
if len(denyMsgs) == 0 {
|
if len(denyMsgs) == 0 {
|
||||||
denyMsgs = nil
|
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{
|
return &Decision{
|
||||||
Allow: allow,
|
Allow: allow,
|
||||||
DenyMessages: denyMsgs,
|
DenyMessages: denyMsgs,
|
||||||
|
Caps: caps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-5
@@ -21,14 +21,24 @@ type Input struct {
|
|||||||
type Decision struct {
|
type Decision struct {
|
||||||
Allow *bool `json:"allow,omitempty"`
|
Allow *bool `json:"allow,omitempty"`
|
||||||
DenyMessages []string `json:"deny_msg,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 {
|
type Env struct {
|
||||||
Args map[string]*string `json:"args,omitempty"`
|
Args map[string]*string `json:"args,omitempty"`
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
Filename string `json:"filename,omitempty"`
|
Filename string `json:"filename,omitempty"`
|
||||||
Target string `json:"target,omitempty"`
|
Target string `json:"target,omitempty"`
|
||||||
Depth int `json:"depth"`
|
CapsRequest bool `json:"capsRequest,omitempty"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
|
|||||||
+145
-52
@@ -130,27 +130,7 @@ func (p *Policy) IsPolicyError(err error) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) {
|
func (p *Policy) regoBaseOpts() ([]func(*rego.Rego), func(), error) {
|
||||||
if req.Source == nil || req.Source.Source == nil {
|
|
||||||
return nil, nil, errors.Errorf("no source info in request")
|
|
||||||
}
|
|
||||||
|
|
||||||
var platform *ocispecs.Platform
|
|
||||||
if req.Platform != nil {
|
|
||||||
pl, err := platformFromReq(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
platform = pl
|
|
||||||
} else {
|
|
||||||
platform = p.opt.DefaultPlatform
|
|
||||||
}
|
|
||||||
|
|
||||||
inp, err := SourceToInput(ctx, p.opt.VerifierProvider, req.Source, platform, p.opt.Log)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.Wrap(err, "failed to build policy input")
|
|
||||||
}
|
|
||||||
|
|
||||||
caps := &ast.Capabilities{
|
caps := &ast.Capabilities{
|
||||||
Builtins: builtins(),
|
Builtins: builtins(),
|
||||||
Features: slices.Clone(ast.Features),
|
Features: slices.Clone(ast.Features),
|
||||||
@@ -171,11 +151,11 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
|
|
||||||
var root fs.StatFS
|
var root fs.StatFS
|
||||||
var closeFS func() error
|
var closeFS func() error
|
||||||
defer func() {
|
closeRoot := func() {
|
||||||
if closeFS != nil {
|
if closeFS != nil {
|
||||||
closeFS()
|
closeFS()
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
comp = comp.WithModuleLoader(func(resolved map[string]*ast.Module) (parsed map[string]*ast.Module, err error) {
|
comp = comp.WithModuleLoader(func(resolved map[string]*ast.Module) (parsed map[string]*ast.Module, err error) {
|
||||||
out := make(map[string]*ast.Module)
|
out := make(map[string]*ast.Module)
|
||||||
@@ -240,6 +220,36 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
baseOpts = append(baseOpts, rego.Module(file.Filename, string(file.Data)))
|
baseOpts = append(baseOpts, rego.Module(file.Filename, string(file.Data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return baseOpts, closeRoot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) {
|
||||||
|
if req.Source == nil || req.Source.Source == nil {
|
||||||
|
return nil, nil, errors.Errorf("no source info in request")
|
||||||
|
}
|
||||||
|
|
||||||
|
var platform *ocispecs.Platform
|
||||||
|
if req.Platform != nil {
|
||||||
|
pl, err := platformFromReq(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
platform = pl
|
||||||
|
} else {
|
||||||
|
platform = p.opt.DefaultPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
inp, err := SourceToInput(ctx, p.opt.VerifierProvider, req.Source, platform, p.opt.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "failed to build policy input")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseOpts, closeRoot, err := p.regoBaseOpts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer closeRoot()
|
||||||
|
|
||||||
p.log(logrus.InfoLevel, "checking policy for source %s", sourceName(req))
|
p.log(logrus.InfoLevel, "checking policy for source %s", sourceName(req))
|
||||||
|
|
||||||
for range maxResolveIterations {
|
for range maxResolveIterations {
|
||||||
@@ -304,42 +314,23 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rs) == 0 {
|
decision, err := policyDecisionFromResult(rs)
|
||||||
return nil, nil, errors.Errorf("policy returned zero result")
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
rsz := rs[0]
|
|
||||||
if len(rsz.Expressions) == 0 {
|
|
||||||
return nil, nil, errors.Errorf("policy returned zero expressions")
|
|
||||||
}
|
|
||||||
v := rsz.Expressions[0].Value
|
|
||||||
vt, ok := v.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil, errors.Errorf("unexpected policy return type: %T %s", vt, rsz.Expressions[0].Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &policysession.DecisionResponse{
|
resp := &policysession.DecisionResponse{
|
||||||
Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY,
|
Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY,
|
||||||
}
|
}
|
||||||
p.log(logrus.DebugLevel, "policy response: %+v", vt)
|
p.log(logrus.DebugLevel, "policy response: %+v", decision)
|
||||||
|
|
||||||
if v, ok := vt["allow"]; ok {
|
if decision.Allow != nil && *decision.Allow {
|
||||||
if vv, ok := v.(bool); !ok {
|
resp.Action = moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW
|
||||||
return nil, nil, errors.Errorf("invalid allowed property type %T, expecting bool", v)
|
|
||||||
} else if vv {
|
|
||||||
resp.Action = moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := vt["deny_msg"]; ok {
|
for _, m := range decision.DenyMessages {
|
||||||
if vv, ok := v.([]any); ok {
|
resp.DenyMessages = append(resp.DenyMessages, &policysession.DenyMessage{
|
||||||
for _, m := range vv {
|
Message: m,
|
||||||
if m, ok := m.(string); ok {
|
})
|
||||||
resp.DenyMessages = append(resp.DenyMessages, &policysession.DenyMessage{
|
|
||||||
Message: m,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Action == moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW {
|
if resp.Action == moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW {
|
||||||
@@ -373,6 +364,108 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
return nil, nil, errors.Errorf("maximum attempts reached for resolving policy metadata")
|
return nil, nil, errors.Errorf("maximum attempts reached for resolving policy metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Policy) CheckCaps(ctx context.Context) (Caps, error) {
|
||||||
|
baseOpts, closeRoot, err := p.regoBaseOpts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer closeRoot()
|
||||||
|
|
||||||
|
env := p.opt.Env
|
||||||
|
env.CapsRequest = true
|
||||||
|
runInput := Input{}
|
||||||
|
applyEnvWithDepth(&runInput, env, 0)
|
||||||
|
|
||||||
|
runOpts := append([]func(*rego.Rego){}, baseOpts...)
|
||||||
|
runOpts = append(runOpts, rego.Input(runInput))
|
||||||
|
|
||||||
|
st := &state{Input: runInput}
|
||||||
|
for _, f := range p.funcs {
|
||||||
|
runOpts = append(runOpts, f.impl(st))
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, err := json.MarshalIndent(runInput, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to marshal policy input")
|
||||||
|
}
|
||||||
|
p.log(logrus.DebugLevel, "policy input: %s", dt)
|
||||||
|
|
||||||
|
rs, err := rego.New(runOpts...).Eval(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if refs := runtimeUnknownInputRefs(st); len(refs) > 0 || st.checksumNeededForSignature != nil {
|
||||||
|
return nil, errors.Errorf("policy caps request cannot resolve source metadata: %+v", summarizeUnknownsForLog(refs))
|
||||||
|
}
|
||||||
|
|
||||||
|
decision, err := policyDecisionFromResult(rs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(decision.Caps) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return decision.Caps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyDecisionFromResult(rs rego.ResultSet) (*Decision, error) {
|
||||||
|
if len(rs) == 0 {
|
||||||
|
return nil, errors.Errorf("policy returned zero result")
|
||||||
|
}
|
||||||
|
rsz := rs[0]
|
||||||
|
if len(rsz.Expressions) == 0 {
|
||||||
|
return nil, errors.Errorf("policy returned zero expressions")
|
||||||
|
}
|
||||||
|
v := rsz.Expressions[0].Value
|
||||||
|
vt, ok := v.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("unexpected policy return type: %T %s", v, rsz.Expressions[0].Text)
|
||||||
|
}
|
||||||
|
return parsePolicyDecision(vt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePolicyDecision(vt map[string]any) (*Decision, error) {
|
||||||
|
decision := &Decision{}
|
||||||
|
|
||||||
|
if v, ok := vt["allow"]; ok {
|
||||||
|
vv, ok := v.(bool)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("invalid allowed property type %T, expecting bool", v)
|
||||||
|
}
|
||||||
|
decision.Allow = &vv
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := vt["deny_msg"]; ok {
|
||||||
|
if vv, ok := v.([]any); ok {
|
||||||
|
for _, m := range vv {
|
||||||
|
if m, ok := m.(string); ok {
|
||||||
|
decision.DenyMessages = append(decision.DenyMessages, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := vt["caps"]; ok {
|
||||||
|
vv, ok := v.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("invalid caps property type %T, expecting object", v)
|
||||||
|
}
|
||||||
|
decision.Caps = make(Caps, len(vv))
|
||||||
|
for k, cv := range vv {
|
||||||
|
if _, ok := KnownCaps[k]; !ok {
|
||||||
|
return nil, errors.Errorf("unknown policy cap %q", k)
|
||||||
|
}
|
||||||
|
b, ok := cv.(bool)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("invalid caps.%s property type %T, expecting bool", k, cv)
|
||||||
|
}
|
||||||
|
decision.Caps[k] = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decision, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Policy) resolveUnknowns(ctx context.Context, input *Input, req *policysession.CheckPolicyRequest, defaultPlatform *ocispecs.Platform, unk []string, st *state) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
func (p *Policy) resolveUnknowns(ctx context.Context, input *Input, req *policysession.CheckPolicyRequest, defaultPlatform *ocispecs.Platform, unk []string, st *state) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
||||||
var resolver SourceMetadataResolver
|
var resolver SourceMetadataResolver
|
||||||
if p.opt.SourceResolver != nil {
|
if p.opt.SourceResolver != nil {
|
||||||
|
|||||||
@@ -883,6 +883,86 @@ func TestSourceToInputSingleSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckCaps(t *testing.T) {
|
||||||
|
p := NewPolicy(Opt{
|
||||||
|
Files: []File{{
|
||||||
|
Filename: "policy.rego",
|
||||||
|
Data: []byte(`
|
||||||
|
package docker
|
||||||
|
|
||||||
|
decision := {
|
||||||
|
"allow": false,
|
||||||
|
"deny_msg": ["ignored for caps"],
|
||||||
|
"caps": {
|
||||||
|
"exec.proxy": input.env.capsRequest,
|
||||||
|
},
|
||||||
|
} if {
|
||||||
|
input.env.filename == "Dockerfile"
|
||||||
|
input.env.target == "release"
|
||||||
|
input.env.args.MODE == "prod"
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
}},
|
||||||
|
Env: Env{
|
||||||
|
Args: map[string]*string{"MODE": stringPtr("prod")},
|
||||||
|
Filename: "Dockerfile",
|
||||||
|
Target: "release",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
caps, err := p.CheckCaps(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, Caps{
|
||||||
|
CapExecProxy: true,
|
||||||
|
}, caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckCapsMalformedCaps(t *testing.T) {
|
||||||
|
p := NewPolicy(Opt{
|
||||||
|
Files: []File{{
|
||||||
|
Filename: "policy.rego",
|
||||||
|
Data: []byte(`
|
||||||
|
package docker
|
||||||
|
|
||||||
|
decision := {
|
||||||
|
"allow": true,
|
||||||
|
"caps": {
|
||||||
|
"exec.proxy": "yes",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := p.CheckCaps(context.Background())
|
||||||
|
require.ErrorContains(t, err, "invalid caps.exec.proxy property type string, expecting bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckCapsUnknownCaps(t *testing.T) {
|
||||||
|
p := NewPolicy(Opt{
|
||||||
|
Files: []File{{
|
||||||
|
Filename: "policy.rego",
|
||||||
|
Data: []byte(`
|
||||||
|
package docker
|
||||||
|
|
||||||
|
decision := {
|
||||||
|
"allow": true,
|
||||||
|
"caps": {
|
||||||
|
"exec.unknown": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := p.CheckCaps(context.Background())
|
||||||
|
require.ErrorContains(t, err, `unknown policy cap "exec.unknown"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPtr(v string) *string {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
func mustMarshalImageConfig(t *testing.T, img ocispecs.Image) []byte {
|
func mustMarshalImageConfig(t *testing.T, img ocispecs.Image) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dt, err := json.Marshal(img)
|
dt, err := json.Marshal(img)
|
||||||
|
|||||||
@@ -40,6 +40,69 @@ var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){
|
|||||||
testBuildPolicyRemotePolicyFiles,
|
testBuildPolicyRemotePolicyFiles,
|
||||||
testBuildPolicyRemoteHTTPPolicyFiles,
|
testBuildPolicyRemoteHTTPPolicyFiles,
|
||||||
testBuildPolicyConfigFlags,
|
testBuildPolicyConfigFlags,
|
||||||
|
testBuildPolicyCapsProxy,
|
||||||
|
testBuildPolicyCapsProxyUnsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
var policyCapsProxyFile = []byte(`
|
||||||
|
package docker
|
||||||
|
default allow = true
|
||||||
|
default caps := {}
|
||||||
|
caps := {"exec.proxy": true} if input.env.capsRequest
|
||||||
|
decision := {"allow": allow, "caps": caps}
|
||||||
|
`)
|
||||||
|
|
||||||
|
func testBuildPolicyCapsProxy(t *testing.T, sb integration.Sandbox) {
|
||||||
|
if buildkitTag() != "master" {
|
||||||
|
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "network proxy requires BuildKit v0.31.0+")
|
||||||
|
}
|
||||||
|
dockerfile := []byte(`
|
||||||
|
FROM scratch
|
||||||
|
COPY foo /foo
|
||||||
|
`)
|
||||||
|
dir := tmpdir(
|
||||||
|
t,
|
||||||
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||||
|
fstest.CreateFile("Dockerfile.rego", policyCapsProxyFile, 0600),
|
||||||
|
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||||
|
"build",
|
||||||
|
"--progress=plain",
|
||||||
|
"--output=type=cacheonly",
|
||||||
|
dir,
|
||||||
|
))
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, string(out))
|
||||||
|
require.Contains(t, string(out), "policy enabled network proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBuildPolicyCapsProxyUnsupported(t *testing.T, sb integration.Sandbox) {
|
||||||
|
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||||
|
if buildkitTag() == "master" || matchesBuildKitVersion(t, sb, ">= 0.31.0-0") {
|
||||||
|
t.Skip("BuildKit daemon supports network proxy")
|
||||||
|
}
|
||||||
|
dockerfile := []byte(`
|
||||||
|
FROM scratch
|
||||||
|
COPY foo /foo
|
||||||
|
`)
|
||||||
|
dir := tmpdir(
|
||||||
|
t,
|
||||||
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||||
|
fstest.CreateFile("Dockerfile.rego", policyCapsProxyFile, 0600),
|
||||||
|
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||||
|
"build",
|
||||||
|
"--progress=plain",
|
||||||
|
"--output=type=cacheonly",
|
||||||
|
dir,
|
||||||
|
))
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
require.Error(t, err, string(out))
|
||||||
|
require.Contains(t, string(out), "network proxy requested by policy is not supported by the current BuildKit daemon")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testBuildPolicyAllow(t *testing.T, sb integration.Sandbox) {
|
func testBuildPolicyAllow(t *testing.T, sb integration.Sandbox) {
|
||||||
|
|||||||
Reference in New Issue
Block a user