Skip to content

Commit 21c5b93

Browse files
authored
feat(blobmanager): add managed CAS backend via S3 Access Points (#3121)
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent 87a28a9 commit 21c5b93

32 files changed

Lines changed: 1350 additions & 163 deletions

.gitattributes

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ app/controlplane/pkg/data/ent/migrate/** linguist-generated=false
44
app/controlplane/pkg/data/ent/migrate/** linguist-detectable=true
55
app/controlplane/pkg/data/ent/schema/* linguist-generated=false
66
app/controlplane/pkg/data/ent/schema/* linguist-detectable=true
7-
app/controlplane/api/gen/jsonschema/** linguist-generated=true
7+
app/controlplane/api/gen/jsonschema/** linguist-generated=true
8+
*.pb.go linguist-generated=true
9+
*.pb.validate.go linguist-generated=true
10+
wire_gen.go linguist-generated=true

app/artifact-cas/internal/server/grpc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func TestJWTAuthFunc(t *testing.T) {
9797

9898
b, err := robotaccount.NewBuilder(opts...)
9999
require.NoError(t, err)
100-
token, err := b.GenerateJWT("backend-type", "secret-id", tc.audience, robotaccount.Downloader, 0)
100+
token, err := b.GenerateJWT("backend-type", "secret-id", tc.audience, robotaccount.Downloader, 0, "org-id")
101101
require.NoError(t, err)
102102

103103
// add bearer token to context

app/artifact-cas/internal/service/bytestream.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro
6464
defer span.End()
6565

6666
// Get auth info and check that it's an uploader token
67-
info, err := infoFromAuth(ctx)
67+
info, err := casJWT.InfoFromAuth(ctx)
6868
if err != nil {
6969
return err
7070
}
@@ -145,7 +145,7 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream.
145145
ctx := stream.Context()
146146
ctx, span := otelx.Start(ctx, byteStreamTracer, "ByteStreamService.Read")
147147
defer span.End()
148-
info, err := infoFromAuth(ctx)
148+
info, err := casJWT.InfoFromAuth(ctx)
149149
if err != nil {
150150
return err
151151
}

app/artifact-cas/internal/service/download.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5252
ctx := r.Context()
5353
ctx, span := otelx.Start(ctx, downloadTracer, "DownloadService.ServeHTTP")
5454
defer span.End()
55-
auth, err := infoFromAuth(ctx)
55+
auth, err := casJWT.InfoFromAuth(ctx)
5656
if err != nil {
5757
http.Error(w, err.Error(), http.StatusUnauthorized)
5858
return

app/artifact-cas/internal/service/resource.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020

2121
v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1"
22+
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
2223
backend "github.com/chainloop-dev/chainloop/pkg/blobmanager"
2324
"github.com/chainloop-dev/chainloop/pkg/otelx"
2425
sl "github.com/chainloop-dev/chainloop/pkg/servicelogger"
@@ -43,7 +44,7 @@ func (s *ResourceService) Describe(ctx context.Context, req *v1.ResourceServiceD
4344
ctx, span := otelx.Start(ctx, resourceTracer, "ResourceService.Describe")
4445
defer span.End()
4546

46-
info, err := infoFromAuth(ctx)
47+
info, err := casJWT.InfoFromAuth(ctx)
4748
if err != nil {
4849
return nil, err
4950
}

app/artifact-cas/internal/service/service.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@ import (
2121
"fmt"
2222
"syscall"
2323

24-
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
2524
backend "github.com/chainloop-dev/chainloop/pkg/blobmanager"
2625
"github.com/chainloop-dev/chainloop/pkg/servicelogger"
2726
kerrors "github.com/go-kratos/kratos/v2/errors"
2827
"github.com/go-kratos/kratos/v2/log"
29-
"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
3028
"github.com/google/wire"
3129
"google.golang.org/grpc/codes"
3230
"google.golang.org/grpc/status"
@@ -104,30 +102,3 @@ func isClientDisconnect(err error) bool {
104102

105103
return false
106104
}
107-
108-
// Extract the JWT claims from the context, note that the JWT verification has happened in the middleware
109-
func infoFromAuth(ctx context.Context) (*casJWT.Claims, error) {
110-
rawClaims, ok := jwt.FromContext(ctx)
111-
if !ok {
112-
return nil, kerrors.Unauthorized("cas", "missing authentication information")
113-
}
114-
115-
claims, ok := rawClaims.(*casJWT.Claims)
116-
if !ok {
117-
return nil, kerrors.Unauthorized("cas", "invalid authentication information")
118-
}
119-
120-
if claims.StoredSecretID == "" {
121-
return nil, kerrors.Unauthorized("cas", "missing secret reference")
122-
}
123-
124-
if claims.BackendType == "" {
125-
return nil, kerrors.Unauthorized("cas", "missing backend type")
126-
}
127-
128-
if claims.Role != casJWT.Uploader && claims.Role != casJWT.Downloader {
129-
return nil, kerrors.Unauthorized("cas", "invalid role")
130-
}
131-
132-
return claims, nil
133-
}

app/artifact-cas/internal/service/service_test.go

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -24,90 +24,15 @@ import (
2424
"syscall"
2525
"testing"
2626

27-
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
2827
backend "github.com/chainloop-dev/chainloop/pkg/blobmanager"
2928
"github.com/chainloop-dev/chainloop/pkg/blobmanager/mocks"
3029
kerrors "github.com/go-kratos/kratos/v2/errors"
31-
jwtm "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
32-
"github.com/golang-jwt/jwt/v4"
3330
"github.com/stretchr/testify/assert"
3431
"github.com/stretchr/testify/mock"
3532
"google.golang.org/grpc/codes"
3633
"google.golang.org/grpc/status"
3734
)
3835

39-
func TestInfoFromAuth(t *testing.T) {
40-
testCases := []struct {
41-
name string
42-
// input
43-
claims jwt.Claims
44-
wantErr bool
45-
}{
46-
{
47-
name: "valid claims downloader",
48-
claims: &casJWT.Claims{
49-
Role: casJWT.Downloader,
50-
StoredSecretID: "test",
51-
BackendType: "backend-type",
52-
},
53-
},
54-
{
55-
name: "valid claims uploader",
56-
claims: &casJWT.Claims{
57-
Role: casJWT.Uploader,
58-
StoredSecretID: "test",
59-
BackendType: "backend-type",
60-
},
61-
},
62-
{
63-
name: "invalid role",
64-
claims: &casJWT.Claims{
65-
Role: "invalid",
66-
StoredSecretID: "test",
67-
BackendType: "backend-type",
68-
},
69-
wantErr: true,
70-
},
71-
{
72-
name: "missing secretID",
73-
claims: &casJWT.Claims{
74-
Role: "test",
75-
BackendType: "backend-type",
76-
},
77-
wantErr: true,
78-
},
79-
{
80-
name: "missing role",
81-
claims: &casJWT.Claims{
82-
StoredSecretID: "test",
83-
BackendType: "backend-type",
84-
},
85-
wantErr: true,
86-
},
87-
{
88-
name: "missing backend type",
89-
claims: &casJWT.Claims{
90-
StoredSecretID: "test",
91-
Role: "test",
92-
},
93-
wantErr: true,
94-
},
95-
}
96-
97-
for _, tc := range testCases {
98-
t.Run(tc.name, func(t *testing.T) {
99-
info, err := infoFromAuth(jwtm.NewContext(context.Background(), tc.claims))
100-
if tc.wantErr {
101-
assert.Error(t, err)
102-
return
103-
}
104-
105-
assert.NoError(t, err)
106-
assert.Equal(t, tc.claims, info)
107-
})
108-
}
109-
}
110-
11136
func TestLoadBackend(t *testing.T) {
11237
testCases := []struct {
11338
name string

app/controlplane/internal/dispatcher/dispatcher.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (d *FanOutDispatcher) Run(ctx context.Context, opts *RunOpts) error {
9292
}
9393

9494
// 2. Hydrate the dispatch queue with the actual inputs
95-
if err := d.loadInputs(ctx, queue, opts.Envelope, opts.DownloadBackendType, opts.DownloadSecretName); err != nil {
95+
if err := d.loadInputs(ctx, queue, opts.Envelope, opts.DownloadBackendType, opts.DownloadSecretName, opts.OrgID); err != nil {
9696
return fmt.Errorf("loading materials: %w", err)
9797
}
9898

@@ -198,7 +198,7 @@ func (d *FanOutDispatcher) initDispatchQueue(ctx context.Context, orgID, workflo
198198
}
199199

200200
// Load the inputs for the dispatchItem, both materials and attestation
201-
func (d *FanOutDispatcher) loadInputs(ctx context.Context, queue dispatchQueue, att *dsse.Envelope, backendType, secretName string) error {
201+
func (d *FanOutDispatcher) loadInputs(ctx context.Context, queue dispatchQueue, att *dsse.Envelope, backendType, secretName, orgID string) error {
202202
if att == nil {
203203
return fmt.Errorf("attestation is nil")
204204
}
@@ -252,8 +252,12 @@ func (d *FanOutDispatcher) loadInputs(ctx context.Context, queue dispatchQueue,
252252
if item.plugin.IsSubscribedTo(material.Type) {
253253
// It's a downloadable and has not been downloaded yet
254254
if !downloaded && material.Hash != nil && material.UploadedToCAS {
255+
orgUUID, err := uuid.Parse(orgID)
256+
if err != nil {
257+
return fmt.Errorf("parsing org id: %w", err)
258+
}
255259
buf := bytes.NewBuffer(nil)
256-
if err := d.casClient.Download(ctx, backendType, secretName, buf, material.Hash.String()); err != nil {
260+
if err := d.casClient.Download(ctx, backendType, secretName, orgUUID, buf, material.Hash.String()); err != nil {
257261
return fmt.Errorf("downloading from CAS: %w", err)
258262
}
259263

app/controlplane/internal/dispatcher/dispatcher_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
3030
mockedSDK "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1/mocks"
3131
"github.com/go-kratos/kratos/v2/log"
32+
"github.com/google/uuid"
3233
"github.com/secure-systems-lab/go-securesystemslib/dsse"
3334

3435
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers"
@@ -57,7 +58,7 @@ func (s *dispatcherTestSuite) TestLoadInputsEnvelope() {
5758
s.ociIntegrationBackend.(*mockedSDK.FanOut).On("IsSubscribedTo", "SBOM_CYCLONEDX_JSON").Return(false)
5859
s.ociIntegrationBackend.(*mockedSDK.FanOut).On("String").Return("mocked-integration")
5960

60-
err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "backend-type", "secret-name")
61+
err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "backend-type", "secret-name", uuid.NewString())
6162
assert.NoError(s.T(), err)
6263

6364
// Only one integration is registered
@@ -101,14 +102,14 @@ func (s *dispatcherTestSuite) TestLoadInputsMaterials() {
101102
require.NoError(s.T(), err)
102103

103104
// Simulate SBOM download
104-
s.casClient.On("Download", mock.Anything, "backend-type", "secret-name", mock.Anything, mock.Anything).
105+
s.casClient.On("Download", mock.Anything, "backend-type", "secret-name", mock.Anything, mock.Anything, mock.Anything).
105106
Return(nil).Run(func(args mock.Arguments) {
106107
buf := bytes.NewBuffer([]byte("SBOM Content"))
107-
_, err := io.Copy(args.Get(3).(io.Writer), buf)
108+
_, err := io.Copy(args.Get(4).(io.Writer), buf)
108109
s.NoError(err)
109110
})
110111

111-
err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "backend-type", "secret-name")
112+
err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "backend-type", "secret-name", uuid.NewString())
112113
assert.NoError(s.T(), err)
113114
require.Len(s.T(), queue, 3)
114115

app/controlplane/internal/service/attestation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte
494494
// Return the backend information and associated credentials (if applicable)
495495
resp := &cpAPI.AttestationServiceGetUploadCredsResponse_Result{Backend: bizCASBackendToPb(backend)}
496496
if backend.SecretName != "" {
497-
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader, MaxBytes: backend.Limits.MaxBytes}
497+
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader, MaxBytes: backend.Limits.MaxBytes, OrgID: backend.OrganizationID}
498498
t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref)
499499
if err != nil {
500500
return nil, handleUseCaseErr(err, s.log)

0 commit comments

Comments
 (0)