diff --git a/build/opt.go b/build/opt.go index 9c4699238..4ec2e7f66 100644 --- a/build/opt.go +++ b/build/opt.go @@ -327,7 +327,11 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O } defers = append(defers, releaseLoad) - if opt.Inputs.policy != nil { + if opt.Inputs.policy == nil { + if len(opt.Policy) > 0 { + return nil, nil, errors.New("policy file specified but no policy FS in build context") + } + } else { env := policy.Env{} for k, v := range opt.BuildArgs { if env.Args == nil { @@ -338,16 +342,25 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O env.Filename = path.Base(opt.Inputs.DockerfilePath) env.Target = opt.Target env.Labels = opt.Labels - p := policy.NewPolicy(policy.Opt{ - Files: opt.Inputs.policy.Files, - Env: env, - Log: func(msg string) { - log.Printf("[policy] %s", msg) - }, - FS: opt.Inputs.policy.FS, - Config: cfg, - }) - so.SourcePolicyProvider = policysession.NewPolicyProvider(p.CheckPolicy) + + popts, err := withPolicyConfig(*opt.Inputs.policy, opt.Policy) + if err != nil { + return nil, nil, err + } + var cbs []policysession.PolicyCallback + for _, popt := range popts { + p := policy.NewPolicy(policy.Opt{ + Files: popt.Files, + Env: env, + Log: func(msg string) { + log.Printf("[policy] %s", msg) + }, + FS: opt.Inputs.policy.FS, + Config: cfg, + }) + cbs = append(cbs, p.CheckPolicy) + } + so.SourcePolicyProvider = policysession.NewPolicyProvider(policy.MultiPolicyCallback(cbs...)) } // add node identifier to shared key if one was specified @@ -561,6 +574,25 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro dockerfileName = "Dockerfile" } + p := &policyOpt{ + FS: func() (fs.StatFS, func() error, error) { + if contextDir == "" { + return nil, nil, errors.Errorf("unimplemented, cannot use policy file without a local build context") + } + root, err := os.OpenRoot(contextDir) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to open root for policy file %s.rego", dockerfileName) + } + baseFS := root.FS() + statFS, ok := baseFS.(fs.StatFS) + if !ok { + root.Close() + return nil, nil, errors.Errorf("invalid root FS type %T", baseFS) + } + return statFS, root.Close, nil + }, + } + if dockerfileDir != "" { if err := setLocalMount("dockerfile", dockerfileDir, target); err != nil { return nil, err @@ -573,33 +605,16 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro if err != nil { return nil, errors.Wrapf(err, "failed to read policy file %s.rego", dockerfileName) } - inp.policy = &policyOpt{ - Files: []policy.File{ - { - Filename: dockerfileName + ".rego", - Data: dt, - }, - }, - FS: func() (fs.StatFS, func() error, error) { - if contextDir == "" { - return nil, nil, errors.Errorf("unimplemented, cannot use policy file without a local build context") - } - root, err := os.OpenRoot(contextDir) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to open root for policy file %s.rego", dockerfileName) - } - baseFS := root.FS() - statFS, ok := baseFS.(fs.StatFS) - if !ok { - root.Close() - return nil, nil, errors.Errorf("invalid root FS type %T", baseFS) - } - return statFS, root.Close, nil + p.Files = []policy.File{ + { + Filename: dockerfileName + ".rego", + Data: dt, }, } } } } + inp.policy = p target.FrontendAttrs["filename"] = dockerfileName diff --git a/commands/build.go b/commands/build.go index 85e5fa0d5..8cee3aed4 100644 --- a/commands/build.go +++ b/commands/build.go @@ -260,7 +260,11 @@ func parsePolicyConfigs(in []string) ([]build.PolicyConfig, error) { if value == "" { return nil, errors.Errorf("invalid value %s", field) } - cfg.Files = append(cfg.Files, policy.File{Filename: value}) + dt, err := os.ReadFile(value) + if err != nil { + return nil, errors.Wrapf(err, "failed to read policy file %s", value) + } + cfg.Files = append(cfg.Files, policy.File{Filename: value, Data: dt}) case "reset": b, err := strconv.ParseBool(value) if err != nil { diff --git a/policy/multipolicy.go b/policy/multipolicy.go new file mode 100644 index 000000000..55c79ad12 --- /dev/null +++ b/policy/multipolicy.go @@ -0,0 +1,47 @@ +package policy + +import ( + "context" + + gwpb "github.com/moby/buildkit/frontend/gateway/pb" + moby_buildkit_v1_sourcepolicy "github.com/moby/buildkit/sourcepolicy/pb" + "github.com/moby/buildkit/sourcepolicy/policysession" +) + +// MultiPolicyCallback returns a policy callback that requires all policies to allow. +func MultiPolicyCallback(callbacks ...policysession.PolicyCallback) policysession.PolicyCallback { + return func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + if len(callbacks) == 0 { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + } + + for _, cb := range callbacks { + if cb == nil { + continue + } + decision, metaReq, err := cb(ctx, req) + if err != nil { + return nil, nil, err + } + if metaReq != nil { + return nil, metaReq, nil + } + if decision == nil { + continue + } + + switch decision.Action { + case moby_buildkit_v1_sourcepolicy.PolicyAction_DENY: + return decision, nil, nil + case moby_buildkit_v1_sourcepolicy.PolicyAction_CONVERT: + return decision, nil, nil + case moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW: + // noop + default: + // treat unknown actions as allow + } + } + + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + } +} diff --git a/policy/multipolicy_test.go b/policy/multipolicy_test.go new file mode 100644 index 000000000..ff7a9e2f9 --- /dev/null +++ b/policy/multipolicy_test.go @@ -0,0 +1,129 @@ +package policy + +import ( + "context" + "testing" + + gwpb "github.com/moby/buildkit/frontend/gateway/pb" + moby_buildkit_v1_sourcepolicy "github.com/moby/buildkit/sourcepolicy/pb" + "github.com/moby/buildkit/sourcepolicy/policysession" + solverpb "github.com/moby/buildkit/solver/pb" + "github.com/stretchr/testify/require" +) + +func TestMultiPolicyCallbackNoCallbacks(t *testing.T) { + cb := MultiPolicyCallback() + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, meta) + require.NotNil(t, decision) + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, decision.Action) +} + +func TestMultiPolicyCallbackAllowOnly(t *testing.T) { + cb := MultiPolicyCallback(func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + }) + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, meta) + require.NotNil(t, decision) + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, decision.Action) +} + +func TestMultiPolicyCallbackDenyWins(t *testing.T) { + denyMsgA := &policysession.DenyMessage{Message: "nope-a"} + denyMsgB := &policysession.DenyMessage{Message: "nope-b"} + + cb := MultiPolicyCallback( + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + }, + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{ + Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, + DenyMessages: []*policysession.DenyMessage{denyMsgA, denyMsgB}, + }, nil, nil + }, + ) + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, meta) + require.NotNil(t, decision) + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, decision.Action) + require.Equal(t, []*policysession.DenyMessage{denyMsgA, denyMsgB}, decision.DenyMessages) +} + +func TestMultiPolicyCallbackDenyShortCircuits(t *testing.T) { + called := false + cb := MultiPolicyCallback( + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY}, nil, nil + }, + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + called = true + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + }, + ) + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, meta) + require.NotNil(t, decision) + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, decision.Action) + require.False(t, called) +} + +func TestMultiPolicyCallbackUpdateReturned(t *testing.T) { + update := &solverpb.SourceOp{} + cb := MultiPolicyCallback( + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + }, + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{ + Action: moby_buildkit_v1_sourcepolicy.PolicyAction_CONVERT, + Update: update, + }, nil, nil + }, + ) + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, meta) + require.NotNil(t, decision) + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_CONVERT, decision.Action) + require.Equal(t, update, decision.Update) +} + +func TestMultiPolicyCallbackMetaRequest(t *testing.T) { + metaReq := &gwpb.ResolveSourceMetaRequest{} + cb := MultiPolicyCallback( + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return nil, metaReq, nil + }, + func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY}, nil, nil + }, + ) + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, decision) + require.Equal(t, metaReq, meta) +} + +func TestMultiPolicyCallbackNilCallbackIgnored(t *testing.T) { + cb := MultiPolicyCallback(nil, func(context.Context, *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { + return &policysession.DecisionResponse{Action: moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW}, nil, nil + }) + + decision, meta, err := cb(context.Background(), &policysession.CheckPolicyRequest{}) + require.NoError(t, err) + require.Nil(t, meta) + require.NotNil(t, decision) + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, decision.Action) +}