Skip to content

Commit 228b90e

Browse files
committed
feat(referrer): add referrer persistance
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent ed792fb commit 228b90e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5029
-27
lines changed

app/controlplane/cmd/wire_gen.go

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

app/controlplane/internal/biz/biz.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var ProviderSet = wire.NewSet(
3535
NewAttestationUseCase,
3636
NewWorkflowRunExpirerUseCase,
3737
NewCASMappingUseCase,
38+
NewReferrerUseCase,
3839
wire.Struct(new(NewIntegrationUseCaseOpts), "*"),
3940
wire.Struct(new(NewUserUseCaseParams), "*"),
4041
)
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
//
2+
// Copyright 2023 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package biz
17+
18+
import (
19+
"bytes"
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"io"
24+
"time"
25+
26+
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
27+
"github.com/chainloop-dev/chainloop/internal/servicelogger"
28+
"github.com/go-kratos/kratos/v2/log"
29+
cr_v1 "github.com/google/go-containerregistry/pkg/v1"
30+
"github.com/google/uuid"
31+
v1 "github.com/in-toto/attestation/go/v1"
32+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
33+
)
34+
35+
type Referrer struct {
36+
Digest string
37+
ArtifactType string
38+
// Wether the item is downloadable from CAS or not
39+
Downloadable bool
40+
// points to other digests
41+
References []string
42+
}
43+
44+
// Actual referrer stored in the DB which includes a nested list of storedReferences
45+
type StoredReferrer struct {
46+
ID uuid.UUID
47+
Digest string
48+
ArtifactType string
49+
// Wether the item is downloadable from CAS or not
50+
Downloadable bool
51+
CreatedAt *time.Time
52+
// Fully expanded list of 1-level off references
53+
References []*StoredReferrer
54+
OrgIDs []uuid.UUID
55+
}
56+
57+
type ReferrerMap map[string]*Referrer
58+
59+
type ReferrerRepo interface {
60+
Save(ctx context.Context, input ReferrerMap, orgID uuid.UUID) error
61+
GetFromRoot(ctx context.Context, digest string) (*StoredReferrer, error)
62+
}
63+
64+
type ReferrerUseCase struct {
65+
repo ReferrerRepo
66+
orgRepo OrganizationRepo
67+
logger *log.Helper
68+
}
69+
70+
func NewReferrerUseCase(repo ReferrerRepo, orgRepo OrganizationRepo, l log.Logger) *ReferrerUseCase {
71+
if l == nil {
72+
l = log.NewStdLogger(io.Discard)
73+
}
74+
75+
return &ReferrerUseCase{repo, orgRepo, servicelogger.ScopedHelper(l, "biz/Referrer")}
76+
}
77+
78+
// ExtractAndPersist extracts the referrers (subject + materials) from the given attestation
79+
// and store it as part of the referrers index table
80+
func (s *ReferrerUseCase) ExtractAndPersist(ctx context.Context, att *dsse.Envelope, orgID string) error {
81+
orgUUID, err := uuid.Parse(orgID)
82+
if err != nil {
83+
return NewErrInvalidUUID(err)
84+
}
85+
86+
if org, err := s.orgRepo.FindByID(ctx, orgUUID); err != nil {
87+
return fmt.Errorf("finding organization: %w", err)
88+
} else if org == nil {
89+
return NewErrNotFound("organization")
90+
}
91+
92+
m, err := extractReferrers(att)
93+
if err != nil {
94+
return fmt.Errorf("extracting referrers: %w", err)
95+
}
96+
97+
if err := s.repo.Save(ctx, m, orgUUID); err != nil {
98+
return fmt.Errorf("saving referrers: %w", err)
99+
}
100+
101+
return nil
102+
}
103+
104+
// GetFromRoot returns the referrer identified by the provided content digest, including its first-level references
105+
// For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it
106+
// TODO:(miguel) authz by user similar to what we do with CASmapping
107+
func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest string) (*StoredReferrer, error) {
108+
ref, err := s.repo.GetFromRoot(ctx, digest)
109+
if err != nil {
110+
return nil, fmt.Errorf("getting referrer from root: %w", err)
111+
} else if ref == nil {
112+
return nil, NewErrNotFound("referrer")
113+
}
114+
115+
return ref, nil
116+
}
117+
118+
const (
119+
referrerAttestationType = "ATTESTATION"
120+
referrerGitHeadType = "GIT_HEAD_COMMIT"
121+
)
122+
123+
// ExtractReferrers extracts the referrers from the given attestation
124+
// this means
125+
// 1 - write an entry for the attestation itself
126+
// 2 - then to all the materials contained in the predicate
127+
// 3 - and the subjects (some of them)
128+
// 4 - creating link between the attestation and the materials/subjects as needed
129+
// see tests for examples
130+
func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) {
131+
// Calculate the attestation hash
132+
jsonAtt, err := json.Marshal(att)
133+
if err != nil {
134+
return nil, fmt.Errorf("marshaling attestation: %w", err)
135+
}
136+
137+
// Calculate the attestation hash
138+
h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonAtt))
139+
if err != nil {
140+
return nil, fmt.Errorf("calculating attestation hash: %w", err)
141+
}
142+
143+
referrers := make(ReferrerMap)
144+
// 1 - Attestation referrer
145+
// Add the attestation itself as a referrer to the map without references yet
146+
attestationHash := h.String()
147+
referrers[attestationHash] = &Referrer{
148+
Digest: attestationHash,
149+
ArtifactType: referrerAttestationType,
150+
Downloadable: true,
151+
}
152+
153+
// 2 - Predicate that's referenced from the attestation
154+
predicate, err := chainloop.ExtractPredicate(att)
155+
if err != nil {
156+
return nil, fmt.Errorf("extracting predicate: %w", err)
157+
}
158+
159+
// Create new referrers for each material
160+
// and link them to the attestation
161+
for _, material := range predicate.GetMaterials() {
162+
// Skip materials that don't have a digest
163+
if material.Hash == nil {
164+
continue
165+
}
166+
167+
// Create its referrer entry if it doesn't exist yet
168+
// the reason it might exist is because you might be attaching the same material twice
169+
// i.e the same SBOM twice, in that case we don't want to create a new referrer
170+
// If we are providing different types for the same digest, we should error out
171+
if r, ok := referrers[material.Hash.String()]; ok {
172+
if r.ArtifactType != material.Type {
173+
return nil, fmt.Errorf("material %s has different types: %s and %s", material.Hash.String(), r.ArtifactType, material.Type)
174+
}
175+
176+
continue
177+
}
178+
179+
referrers[material.Hash.String()] = &Referrer{
180+
Digest: material.Hash.String(),
181+
ArtifactType: material.Type,
182+
Downloadable: material.UploadedToCAS,
183+
}
184+
185+
// Add the reference to the attestation
186+
referrers[attestationHash].References = append(referrers[attestationHash].References, material.Hash.String())
187+
}
188+
189+
// 3 - Subject that points to the attestation
190+
statement, err := chainloop.ExtractStatement(att)
191+
if err != nil {
192+
return nil, fmt.Errorf("extracting predicate: %w", err)
193+
}
194+
195+
for _, subject := range statement.Subject {
196+
subjectRef, err := intotoSubjectToReferrer(subject)
197+
if err != nil {
198+
return nil, fmt.Errorf("transforming subject to referrer: %w", err)
199+
}
200+
201+
if subjectRef == nil {
202+
continue
203+
}
204+
205+
// check if we already have a referrer for this digest and set it otherwise
206+
// this is the case for example for git.Head ones
207+
if _, ok := referrers[subjectRef.Digest]; !ok {
208+
referrers[subjectRef.Digest] = subjectRef
209+
// add it to the list of of attestation-referenced digests
210+
referrers[attestationHash].References = append(referrers[attestationHash].References, subjectRef.Digest)
211+
}
212+
213+
// Update referrer to point to the attestation
214+
referrers[subjectRef.Digest].References = []string{attestationHash}
215+
}
216+
217+
return referrers, nil
218+
}
219+
220+
// transforms the in-toto subject to a referrer by deterministically picking
221+
// the subject types we care about (and return nil otherwise), for now we just care about the subjects
222+
// - git.Head and
223+
// - material types
224+
func intotoSubjectToReferrer(r *v1.ResourceDescriptor) (*Referrer, error) {
225+
var digestStr string
226+
for alg, val := range r.Digest {
227+
digestStr = fmt.Sprintf("%s:%s", alg, val)
228+
break
229+
}
230+
231+
// it's a.git head type
232+
if r.Name == chainloop.SubjectGitHead {
233+
if digestStr == "" {
234+
return nil, fmt.Errorf("no digest found for subject %s", r.Name)
235+
}
236+
237+
return &Referrer{
238+
Digest: digestStr,
239+
ArtifactType: referrerGitHeadType,
240+
}, nil
241+
}
242+
243+
// Iterate on material types
244+
var materialType string
245+
var uploadedToCAS bool
246+
// it's a material type
247+
for k, v := range r.Annotations.AsMap() {
248+
// It's a material type
249+
if k == chainloop.AnnotationMaterialType {
250+
materialType = v.(string)
251+
} else if k == chainloop.AnnotationMaterialCAS {
252+
uploadedToCAS = v.(bool)
253+
}
254+
}
255+
256+
// it's not a material type
257+
if materialType == "" {
258+
return nil, nil
259+
}
260+
261+
if digestStr == "" {
262+
return nil, fmt.Errorf("no digest found for subject %s", r.Name)
263+
}
264+
265+
return &Referrer{
266+
Digest: digestStr,
267+
ArtifactType: materialType,
268+
Downloadable: uploadedToCAS,
269+
}, nil
270+
}

0 commit comments

Comments
 (0)