Skip to content

Commit ca80412

Browse files
authored
feat: resolve policy evaluations from CAS in workflow-run View API (#2953)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent fa49310 commit ca80412

8 files changed

Lines changed: 251 additions & 29 deletions

File tree

app/controlplane/cmd/wire.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,13 @@ func newAuthAllowList(conf *conf.Bootstrap) *pkgConf.AllowList {
138138
var cacheProviderSet = wire.NewSet(
139139
newMembershipsCache,
140140
newClaimsCache,
141+
newPolicyEvalBundleCache,
141142
)
142143

143144
func newClaimsCache(conn *nats.Conn, logger log.Logger) (cache.Cache[*jwt.MapClaims], error) {
144145
l := log.NewHelper(logger)
145146
backend := "memory"
146-
opts := []cache.Option{cache.WithTTL(10 * time.Second), cache.WithLogger(&kratosLogAdapter{h: l})}
147+
opts := []cache.Option{cache.WithTTL(10 * time.Second), cache.WithLogger(&kratosLogAdapter{h: l}), cache.WithDescription("Cache for JWT claims")}
147148
if conn != nil {
148149
backend = "nats"
149150
opts = append(opts, cache.WithNATS(conn, "chainloop-jwt-claims"))
@@ -155,7 +156,7 @@ func newClaimsCache(conn *nats.Conn, logger log.Logger) (cache.Cache[*jwt.MapCla
155156
func newMembershipsCache(conn *nats.Conn, logger log.Logger) (cache.Cache[*entities.Membership], error) {
156157
l := log.NewHelper(logger)
157158
backend := "memory"
158-
opts := []cache.Option{cache.WithTTL(time.Second), cache.WithLogger(&kratosLogAdapter{h: l})}
159+
opts := []cache.Option{cache.WithTTL(time.Second), cache.WithLogger(&kratosLogAdapter{h: l}), cache.WithDescription("Cache for org memberships")}
159160
if conn != nil {
160161
backend = "nats"
161162
opts = append(opts, cache.WithNATS(conn, "chainloop-memberships"))
@@ -164,6 +165,18 @@ func newMembershipsCache(conn *nats.Conn, logger log.Logger) (cache.Cache[*entit
164165
return cache.New[*entities.Membership](opts...)
165166
}
166167

168+
func newPolicyEvalBundleCache(conn *nats.Conn, logger log.Logger) (cache.Cache[[]byte], error) {
169+
l := log.NewHelper(logger)
170+
backend := "memory"
171+
opts := []cache.Option{cache.WithTTL(24 * time.Hour), cache.WithLogger(&kratosLogAdapter{h: l}), cache.WithDescription("Cache for policy evaluation bundles from CAS")}
172+
if conn != nil {
173+
backend = "nats"
174+
opts = append(opts, cache.WithNATS(conn, "chainloop-policy-eval-bundles"))
175+
}
176+
l.Infow("msg", "cache initialized", "bucket", "chainloop-policy-eval-bundles", "backend", backend, "ttl", "24h")
177+
return cache.New[[]byte](opts...)
178+
}
179+
167180
// kratosLogAdapter adapts kratos log.Helper (Debugw(...interface{})) to cache.Logger (Debugw(string, ...any)).
168181
type kratosLogAdapter struct{ h *log.Helper }
169182

app/controlplane/cmd/wire_gen.go

Lines changed: 25 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/service/attestation.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ func (s *AttestationService) GetPolicyGroup(ctx context.Context, req *cpAPI.Atte
533533
}}, nil
534534
}
535535

536-
func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) {
536+
func bizAttestationToPb(att *biz.Attestation, predicate chainloop.NormalizablePredicate) (*cpAPI.AttestationItem, error) {
537537
if att == nil || att.Envelope == nil {
538538
return nil, nil
539539
}
@@ -543,11 +543,6 @@ func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) {
543543
return nil, err
544544
}
545545

546-
predicate, err := chainloop.ExtractPredicate(att.Envelope)
547-
if err != nil {
548-
return nil, fmt.Errorf("error extracting predicate from attestation: %w", err)
549-
}
550-
551546
materials, err := extractMaterials(predicate.GetMaterials())
552547
if err != nil {
553548
return nil, fmt.Errorf("error extracting materials from attestation: %w", err)

app/controlplane/internal/service/workflowrun.go

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024-2025 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616
package service
1717

1818
import (
19+
"bytes"
1920
"context"
2021
"fmt"
2122
"slices"
@@ -25,9 +26,12 @@ import (
2526
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2627
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
2728
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination"
29+
chainloop "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
30+
"github.com/chainloop-dev/chainloop/pkg/cache"
2831
"github.com/chainloop-dev/chainloop/pkg/credentials"
2932
errors "github.com/go-kratos/kratos/v2/errors"
3033
"github.com/google/uuid"
34+
intoto "github.com/in-toto/attestation/go/v1"
3135
"google.golang.org/protobuf/types/known/timestamppb"
3236
)
3337

@@ -40,6 +44,9 @@ type WorkflowRunService struct {
4044
workflowContractUseCase *biz.WorkflowContractUseCase
4145
projectUseCase *biz.ProjectUseCase
4246
credsReader credentials.Reader
47+
casClient biz.CASClient
48+
casMappingUC *biz.CASMappingUseCase
49+
policyEvalCache cache.Cache[[]byte]
4350
}
4451

4552
type NewWorkflowRunServiceOpts struct {
@@ -48,6 +55,9 @@ type NewWorkflowRunServiceOpts struct {
4855
WorkflowContractUC *biz.WorkflowContractUseCase
4956
ProjectUC *biz.ProjectUseCase
5057
CredsReader credentials.Reader
58+
CASClient biz.CASClient
59+
CASMappingUC *biz.CASMappingUseCase
60+
PolicyEvalCache cache.Cache[[]byte]
5161
Opts []NewOpt
5262
}
5363

@@ -59,9 +69,56 @@ func NewWorkflowRunService(opts *NewWorkflowRunServiceOpts) *WorkflowRunService
5969
workflowContractUseCase: opts.WorkflowContractUC,
6070
projectUseCase: opts.ProjectUC,
6171
credsReader: opts.CredsReader,
72+
casClient: opts.CASClient,
73+
casMappingUC: opts.CASMappingUC,
74+
policyEvalCache: opts.PolicyEvalCache,
6275
}
6376
}
6477

78+
type casResolvedPredicate struct {
79+
chainloop.NormalizablePredicate
80+
evals map[string][]*chainloop.PolicyEvaluation
81+
}
82+
83+
func (p *casResolvedPredicate) GetPolicyEvaluations() map[string][]*chainloop.PolicyEvaluation {
84+
return p.evals
85+
}
86+
87+
func (s *WorkflowRunService) resolvePolicyEvaluations(
88+
ctx context.Context,
89+
ref *intoto.ResourceDescriptor,
90+
orgID uuid.UUID,
91+
) (map[string][]*chainloop.PolicyEvaluation, error) {
92+
if ref == nil {
93+
return nil, nil
94+
}
95+
96+
hexDigest, ok := ref.Digest["sha256"]
97+
if !ok {
98+
return nil, fmt.Errorf("no sha256 digest in policy evaluations ref")
99+
}
100+
digest := fmt.Sprintf("sha256:%s", hexDigest)
101+
102+
if cached, found, err := s.policyEvalCache.Get(ctx, digest); err == nil && found {
103+
return chainloop.PolicyEvaluationsFromBundle(cached)
104+
}
105+
106+
mapping, err := s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, digest, []uuid.UUID{orgID}, nil)
107+
if err != nil {
108+
return nil, fmt.Errorf("finding CAS mapping: %w", err)
109+
}
110+
111+
var buf bytes.Buffer
112+
if err := s.casClient.Download(ctx, string(mapping.CASBackend.Provider), mapping.CASBackend.SecretName, &buf, digest); err != nil {
113+
return nil, fmt.Errorf("downloading policy eval bundle: %w", err)
114+
}
115+
116+
data := buf.Bytes()
117+
_ = s.policyEvalCache.Set(ctx, digest, data)
118+
119+
return chainloop.PolicyEvaluationsFromBundle(data)
120+
}
121+
65122
func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServiceListRequest) (*pb.WorkflowRunServiceListResponse, error) {
66123
currentOrg, err := requireCurrentOrg(ctx)
67124
if err != nil {
@@ -185,7 +242,24 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic
185242
verificationResult = bizVerificationToPb(vr)
186243
}
187244

188-
attestation, err := bizAttestationToPb(run.Attestation)
245+
var predicate chainloop.NormalizablePredicate
246+
if run.Attestation != nil && run.Attestation.Envelope != nil {
247+
predicate, err = chainloop.ExtractPredicate(run.Attestation.Envelope)
248+
if err != nil {
249+
return nil, handleUseCaseErr(err, s.log)
250+
}
251+
252+
if ref := predicate.GetPolicyEvaluationsRef(); ref != nil {
253+
resolved, resolveErr := s.resolvePolicyEvaluations(ctx, ref, run.Workflow.OrgID)
254+
if resolveErr != nil {
255+
s.log.Warnw("msg", "failed to resolve policy evaluations from CAS, using inline", "err", resolveErr)
256+
} else if resolved != nil {
257+
predicate = &casResolvedPredicate{NormalizablePredicate: predicate, evals: resolved}
258+
}
259+
}
260+
}
261+
262+
attestation, err := bizAttestationToPb(run.Attestation, predicate)
189263
if err != nil {
190264
return nil, handleUseCaseErr(err, s.log)
191265
}

pkg/attestation/renderer/chainloop/v02.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,27 @@ func (r *RendererV02) predicate() (*structpb.Struct, error) {
288288
return predicate, nil
289289
}
290290

291-
// collect all policy evaluations grouped by material and returns if there is a policy violation
292291
func mappedPolicyEvaluations(att *v1.Attestation) (map[string][]*PolicyEvaluation, bool, error) {
293-
var hasPolicyViolations bool
294-
result := map[string][]*PolicyEvaluation{}
292+
return groupEvaluations(att.GetPolicyEvaluations())
293+
}
294+
295+
// PolicyEvaluationsFromBundle deserializes a PolicyEvaluationBundle from protojson bytes
296+
// and returns evaluations grouped by material name.
297+
func PolicyEvaluationsFromBundle(data []byte) (map[string][]*PolicyEvaluation, error) {
298+
var bundle v1.PolicyEvaluationBundle
299+
if err := protojson.Unmarshal(data, &bundle); err != nil {
300+
return nil, fmt.Errorf("unmarshaling policy evaluation bundle: %w", err)
301+
}
302+
303+
evals, _, err := groupEvaluations(bundle.GetEvaluations())
304+
return evals, err
305+
}
306+
307+
func groupEvaluations(evals []*v1.PolicyEvaluation) (map[string][]*PolicyEvaluation, bool, error) {
308+
var hasViolations bool
309+
result := make(map[string][]*PolicyEvaluation)
295310

296-
for _, p := range att.GetPolicyEvaluations() {
311+
for _, p := range evals {
297312
keyName := p.MaterialName
298313
if keyName == "" {
299314
keyName = AttPolicyEvaluation
@@ -305,13 +320,13 @@ func mappedPolicyEvaluations(att *v1.Attestation) (map[string][]*PolicyEvaluatio
305320
}
306321

307322
if len(ev.Violations) > 0 {
308-
hasPolicyViolations = true
323+
hasViolations = true
309324
}
310325

311326
result[keyName] = append(result[keyName], ev)
312327
}
313328

314-
return result, hasPolicyViolations, nil
329+
return result, hasViolations, nil
315330
}
316331

317332
func renderEvaluation(ev *v1.PolicyEvaluation) (*PolicyEvaluation, error) {

0 commit comments

Comments
 (0)