Skip to content

Commit d5d0728

Browse files
authored
feat(referrer): support same digest for two different kinds (#431)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 36237b0 commit d5d0728

File tree

5 files changed

+244
-123
lines changed

5 files changed

+244
-123
lines changed

app/controlplane/internal/biz/errors.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,19 @@ func (e ErrUnauthorized) Error() string {
9191
func IsErrUnauthorized(err error) bool {
9292
return errors.As(err, &ErrUnauthorized{})
9393
}
94+
95+
// A referrer with the same digest points to two different artifact types
96+
// and we require filtering out which one
97+
type ErrAmbiguousReferrer struct {
98+
digest string
99+
// what kinds contain duplicates
100+
kinds []string
101+
}
102+
103+
func NewErrReferrerAmbiguous(digest string, kinds []string) error {
104+
return ErrAmbiguousReferrer{digest, kinds}
105+
}
106+
107+
func (e ErrAmbiguousReferrer) Error() string {
108+
return fmt.Sprintf("digest %s present in %d kinds %q", e.digest, len(e.kinds), e.kinds)
109+
}

app/controlplane/internal/biz/referrer.go

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import (
1919
"bytes"
2020
"context"
2121
"encoding/json"
22+
"errors"
2223
"fmt"
2324
"io"
25+
"sort"
2426
"time"
2527

2628
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
@@ -37,27 +39,21 @@ type Referrer struct {
3739
Kind string
3840
// Wether the item is downloadable from CAS or not
3941
Downloadable bool
40-
// points to other digests
41-
References []string
42+
References []*Referrer
4243
}
4344

4445
// Actual referrer stored in the DB which includes a nested list of storedReferences
4546
type StoredReferrer struct {
46-
ID uuid.UUID
47-
Digest string
48-
Kind string
49-
// Wether the item is downloadable from CAS or not
50-
Downloadable bool
51-
CreatedAt *time.Time
47+
*Referrer
48+
ID uuid.UUID
49+
CreatedAt *time.Time
5250
// Fully expanded list of 1-level off references
5351
References []*StoredReferrer
5452
OrgIDs []uuid.UUID
5553
}
5654

57-
type ReferrerMap map[string]*Referrer
58-
5955
type ReferrerRepo interface {
60-
Save(ctx context.Context, input ReferrerMap, orgID uuid.UUID) error
56+
Save(ctx context.Context, input []*Referrer, orgID uuid.UUID) error
6157
// GetFromRoot returns the referrer identified by the provided content digest, including its first-level references
6258
// For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it
6359
// OrgIDs represent an allowList of organizations where the referrers should be looked for
@@ -129,6 +125,10 @@ func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest string, userID
129125

130126
ref, err := s.repo.GetFromRoot(ctx, digest, orgIDs)
131127
if err != nil {
128+
if errors.As(err, &ErrAmbiguousReferrer{}) {
129+
return nil, NewErrValidation(fmt.Errorf("please provide the referrer kind: %w", err))
130+
}
131+
132132
return nil, fmt.Errorf("getting referrer from root: %w", err)
133133
} else if ref == nil {
134134
return nil, NewErrNotFound("referrer")
@@ -142,14 +142,22 @@ const (
142142
referrerGitHeadType = "GIT_HEAD_COMMIT"
143143
)
144144

145+
func newRef(digest, kind string) string {
146+
return fmt.Sprintf("%s-%s", kind, digest)
147+
}
148+
149+
func (r *Referrer) MapID() string {
150+
return newRef(r.Digest, r.Kind)
151+
}
152+
145153
// ExtractReferrers extracts the referrers from the given attestation
146154
// this means
147155
// 1 - write an entry for the attestation itself
148156
// 2 - then to all the materials contained in the predicate
149157
// 3 - and the subjects (some of them)
150158
// 4 - creating link between the attestation and the materials/subjects as needed
151159
// see tests for examples
152-
func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) {
160+
func extractReferrers(att *dsse.Envelope) ([]*Referrer, error) {
153161
// Calculate the attestation hash
154162
jsonAtt, err := json.Marshal(att)
155163
if err != nil {
@@ -162,16 +170,18 @@ func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) {
162170
return nil, fmt.Errorf("calculating attestation hash: %w", err)
163171
}
164172

165-
referrers := make(ReferrerMap)
173+
referrersMap := make(map[string]*Referrer)
166174
// 1 - Attestation referrer
167175
// Add the attestation itself as a referrer to the map without references yet
168176
attestationHash := h.String()
169-
referrers[attestationHash] = &Referrer{
177+
attestationReferrer := &Referrer{
170178
Digest: attestationHash,
171179
Kind: referrerAttestationType,
172180
Downloadable: true,
173181
}
174182

183+
referrersMap[newRef(attestationHash, referrerAttestationType)] = attestationReferrer
184+
175185
// 2 - Predicate that's referenced from the attestation
176186
predicate, err := chainloop.ExtractPredicate(att)
177187
if err != nil {
@@ -189,23 +199,23 @@ func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) {
189199
// Create its referrer entry if it doesn't exist yet
190200
// the reason it might exist is because you might be attaching the same material twice
191201
// i.e the same SBOM twice, in that case we don't want to create a new referrer
192-
// If we are providing different types for the same digest, we should error out
193-
if r, ok := referrers[material.Hash.String()]; ok {
194-
if r.Kind != material.Type {
195-
return nil, fmt.Errorf("material %s has different types: %s and %s", material.Hash.String(), r.Kind, material.Type)
196-
}
197-
202+
materialRef := newRef(material.Hash.String(), material.Type)
203+
if _, ok := referrersMap[materialRef]; ok {
198204
continue
199205
}
200206

201-
referrers[material.Hash.String()] = &Referrer{
207+
referrersMap[materialRef] = &Referrer{
202208
Digest: material.Hash.String(),
203209
Kind: material.Type,
204210
Downloadable: material.UploadedToCAS,
205211
}
206212

213+
materialReferrer := referrersMap[materialRef]
214+
207215
// Add the reference to the attestation
208-
referrers[attestationHash].References = append(referrers[attestationHash].References, material.Hash.String())
216+
attestationReferrer.References = append(attestationReferrer.References, &Referrer{
217+
Digest: materialReferrer.Digest, Kind: materialReferrer.Kind,
218+
})
209219
}
210220

211221
// 3 - Subject that points to the attestation
@@ -215,25 +225,42 @@ func extractReferrers(att *dsse.Envelope) (ReferrerMap, error) {
215225
}
216226

217227
for _, subject := range statement.Subject {
218-
subjectRef, err := intotoSubjectToReferrer(subject)
228+
subjectReferrer, err := intotoSubjectToReferrer(subject)
219229
if err != nil {
220230
return nil, fmt.Errorf("transforming subject to referrer: %w", err)
221231
}
222232

223-
if subjectRef == nil {
233+
if subjectReferrer == nil {
224234
continue
225235
}
226236

237+
subjectRef := newRef(subjectReferrer.Digest, subjectReferrer.Kind)
238+
227239
// check if we already have a referrer for this digest and set it otherwise
228240
// this is the case for example for git.Head ones
229-
if _, ok := referrers[subjectRef.Digest]; !ok {
230-
referrers[subjectRef.Digest] = subjectRef
241+
if _, ok := referrersMap[subjectRef]; !ok {
242+
referrersMap[subjectRef] = subjectReferrer
231243
// add it to the list of of attestation-referenced digests
232-
referrers[attestationHash].References = append(referrers[attestationHash].References, subjectRef.Digest)
244+
attestationReferrer.References = append(attestationReferrer.References,
245+
&Referrer{
246+
Digest: subjectReferrer.Digest, Kind: subjectReferrer.Kind,
247+
})
233248
}
234249

235250
// Update referrer to point to the attestation
236-
referrers[subjectRef.Digest].References = []string{attestationHash}
251+
referrersMap[subjectRef].References = []*Referrer{{Digest: attestationReferrer.Digest, Kind: attestationReferrer.Kind}}
252+
}
253+
254+
// Return a sorted list of referrers
255+
mapKeys := make([]string, 0, len(referrersMap))
256+
for k := range referrersMap {
257+
mapKeys = append(mapKeys, k)
258+
}
259+
sort.Strings(mapKeys)
260+
261+
referrers := make([]*Referrer, 0, len(referrersMap))
262+
for _, k := range mapKeys {
263+
referrers = append(referrers, referrersMap[k])
237264
}
238265

239266
return referrers, nil

app/controlplane/internal/biz/referrer_integration_test.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,42 +36,42 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
3636
var envelope *dsse.Envelope
3737
require.NoError(s.T(), json.Unmarshal(attJSON, &envelope))
3838

39-
wantReferrerAtt := &biz.StoredReferrer{
39+
wantReferrerAtt := &biz.Referrer{
4040
Digest: "sha256:ad704d286bcad6e155e71c33d48247931231338396acbcd9769087530085b2a2",
4141
Kind: "ATTESTATION",
4242
Downloadable: true,
4343
}
4444

45-
wantReferrerCommit := &biz.StoredReferrer{
45+
wantReferrerCommit := &biz.Referrer{
4646
Digest: "sha1:78ac366c9e8a300d51808d581422ca61f7b5b721",
4747
Kind: "GIT_HEAD_COMMIT",
4848
}
4949

50-
wantReferrerSBOM := &biz.StoredReferrer{
50+
wantReferrerSBOM := &biz.Referrer{
5151
Digest: "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c",
5252
Kind: "SBOM_CYCLONEDX_JSON",
5353
Downloadable: true,
5454
}
5555

56-
wantReferrerArtifact := &biz.StoredReferrer{
56+
wantReferrerArtifact := &biz.Referrer{
5757
Digest: "sha256:385c4188b9c080499413f2e0fa0b3951ed107b5f0cb35c2f2b1f07a7be9a7512",
5858
Kind: "ARTIFACT",
5959
Downloadable: true,
6060
}
6161

62-
wantReferrerOpenVEX := &biz.StoredReferrer{
62+
wantReferrerOpenVEX := &biz.Referrer{
6363
Digest: "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2",
6464
Kind: "OPENVEX",
6565
Downloadable: true,
6666
}
6767

68-
wantReferrerSarif := &biz.StoredReferrer{
68+
wantReferrerSarif := &biz.Referrer{
6969
Digest: "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95",
7070
Kind: "SARIF",
7171
Downloadable: true,
7272
}
7373

74-
wantReferrerContainerImage := &biz.StoredReferrer{
74+
wantReferrerContainerImage := &biz.Referrer{
7575
Digest: "sha256:fbd9335f55d83d8aaf9ab1a539b0f2a87b444e8c54f34c9a1ca9d7df15605db4",
7676
Kind: "CONTAINER_IMAGE",
7777
}
@@ -109,12 +109,10 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
109109
// it has all the references
110110
require.Len(t, got.References, 6)
111111

112-
for i, want := range []*biz.StoredReferrer{
112+
for i, want := range []*biz.Referrer{
113113
wantReferrerCommit, wantReferrerSBOM, wantReferrerArtifact, wantReferrerOpenVEX, wantReferrerSarif, wantReferrerContainerImage} {
114114
gotR := got.References[i]
115-
s.Equal(want.Digest, gotR.Digest)
116-
s.Equal(want.Kind, gotR.Kind)
117-
s.Equal(want.Downloadable, gotR.Downloadable)
115+
s.Equal(want, gotR.Referrer)
118116
}
119117
s.Equal([]uuid.UUID{s.org1UUID}, got.OrgIDs)
120118
})
@@ -181,13 +179,13 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
181179
s.Nil(got)
182180
})
183181

184-
s.T().Run("it should fail if the attestation has the same material twice with different types", func(t *testing.T) {
182+
s.T().Run("it should NOT fail storing the attestation with the same material twice with different types", func(t *testing.T) {
185183
attJSON, err = os.ReadFile("testdata/attestations/with-duplicated-sha.json")
186184
require.NoError(s.T(), err)
187185
require.NoError(s.T(), json.Unmarshal(attJSON, &envelope))
188186

189187
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.org1.ID)
190-
s.ErrorContains(err, "has different types")
188+
s.NoError(err)
191189
})
192190

193191
s.T().Run("it should fail on retrieval if we have stored two referrers with same digest (for two different types)", func(t *testing.T) {
@@ -203,7 +201,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
203201
// but retrieval should fail. In the future we will ask the user to provide the artifact type in these cases of ambiguity
204202
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerSarif.Digest, s.user.ID)
205203
s.Nil(got)
206-
s.ErrorContains(err, "found more than one referrer with digest")
204+
s.ErrorContains(err, "present in 2 kinds")
207205
})
208206

209207
s.T().Run("now there should a container image pointing to two attestations", func(t *testing.T) {

0 commit comments

Comments
 (0)