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:
Tonis Tiigi
2026-06-10 15:04:23 +02:00
committed by CrazyMax
parent 2fcfce0e71
commit 4da04e3bf4
8 changed files with 431 additions and 57 deletions
+14
View File
@@ -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,
}
}
+15 -5
View File
@@ -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 {
+145 -52
View File
@@ -130,27 +130,7 @@ func (p *Policy) IsPolicyError(err error) bool {
return false
}
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")
}
func (p *Policy) regoBaseOpts() ([]func(*rego.Rego), func(), error) {
caps := &ast.Capabilities{
Builtins: builtins(),
Features: slices.Clone(ast.Features),
@@ -171,11 +151,11 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
var root fs.StatFS
var closeFS func() error
defer func() {
closeRoot := func() {
if closeFS != nil {
closeFS()
}
}()
}
comp = comp.WithModuleLoader(func(resolved map[string]*ast.Module) (parsed map[string]*ast.Module, err error) {
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)))
}
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))
for range maxResolveIterations {
@@ -304,42 +314,23 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
continue
}
if len(rs) == 0 {
return nil, nil, errors.Errorf("policy returned zero result")
decision, err := policyDecisionFromResult(rs)
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{
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 vv, ok := v.(bool); !ok {
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 decision.Allow != nil && *decision.Allow {
resp.Action = moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW
}
if v, ok := vt["deny_msg"]; ok {
if vv, ok := v.([]any); ok {
for _, m := range vv {
if m, ok := m.(string); ok {
resp.DenyMessages = append(resp.DenyMessages, &policysession.DenyMessage{
Message: m,
})
}
}
}
for _, m := range decision.DenyMessages {
resp.DenyMessages = append(resp.DenyMessages, &policysession.DenyMessage{
Message: m,
})
}
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")
}
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) {
var resolver SourceMetadataResolver
if p.opt.SourceResolver != nil {
+80
View File
@@ -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 {
t.Helper()
dt, err := json.Marshal(img)