Skip to content

Commit e1f3ea0

Browse files
authored
feat: upload size server-size validation (#1001)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent d5141ae commit e1f3ea0

File tree

9 files changed

+44
-14
lines changed

9 files changed

+44
-14
lines changed

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

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

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

102102
// add bearer token to context

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 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.
@@ -92,7 +92,7 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro
9292

9393
s.log.Infow("msg", "artifact does not exist, uploading", "digest", req.resource.Digest, "name", req.resource.FileName)
9494
// Create a buffer that will be filled in the background before sending its content to the backend
95-
buffer := newStreamReader()
95+
buffer := newStreamReader(info.MaxBytes)
9696
// Add data from first request
9797
if err = buffer.Write(req.GetData()); err != nil {
9898
return sl.LogAndMaskErr(err, s.log)
@@ -213,7 +213,7 @@ func bufferStream(ctx context.Context, stream bytestream.ByteStream_WriteServer,
213213
return
214214
}
215215

216-
log.Debugw("msg", "upload chunk received", "digest", req.resource.Digest, "bufferSize", buffer.size, "chunkSize", len(req.GetData()))
216+
log.Debugw("msg", "upload chunk received", "digest", req.resource.Digest, "currentSize", buffer.size, "maxSize", buffer.maxSize, "chunkSize", len(req.GetData()))
217217
}
218218
}
219219
}
@@ -222,22 +222,32 @@ type streamReader struct {
222222
*bytes.Buffer
223223
// total size of the in-memory buffer in bytes
224224
size int64
225+
// Max size allowed to be uploaded
226+
maxSize int64
225227
// there was an error during stream data filling
226228
errorChan chan error
227229
}
228230

229231
// Wrapper around a buffer that adds
230232
// the ability to record the total size of the data that went through it
231233
// and a channel to be used by the clients to signal when the buffer has been filled
232-
func newStreamReader() *streamReader {
234+
func newStreamReader(maxSize int64) *streamReader {
233235
return &streamReader{
234236
Buffer: bytes.NewBuffer(nil),
235237
errorChan: make(chan error),
238+
maxSize: maxSize,
236239
}
237240
}
238241

239242
func (r *streamReader) Write(data []byte) error {
240243
r.size += int64(len(data))
244+
245+
// Check if the size of the buffer has exceeded the maximum allowed size
246+
// if maxSize is 0, then there is no limit
247+
if r.maxSize != 0 && r.size > r.maxSize {
248+
return fmt.Errorf("max size of upload exceeded: want=%d, max=%d", r.size, r.maxSize)
249+
}
250+
241251
_, err := r.Buffer.Write(data)
242252
return err
243253
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 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.
@@ -46,7 +46,7 @@ import (
4646
)
4747

4848
func (s *bytestreamSuite) TestStreamReader() {
49-
buffer := newStreamReader()
49+
buffer := newStreamReader(0)
5050
// Write twice and check the length
5151
err := buffer.Write([]byte("hello"))
5252
s.NoError(err)
@@ -68,6 +68,18 @@ func (s *bytestreamSuite) TestStreamReader() {
6868
s.Equal(0, buffer.Len())
6969
}
7070

71+
func (s *bytestreamSuite) TestStreamReaderOverflow() {
72+
// a buffer with 8 bytes limit
73+
buffer := newStreamReader(8)
74+
// Write twice and check the length
75+
err := buffer.Write([]byte("hello"))
76+
s.NoError(err)
77+
s.Equal(int64(5), buffer.size)
78+
err = buffer.Write([]byte("chainloop"))
79+
s.Error(err)
80+
s.ErrorContains(err, "max size of upload exceeded")
81+
}
82+
7183
func (s *bytestreamSuite) TestWrite() {
7284
ctx := s.upCtx
7385

app/controlplane/internal/service/attestation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte
336336
// Return the backend information and associated credentials (if applicable)
337337
resp := &cpAPI.AttestationServiceGetUploadCredsResponse_Result{Backend: bizCASBackendToPb(backend)}
338338
if backend.SecretName != "" {
339-
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader}
339+
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader, MaxBytes: backend.Limits.MaxBytes}
340340
t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref)
341341
if err != nil {
342342
return nil, handleUseCaseErr(err, s.log)

app/controlplane/internal/service/cascredential.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS
121121
return nil, errors.BadRequest("invalid argument", "cannot upload or download artifacts from an inline CAS backend")
122122
}
123123

124-
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: role}
124+
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: role, MaxBytes: backend.Limits.MaxBytes}
125125
t, err := s.casUC.GenerateTemporaryCredentials(ref)
126126
if err != nil {
127127
return nil, handleUseCaseErr(err, s.log)

app/controlplane/internal/service/casredirect.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown
115115

116116
// 2- add authentication token to the query params ?t=[token]
117117
if backend.SecretName != "" {
118-
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Downloader}
118+
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Downloader, MaxBytes: backend.Limits.MaxBytes}
119119
t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref)
120120
if err != nil {
121121
return nil, handleUseCaseErr(err, s.log)

app/controlplane/pkg/biz/cascredentials.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 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.
@@ -47,8 +47,9 @@ type CASCredsOpts struct {
4747
BackendType string // i.e OCI, S3
4848
SecretPath string // path to for example the OCI secret in the vault
4949
Role robotaccount.Role
50+
MaxBytes int64
5051
}
5152

5253
func (uc *CASCredentialsUseCase) GenerateTemporaryCredentials(backendRef *CASCredsOpts) (string, error) {
53-
return uc.jwtBuilder.GenerateJWT(backendRef.BackendType, backendRef.SecretPath, jwt.CASAudience, backendRef.Role)
54+
return uc.jwtBuilder.GenerateJWT(backendRef.BackendType, backendRef.SecretPath, jwt.CASAudience, backendRef.Role, backendRef.MaxBytes)
5455
}

internal/robotaccount/cas/robotaccount.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Claims struct {
3737
Role Role `json:"role"` // either downloader or uploader
3838
StoredSecretID string `json:"secret-id"` // path to the OCI secret in the vault
3939
BackendType string `json:"backend"` // backend to use, i.e OCI
40+
MaxBytes int64 `json:"maxbytes"` // max bytes to upload
4041
}
4142

4243
type Role string
@@ -102,7 +103,7 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) {
102103
return b, nil
103104
}
104105

105-
func (ra *Builder) GenerateJWT(backendType, secretID, audience string, role Role) (string, error) {
106+
func (ra *Builder) GenerateJWT(backendType, secretID, audience string, role Role, maxBytes int64) (string, error) {
106107
if backendType == "" {
107108
return "", fmt.Errorf("backend type is required")
108109
}
@@ -131,6 +132,11 @@ func (ra *Builder) GenerateJWT(backendType, secretID, audience string, role Role
131132
},
132133
}
133134

135+
// If there is limit on the size of the upload we store it as claim
136+
if maxBytes != 0 {
137+
claims.MaxBytes = maxBytes
138+
}
139+
134140
if ra.expiration != nil {
135141
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(*ra.expiration))
136142
}

internal/robotaccount/cas/robotaccount_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func TestGenerateJWT(t *testing.T) {
150150
)
151151

152152
require.NoError(t, err)
153-
token, err := b.GenerateJWT("OCI", "secret-id", JWTAudience, Uploader)
153+
token, err := b.GenerateJWT("OCI", "secret-id", JWTAudience, Uploader, 123)
154154
assert.NoError(t, err)
155155
assert.NotEmpty(t, token)
156156

@@ -166,6 +166,7 @@ func TestGenerateJWT(t *testing.T) {
166166
assert.Equal(t, Uploader, claims.Role)
167167
assert.Equal(t, "my-issuer", claims.Issuer)
168168
assert.Contains(t, claims.Audience, "artifact-cas.chainloop")
169+
assert.Equal(t, claims.MaxBytes, int64(123))
169170
assert.WithinDuration(t, time.Now(), claims.ExpiresAt.Time, 10*time.Second)
170171
}
171172

0 commit comments

Comments
 (0)