Skip to content

Commit 38a4ba9

Browse files
authored
Merge pull request #6458 from tonistiigi/policy-deny-msg
policy: propagate deny messages from policy error
2 parents 1a73071 + 5137642 commit 38a4ba9

File tree

5 files changed

+97
-1
lines changed

5 files changed

+97
-1
lines changed

client/client_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
252252
testGitResolveMutatedSource,
253253
testImageResolveAttestationChainRequiresNetwork,
254254
testSourcePolicySession,
255+
testSourcePolicySessionDenyMessages,
255256
testSourceMetaPolicySession,
256257
testSourcePolicyParallelSession,
257258
testSourcePolicySignedCommit,

client/policy_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,40 @@ func testSourcePolicySession(t *testing.T, sb integration.Sandbox) {
147147
}
148148
}
149149

150+
func testSourcePolicySessionDenyMessages(t *testing.T, sb integration.Sandbox) {
151+
requiresLinux(t)
152+
153+
ctx := sb.Context()
154+
155+
c, err := New(ctx, sb.Address())
156+
require.NoError(t, err)
157+
defer c.Close()
158+
159+
def, err := llb.Image("alpine").Marshal(ctx)
160+
require.NoError(t, err)
161+
162+
p := policysession.NewPolicyProvider(func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
163+
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
164+
return &policysession.DecisionResponse{
165+
Action: sourcepolicypb.PolicyAction_DENY,
166+
DenyMessages: []*policysession.DenyMessage{
167+
{Message: "policy blocked alpine"},
168+
{Message: "use busybox instead"},
169+
},
170+
}, nil, nil
171+
})
172+
173+
_, err = c.Solve(ctx, def, SolveOpt{
174+
SourcePolicyProvider: p,
175+
}, nil)
176+
require.Error(t, err)
177+
178+
denyMessages := policysession.DenyMessages(err)
179+
require.Len(t, denyMessages, 2)
180+
require.Equal(t, "policy blocked alpine", denyMessages[0].GetMessage())
181+
require.Equal(t, "use busybox instead", denyMessages[1].GetMessage())
182+
}
183+
150184
func testSourceMetaPolicySession(t *testing.T, sb integration.Sandbox) {
151185
requiresLinux(t)
152186

cmd/buildctl/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
_ "github.com/moby/buildkit/client/connhelper/ssh"
1313
bccommon "github.com/moby/buildkit/cmd/buildctl/common"
1414
"github.com/moby/buildkit/solver/errdefs"
15+
"github.com/moby/buildkit/sourcepolicy/policysession"
1516
"github.com/moby/buildkit/util/apicaps"
1617
"github.com/moby/buildkit/util/appdefaults"
1718
_ "github.com/moby/buildkit/util/grpcutil/encoding/proto"
@@ -146,6 +147,11 @@ func handleErr(debug bool, err error) {
146147
for _, s := range errdefs.Sources(err) {
147148
s.Print(os.Stderr)
148149
}
150+
for _, msg := range policysession.DenyMessages(err) {
151+
if msg.GetMessage() != "" {
152+
fmt.Fprintf(os.Stderr, "policy deny: %s\n", msg.GetMessage())
153+
}
154+
}
149155
if debug {
150156
fmt.Fprintf(os.Stderr, "error: %+v", stack.Formatter(err))
151157
} else {

solver/llbsolver/policy.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ func (p *policyEvaluator) evaluate(ctx context.Context, op *pb.Op, max int) (boo
133133
return true, nil
134134
}
135135
if decision.Action != spb.PolicyAction_ALLOW {
136-
return false, errors.Errorf("source %q not allowed by policy: action %s", source.Identifier, decision.Action.String())
136+
err := errors.Errorf("source %q not allowed by policy: action %s", source.Identifier, decision.Action.String())
137+
return false, policysession.WrapDenyMessages(err, decision.GetDenyMessages())
137138
}
138139
return ok, nil
139140
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package policysession
2+
3+
import (
4+
"github.com/containerd/typeurl/v2"
5+
spb "github.com/moby/buildkit/sourcepolicy/pb"
6+
"github.com/moby/buildkit/util/grpcerrors"
7+
"github.com/pkg/errors"
8+
)
9+
10+
func init() {
11+
typeurl.Register((*DecisionResponse)(nil), "github.com/moby/buildkit", "policysession.DecisionResponse+json")
12+
}
13+
14+
// DenyMessagesError wraps an error with policy deny messages so they can be
15+
// propagated as a typed error detail.
16+
type DenyMessagesError struct {
17+
Messages []*DenyMessage
18+
error
19+
}
20+
21+
func (e *DenyMessagesError) Unwrap() error {
22+
return e.error
23+
}
24+
25+
func (e *DenyMessagesError) ToProto() grpcerrors.TypedErrorProto {
26+
return &DecisionResponse{
27+
Action: spb.PolicyAction_DENY,
28+
DenyMessages: e.Messages,
29+
}
30+
}
31+
32+
// WrapDenyMessages adds deny messages to an error when available.
33+
func WrapDenyMessages(err error, msgs []*DenyMessage) error {
34+
if err == nil || len(msgs) == 0 {
35+
return err
36+
}
37+
return &DenyMessagesError{Messages: msgs, error: err}
38+
}
39+
40+
// DenyMessages extracts policy deny messages from an error chain.
41+
func DenyMessages(err error) []*DenyMessage {
42+
var out []*DenyMessage
43+
var de *DenyMessagesError
44+
if errors.As(err, &de) {
45+
out = DenyMessages(de.Unwrap())
46+
out = append(out, de.Messages...)
47+
}
48+
return out
49+
}
50+
51+
// WrapError implements grpcerrors.TypedErrorProto for DecisionResponse.
52+
func (d *DecisionResponse) WrapError(err error) error {
53+
return WrapDenyMessages(err, d.GetDenyMessages())
54+
}

0 commit comments

Comments
 (0)