-
Notifications
You must be signed in to change notification settings - Fork 25
/
referrer.go
409 lines (346 loc) · 12.4 KB
/
referrer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//
// Copyright 2023 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package biz
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"time"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
"github.com/chainloop-dev/chainloop/internal/servicelogger"
"github.com/go-kratos/kratos/v2/log"
cr_v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/uuid"
v1 "github.com/in-toto/attestation/go/v1"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
type ReferrerUseCase struct {
repo ReferrerRepo
membershipRepo MembershipRepo
workflowRepo WorkflowRepo
logger *log.Helper
indexConfig *conf.ReferrerSharedIndex
}
func NewReferrerUseCase(repo ReferrerRepo, wfRepo WorkflowRepo, mRepo MembershipRepo, indexCfg *conf.ReferrerSharedIndex, l log.Logger) (*ReferrerUseCase, error) {
if l == nil {
l = log.NewStdLogger(io.Discard)
}
logger := servicelogger.ScopedHelper(l, "biz/referrer")
if indexCfg != nil {
if err := indexCfg.ValidateOrgs(); err != nil {
return nil, fmt.Errorf("invalid shared index config: %w", err)
}
if indexCfg.Enabled {
logger.Infow("msg", "shared index enabled", "allowedOrgs", indexCfg.AllowedOrgs)
}
}
return &ReferrerUseCase{
repo: repo,
membershipRepo: mRepo,
indexConfig: indexCfg,
workflowRepo: wfRepo,
logger: logger,
}, nil
}
type ReferrerRepo interface {
Save(ctx context.Context, input []*Referrer, workflowID uuid.UUID) error
// GetFromRoot returns the referrer identified by the provided content digest, including its first-level references
// For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it
// OrgIDs represent an allowList of organizations where the referrers should be looked for
GetFromRoot(ctx context.Context, digest string, orgIDS []uuid.UUID, filters ...GetFromRootFilter) (*StoredReferrer, error)
}
type Referrer struct {
Digest string
Kind string
// Wether the item is downloadable from CAS or not
Downloadable bool
// If this referrer is part of a public workflow
InPublicWorkflow bool
References []*Referrer
}
// Actual referrer stored in the DB which includes a nested list of storedReferences
type StoredReferrer struct {
*Referrer
ID uuid.UUID
CreatedAt *time.Time
// Fully expanded list of 1-level off references
References []*StoredReferrer
OrgIDs, WorkflowIDs []uuid.UUID
}
type GetFromRootFilters struct {
// RootKind is the kind of the root referrer, i.e ATTESTATION
RootKind *string
// Wether to filter by visibility or not
Public *bool
}
type GetFromRootFilter func(*GetFromRootFilters)
func WithKind(kind string) func(*GetFromRootFilters) {
return func(o *GetFromRootFilters) {
o.RootKind = &kind
}
}
func WithPublicVisibility(public bool) func(*GetFromRootFilters) {
return func(o *GetFromRootFilters) {
o.Public = &public
}
}
// ExtractAndPersist extracts the referrers (subject + materials) from the given attestation
// and store it as part of the referrers index table
func (s *ReferrerUseCase) ExtractAndPersist(ctx context.Context, att *dsse.Envelope, workflowID string) error {
workflowUUID, err := uuid.Parse(workflowID)
if err != nil {
return NewErrInvalidUUID(err)
}
// Check that the workflow belongs to the organization
if wf, err := s.workflowRepo.FindByID(ctx, workflowUUID); err != nil {
return fmt.Errorf("finding workflow: %w", err)
} else if wf == nil {
return NewErrNotFound("workflow")
}
m, err := extractReferrers(att)
if err != nil {
return fmt.Errorf("extracting referrers: %w", err)
}
if err := s.repo.Save(ctx, m, workflowUUID); err != nil {
return fmt.Errorf("saving referrers: %w", err)
}
return nil
}
// GetFromRoot returns the referrer identified by the provided content digest, including its first-level references
// For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it
// It only returns referrers that belong to organizations the user is member of
func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind, userID string) (*StoredReferrer, error) {
userUUID, err := uuid.Parse(userID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}
// We pass the list of organizationsIDs from where to look for the referrer
// For now we just pass the list of organizations the user is member of
// in the future we will expand this to publicly available orgs and so on.
memberships, err := s.membershipRepo.FindByUser(ctx, userUUID)
if err != nil {
return nil, fmt.Errorf("finding memberships: %w", err)
}
orgIDs := make([]uuid.UUID, 0, len(memberships))
for _, m := range memberships {
orgIDs = append(orgIDs, m.OrganizationID)
}
filters := make([]GetFromRootFilter, 0)
if rootKind != "" {
filters = append(filters, WithKind(rootKind))
}
ref, err := s.repo.GetFromRoot(ctx, digest, orgIDs, filters...)
if err != nil {
if errors.As(err, &ErrAmbiguousReferrer{}) {
return nil, NewErrValidation(fmt.Errorf("please provide the referrer kind: %w", err))
}
return nil, fmt.Errorf("getting referrer from root: %w", err)
} else if ref == nil {
return nil, NewErrNotFound("referrer")
}
return ref, nil
}
// Get the list of public referrers from organizations
// that have been allowed to be shown in a shared index
// NOTE: This is a public endpoint under /discover/[sha256:deadbeef]
func (s *ReferrerUseCase) GetFromRootInPublicSharedIndex(ctx context.Context, digest, rootKind string) (*StoredReferrer, error) {
if s.indexConfig == nil || !s.indexConfig.Enabled {
return nil, NewErrUnauthorizedStr("shared referrer index functionality is not enabled")
}
if _, err := cr_v1.NewHash(digest); err != nil {
return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err))
}
// Load the organizations that are allowed to appear in the shared index
orgIDs := make([]uuid.UUID, 0)
for _, orgID := range s.indexConfig.AllowedOrgs {
orgUUID, err := uuid.Parse(orgID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}
orgIDs = append(orgIDs, orgUUID)
}
// and ask only for the public referrers of those orgs
filters := []GetFromRootFilter{WithPublicVisibility(true)}
if rootKind != "" {
filters = append(filters, WithKind(rootKind))
}
ref, err := s.repo.GetFromRoot(ctx, digest, orgIDs, filters...)
if err != nil {
if errors.As(err, &ErrAmbiguousReferrer{}) {
return nil, NewErrValidation(fmt.Errorf("please provide the referrer kind: %w", err))
}
return nil, fmt.Errorf("getting referrer from root: %w", err)
} else if ref == nil {
return nil, NewErrNotFound("referrer")
}
return ref, nil
}
const (
referrerAttestationType = "ATTESTATION"
referrerGitHeadType = "GIT_HEAD_COMMIT"
)
func newRef(digest, kind string) string {
return fmt.Sprintf("%s-%s", kind, digest)
}
func (r *Referrer) MapID() string {
return newRef(r.Digest, r.Kind)
}
// ExtractReferrers extracts the referrers from the given attestation
// this means
// 1 - write an entry for the attestation itself
// 2 - then to all the materials contained in the predicate
// 3 - and the subjects (some of them)
// 4 - creating link between the attestation and the materials/subjects as needed
// see tests for examples
func extractReferrers(att *dsse.Envelope) ([]*Referrer, error) {
// Calculate the attestation hash
jsonAtt, err := json.Marshal(att)
if err != nil {
return nil, fmt.Errorf("marshaling attestation: %w", err)
}
// Calculate the attestation hash
h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonAtt))
if err != nil {
return nil, fmt.Errorf("calculating attestation hash: %w", err)
}
referrersMap := make(map[string]*Referrer)
// 1 - Attestation referrer
// Add the attestation itself as a referrer to the map without references yet
attestationHash := h.String()
attestationReferrer := &Referrer{
Digest: attestationHash,
Kind: referrerAttestationType,
Downloadable: true,
}
referrersMap[newRef(attestationHash, referrerAttestationType)] = attestationReferrer
// 2 - Predicate that's referenced from the attestation
predicate, err := chainloop.ExtractPredicate(att)
if err != nil {
return nil, fmt.Errorf("extracting predicate: %w", err)
}
// Create new referrers for each material
// and link them to the attestation
for _, material := range predicate.GetMaterials() {
// Skip materials that don't have a digest
if material.Hash == nil {
continue
}
// Create its referrer entry if it doesn't exist yet
// the reason it might exist is because you might be attaching the same material twice
// i.e the same SBOM twice, in that case we don't want to create a new referrer
materialRef := newRef(material.Hash.String(), material.Type)
if _, ok := referrersMap[materialRef]; ok {
continue
}
referrersMap[materialRef] = &Referrer{
Digest: material.Hash.String(),
Kind: material.Type,
Downloadable: material.UploadedToCAS,
}
materialReferrer := referrersMap[materialRef]
// Add the reference to the attestation
attestationReferrer.References = append(attestationReferrer.References, &Referrer{
Digest: materialReferrer.Digest, Kind: materialReferrer.Kind,
})
}
// 3 - Subject that points to the attestation
statement, err := chainloop.ExtractStatement(att)
if err != nil {
return nil, fmt.Errorf("extracting predicate: %w", err)
}
for _, subject := range statement.Subject {
subjectReferrer, err := intotoSubjectToReferrer(subject)
if err != nil {
return nil, fmt.Errorf("transforming subject to referrer: %w", err)
}
if subjectReferrer == nil {
continue
}
subjectRef := newRef(subjectReferrer.Digest, subjectReferrer.Kind)
// check if we already have a referrer for this digest and set it otherwise
// this is the case for example for git.Head ones
if _, ok := referrersMap[subjectRef]; !ok {
referrersMap[subjectRef] = subjectReferrer
// add it to the list of of attestation-referenced digests
attestationReferrer.References = append(attestationReferrer.References,
&Referrer{
Digest: subjectReferrer.Digest, Kind: subjectReferrer.Kind,
})
}
// Update referrer to point to the attestation
referrersMap[subjectRef].References = []*Referrer{{Digest: attestationReferrer.Digest, Kind: attestationReferrer.Kind}}
}
// Return a sorted list of referrers
mapKeys := make([]string, 0, len(referrersMap))
for k := range referrersMap {
mapKeys = append(mapKeys, k)
}
sort.Strings(mapKeys)
referrers := make([]*Referrer, 0, len(referrersMap))
for _, k := range mapKeys {
referrers = append(referrers, referrersMap[k])
}
return referrers, nil
}
// transforms the in-toto subject to a referrer by deterministically picking
// the subject types we care about (and return nil otherwise), for now we just care about the subjects
// - git.Head and
// - material types
func intotoSubjectToReferrer(r *v1.ResourceDescriptor) (*Referrer, error) {
var digestStr string
for alg, val := range r.Digest {
digestStr = fmt.Sprintf("%s:%s", alg, val)
break
}
// it's a.git head type
if r.Name == chainloop.SubjectGitHead {
if digestStr == "" {
return nil, fmt.Errorf("no digest found for subject %s", r.Name)
}
return &Referrer{
Digest: digestStr,
Kind: referrerGitHeadType,
}, nil
}
// Iterate on material types
var materialType string
var uploadedToCAS bool
// it's a material type
for k, v := range r.Annotations.AsMap() {
// It's a material type
if k == chainloop.AnnotationMaterialType {
materialType = v.(string)
} else if k == chainloop.AnnotationMaterialCAS {
uploadedToCAS = v.(bool)
}
}
// it's not a material type
if materialType == "" {
return nil, nil
}
if digestStr == "" {
return nil, fmt.Errorf("no digest found for subject %s", r.Name)
}
return &Referrer{
Digest: digestStr,
Kind: materialType,
Downloadable: uploadedToCAS,
}, nil
}