Skip to content

Commit c943d39

Browse files
authored
feat: decouple cas-mappings table from attestations (#1921)
Signed-off-by: Miguel Martinez <[email protected]>
1 parent 7581865 commit c943d39

16 files changed

+167
-63
lines changed

app/controlplane/internal/service/cascredential.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS
103103
}
104104

105105
// If we can't find a mapping, we'll use the default backend
106-
if err != nil && !biz.IsNotFound(err) && !biz.IsErrUnauthorized(err) {
106+
if err != nil && !biz.IsNotFound(err) {
107107
if biz.IsErrValidation(err) {
108108
return nil, errors.BadRequest("invalid", err.Error())
109109
}

app/controlplane/internal/service/casredirect.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown
8888
if err != nil {
8989
// We don't want to leak the fact that the asset exists but the user does not have permissions
9090
// that's why we return a generic 404 in unauthorized scenarios too
91-
if biz.IsNotFound(err) || biz.IsErrUnauthorized(err) {
91+
if biz.IsNotFound(err) {
9292
return nil, kerrors.NotFound("not found", "artifact not found")
9393
} else if biz.IsErrValidation(err) {
9494
return nil, kerrors.BadRequest("invalid", err.Error())

app/controlplane/pkg/biz/biz.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 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.
@@ -113,3 +113,7 @@ type EntityRef struct {
113113
// Name is the name of the entity
114114
Name string
115115
}
116+
117+
func ToPtr[T any](v T) *T {
118+
return &v
119+
}

app/controlplane/pkg/biz/casmapping.go

+31-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2025 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.
@@ -17,7 +17,6 @@ package biz
1717

1818
import (
1919
"context"
20-
"errors"
2120
"fmt"
2221
"time"
2322

@@ -39,7 +38,8 @@ type CASMapping struct {
3938
}
4039

4140
type CASMappingRepo interface {
42-
Create(ctx context.Context, digest string, casBackendID, workflowRunID uuid.UUID) (*CASMapping, error)
41+
// Create a mapping with an optional workflow run id
42+
Create(ctx context.Context, digest string, casBackendID uuid.UUID, workflowRunID *uuid.UUID) (*CASMapping, error)
4343
// List all the CAS mappings for the given digest
4444
FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error)
4545
}
@@ -54,15 +54,20 @@ func NewCASMappingUseCase(repo CASMappingRepo, mRepo MembershipRepo, logger log.
5454
return &CASMappingUseCase{repo, mRepo, servicelogger.ScopedHelper(logger, "cas-mapping-usecase")}
5555
}
5656

57-
func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID, workflowRunID string) (*CASMapping, error) {
57+
// Create a mapping with an optional workflow run id
58+
func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID string, workflowRunID string) (*CASMapping, error) {
5859
casBackendUUID, err := uuid.Parse(casBackendID)
5960
if err != nil {
6061
return nil, NewErrInvalidUUID(err)
6162
}
6263

63-
workflowRunUUID, err := uuid.Parse(workflowRunID)
64-
if err != nil {
65-
return nil, NewErrInvalidUUID(err)
64+
var workflowRunUUID *uuid.UUID
65+
if workflowRunID != "" {
66+
runUUID, err := uuid.Parse(workflowRunID)
67+
if err != nil {
68+
return nil, NewErrInvalidUUID(err)
69+
}
70+
workflowRunUUID = &runUUID
6671
}
6772

6873
// parse the digest to make sure is a valid sha256 sum
@@ -101,14 +106,28 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context
101106
userOrgs = append(userOrgs, m.OrganizationID.String())
102107
}
103108

104-
return uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs)
109+
mapping, err := uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs)
110+
if err != nil {
111+
return nil, fmt.Errorf("failed to find cas mapping for download: %w", err)
112+
}
113+
114+
return mapping, nil
105115
}
106116

107-
func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, digest string, orgs []string) (*CASMapping, error) {
117+
func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, digest string, orgs []string) (result *CASMapping, err error) {
108118
if _, err := cr_v1.NewHash(digest); err != nil {
109119
return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err))
110120
}
111121

122+
// log the result
123+
defer func() {
124+
if result != nil {
125+
uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
126+
} else if err == nil || IsNotFound(err) {
127+
uc.logger.Infow("msg", "no mapping found!", "digest", digest, "orgs", orgs)
128+
}
129+
}()
130+
112131
if len(orgs) == 0 {
113132
return nil, NewErrValidationStr("no organizations provided")
114133
}
@@ -129,24 +148,19 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context,
129148
if err != nil {
130149
return nil, fmt.Errorf("failed to load mappings associated to an user: %w", err)
131150
} else if len(orgMappings) > 0 {
132-
result := defaultOrFirst(orgMappings)
133-
134-
uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
135-
return result, nil
151+
return defaultOrFirst(orgMappings), nil
136152
}
137153

138154
// 3 - mappings that are public
139155
publicMappings := filterByPublic(mappings)
140156
// The user has not access to neither proprietary nor public mappings
141157
if len(publicMappings) == 0 {
142158
uc.logger.Warnw("msg", "digest exist but user does not have access to it", "digest", digest, "orgs", orgs)
143-
return nil, NewErrUnauthorized(errors.New("unauthorized access to the artifact"))
159+
return nil, NewErrNotFound("digest not found in any mapping")
144160
}
145161

146162
// Pick the appropriate mapping from multiple ones
147-
result := defaultOrFirst(publicMappings)
148-
uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
149-
return result, nil
163+
return defaultOrFirst(publicMappings), nil
150164
}
151165

152166
// Extract only the mappings associated with a list of orgs

app/controlplane/pkg/biz/casmapping_integration_test.go

+48-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2025 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.
@@ -33,11 +33,12 @@ import (
3333

3434
const (
3535
// This is the digest of the empty envelope
36-
validDigest = "sha256:f845058d865c3d4d491c9019f6afe9c543ad2cd11b31620cc512e341fb03d3d8"
37-
validDigest2 = "sha256:2b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
38-
validDigest3 = "sha256:1b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
39-
validDigestPublic = "sha256:8b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
40-
invalidDigest = "sha256:deadbeef"
36+
validDigest = "sha256:f845058d865c3d4d491c9019f6afe9c543ad2cd11b31620cc512e341fb03d3d8"
37+
validDigest2 = "sha256:2b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
38+
validDigest3 = "sha256:1b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
39+
validDigestPublic = "sha256:8b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
40+
validDigestWithoutRun = "sha256:63e8ec8e489d31265fb920241da3300ec36c10865d2e287e055d4e1287ce25e6"
41+
invalidDigest = "sha256:deadbeef"
4142
)
4243

4344
func (s *casMappingIntegrationSuite) TestCASMappingForDownloadUser() {
@@ -121,6 +122,8 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadByOrg() {
121122
require.NoError(s.T(), err)
122123
_, err = s.CASMapping.Create(ctx, validDigestPublic, s.casBackend3.ID.String(), s.publicWorkflowRun.ID.String())
123124
require.NoError(s.T(), err)
125+
_, err = s.CASMapping.Create(ctx, validDigestWithoutRun, s.casBackend3.ID.String(), "")
126+
require.NoError(s.T(), err)
124127

125128
// both validDigest and validDigest2 from two different orgs
126129
s.Run("validDigest is in org1", func() {
@@ -137,6 +140,17 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadByOrg() {
137140
s.Equal(s.casBackend3.ID, mapping.CASBackend.ID)
138141
})
139142

143+
s.Run("validDigestWithoutRun is available only to org 3", func() {
144+
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestWithoutRun, []string{s.casBackend3.OrganizationID.String()})
145+
s.NoError(err)
146+
s.NotNil(mapping)
147+
s.Equal(s.casBackend3.ID, mapping.CASBackend.ID)
148+
149+
mapping, err = s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestWithoutRun, []string{s.org1.ID})
150+
s.Error(err)
151+
s.Nil(mapping)
152+
})
153+
140154
s.Run("can't find an invalid digest", func() {
141155
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, invalidDigest, []string{s.org1.ID})
142156
s.Error(err)
@@ -234,70 +248,84 @@ func (s *casMappingIntegrationSuite) TestCreate() {
234248
name string
235249
digest string
236250
casBackendID uuid.UUID
237-
workflowRunID uuid.UUID
251+
workflowRunID *uuid.UUID
238252
wantErr bool
239253
wantPublic bool
240254
}{
241255
{
242256
name: "valid",
243257
digest: validDigest,
244258
casBackendID: s.casBackend1.ID,
245-
workflowRunID: s.workflowRun.ID,
259+
workflowRunID: biz.ToPtr(s.workflowRun.ID),
246260
},
247261
{
248262
name: "created again with same digest",
249263
digest: validDigest,
250264
casBackendID: s.casBackend1.ID,
251-
workflowRunID: s.workflowRun.ID,
265+
workflowRunID: biz.ToPtr(s.workflowRun.ID),
252266
},
253267
{
254268
name: "invalid digest format",
255269
digest: invalidDigest,
256270
casBackendID: s.casBackend1.ID,
257-
workflowRunID: s.workflowRun.ID,
271+
workflowRunID: biz.ToPtr(s.workflowRun.ID),
258272
wantErr: true,
259273
},
260274
{
261275
name: "invalid digest missing prefix",
262276
digest: "3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d",
263277
casBackendID: s.casBackend1.ID,
264-
workflowRunID: s.workflowRun.ID,
278+
workflowRunID: biz.ToPtr(s.workflowRun.ID),
265279
wantErr: true,
266280
},
267281
{
268282
name: "non-existing CASBackend",
269283
digest: validDigest,
270284
casBackendID: uuid.New(),
271-
workflowRunID: s.workflowRun.ID,
285+
workflowRunID: biz.ToPtr(s.workflowRun.ID),
272286
wantErr: true,
273287
},
274288
{
275289
name: "non-existing WorkflowRunID",
276290
digest: validDigest,
277291
casBackendID: s.casBackend1.ID,
278-
workflowRunID: uuid.New(),
292+
workflowRunID: biz.ToPtr(uuid.New()),
279293
wantErr: true,
280294
},
281295
{
282296
name: "public workflowrun",
283297
digest: validDigest,
284298
casBackendID: s.casBackend1.ID,
285-
workflowRunID: s.publicWorkflowRun.ID,
299+
workflowRunID: biz.ToPtr(s.publicWorkflowRun.ID),
286300
wantPublic: true,
287301
},
302+
{
303+
name: "not associated to any workflowrun",
304+
digest: validDigest,
305+
casBackendID: s.casBackend1.ID,
306+
wantPublic: false,
307+
},
288308
}
289309

290310
for _, tc := range testCases {
291311
want := &biz.CASMapping{
292-
Digest: validDigest,
293-
CASBackend: &biz.CASBackend{ID: s.casBackend1.ID},
294-
WorkflowRunID: tc.workflowRunID,
295-
OrgID: s.casBackend1.OrganizationID,
296-
Public: tc.wantPublic,
312+
Digest: validDigest,
313+
CASBackend: &biz.CASBackend{ID: s.casBackend1.ID},
314+
OrgID: s.casBackend1.OrganizationID,
315+
Public: tc.wantPublic,
316+
}
317+
318+
if tc.workflowRunID != nil {
319+
want.WorkflowRunID = *tc.workflowRunID
297320
}
298321

299322
s.Run(tc.name, func() {
300-
got, err := s.CASMapping.Create(context.TODO(), tc.digest, tc.casBackendID.String(), tc.workflowRunID.String())
323+
var workflowRunID string
324+
if tc.workflowRunID != nil {
325+
workflowRunID = tc.workflowRunID.String()
326+
}
327+
328+
got, err := s.CASMapping.Create(context.TODO(), tc.digest, tc.casBackendID.String(), workflowRunID)
301329
if tc.wantErr {
302330
s.Error(err)
303331
} else {

app/controlplane/pkg/biz/casmapping_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2025 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.
@@ -90,7 +90,7 @@ func (s *casMappingSuite) TestCreate() {
9090
}
9191

9292
// Mock successful repo call
93-
s.repo.On("Create", mock.Anything, validDigest, validUUID, validUUID).Return(want, nil).Maybe()
93+
s.repo.On("Create", mock.Anything, validDigest, validUUID, biz.ToPtr(validUUID)).Return(want, nil).Maybe()
9494

9595
for _, tc := range testCases {
9696
s.Run(tc.name, func() {

app/controlplane/pkg/biz/mocks/CASMappingRepo.go

+17-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)