aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2024-01-02 17:16:55 +0100
committerLibravatar Rutger Broekhoff2024-01-02 17:16:55 +0100
commit6e97a3edaa18ef8e5b16feba29f04e993957b7a7 (patch)
tree4766c21f58811728acd933aa5dc2045e90369ad6
parent91fcc2aeda01b7680cae826349e34eb7c8c1ec3b (diff)
downloadgitolfs3-6e97a3edaa18ef8e5b16feba29f04e993957b7a7.tar.gz
gitolfs3-6e97a3edaa18ef8e5b16feba29f04e993957b7a7.zip
Token types, download verification
-rw-r--r--cmd/git-lfs-authenticate/main.go2
-rw-r--r--cmd/git-lfs-server/main.go158
2 files changed, 140 insertions, 20 deletions
diff --git a/cmd/git-lfs-authenticate/main.go b/cmd/git-lfs-authenticate/main.go
index fc98246..d2dee21 100644
--- a/cmd/git-lfs-authenticate/main.go
+++ b/cmd/git-lfs-authenticate/main.go
@@ -89,6 +89,7 @@ func getGitoliteAccess(logger *logger, reqID, path, user, gitolitePerm string) b
89} 89}
90 90
91type gitolfs3Claims struct { 91type gitolfs3Claims struct {
92 Type string `json:"type"`
92 Repository string `json:"repository"` 93 Repository string `json:"repository"`
93 Permission string `json:"permission"` 94 Permission string `json:"permission"`
94} 95}
@@ -190,6 +191,7 @@ func main() {
190 expiresIn := time.Hour * 24 191 expiresIn := time.Hour * 24
191 claims := customClaims{ 192 claims := customClaims{
192 Gitolfs3: gitolfs3Claims{ 193 Gitolfs3: gitolfs3Claims{
194 Type: "batch-api",
193 Repository: repo, 195 Repository: repo,
194 Permission: operation, 196 Permission: operation,
195 }, 197 },
diff --git a/cmd/git-lfs-server/main.go b/cmd/git-lfs-server/main.go
index 191a696..c7feeff 100644
--- a/cmd/git-lfs-server/main.go
+++ b/cmd/git-lfs-server/main.go
@@ -163,19 +163,35 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par
163 return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError) 163 return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError)
164 } 164 }
165 if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash { 165 if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash {
166 return makeObjError(obj, "Corrupted file", http.StatusUnprocessableEntity) 166 return makeObjError(obj, "Object corrupted", http.StatusUnprocessableEntity)
167 } 167 }
168 if info.Size != obj.size { 168 if info.Size != obj.size {
169 return makeObjError(obj, "Incorrect size specified for object", http.StatusUnprocessableEntity) 169 return makeObjError(obj, "Incorrect size specified for object or object currupted", http.StatusUnprocessableEntity)
170 } 170 }
171 171
172 presigned, err := h.mc.PresignedGetObject(ctx, h.bucket, fullPath, expiresIn, url.Values{}) 172 claims := handleObjectCustomClaims{
173 Gitolfs3: handleObjectGitolfs3Claims{
174 Type: "basic-transfer",
175 Operation: operationDownload,
176 Repository: repo,
177 OID: obj.fullHash,
178 Size: obj.size,
179 },
180 RegisteredClaims: &jwt.RegisteredClaims{
181 IssuedAt: jwt.NewNumericDate(time.Now()),
182 ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
183 },
184 }
185
186 token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
187 ss, err := token.SignedString(h.privateKey)
173 if err != nil { 188 if err != nil {
174 // TODO: consider not making this an object-specific, but rather a 189 // TODO: consider not making this an object-specific, but rather a
175 // generic error such that the entire Batch API request fails. 190 // generic error such that the entire Batch API request fails.
176 reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err) 191 reqlog(ctx, "Fatal: failed to generate JWT: %s", err)
177 return makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError) 192 return makeObjError(obj, "Failed to generate token", http.StatusInternalServerError)
178 } 193 }
194 uploadPath := path.Join(repo+".git", "info/lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
179 195
180 authenticated := true 196 authenticated := true
181 return batchResponseObject{ 197 return batchResponseObject{
@@ -184,21 +200,26 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par
184 Authenticated: &authenticated, 200 Authenticated: &authenticated,
185 Actions: map[operation]batchAction{ 201 Actions: map[operation]batchAction{
186 operationDownload: { 202 operationDownload: {
187 HRef: presigned.String(), 203 Header: map[string]string{
204 "Authorization": "Bearer " + ss,
205 },
206 HRef: h.baseURL.ResolveReference(&url.URL{Path: uploadPath}).String(),
188 ExpiresIn: int64(expiresIn.Seconds()), 207 ExpiresIn: int64(expiresIn.Seconds()),
189 }, 208 },
190 }, 209 },
191 } 210 }
192} 211}
193 212
194type uploadObjectGitolfs3Claims struct { 213type handleObjectGitolfs3Claims struct {
195 Repository string `json:"repository"` 214 Type string `json:"type"`
196 OID string `json:"oid"` 215 Operation operation `json:"operation"`
197 Size int64 `json:"size"` 216 Repository string `json:"repository"`
217 OID string `json:"oid"`
218 Size int64 `json:"size"`
198} 219}
199 220
200type uploadObjectCustomClaims struct { 221type handleObjectCustomClaims struct {
201 Gitolfs3 uploadObjectGitolfs3Claims `json:"gitolfs3"` 222 Gitolfs3 handleObjectGitolfs3Claims `json:"gitolfs3"`
202 *jwt.RegisteredClaims 223 *jwt.RegisteredClaims
203} 224}
204 225
@@ -221,8 +242,10 @@ func (h *handler) handleUploadObject(ctx context.Context, repo string, obj parse
221 } 242 }
222 243
223 expiresIn := time.Hour * 24 244 expiresIn := time.Hour * 24
224 claims := uploadObjectCustomClaims{ 245 claims := handleObjectCustomClaims{
225 Gitolfs3: uploadObjectGitolfs3Claims{ 246 Gitolfs3: handleObjectGitolfs3Claims{
247 Type: "basic-transfer",
248 Operation: operationUpload,
226 Repository: repo, 249 Repository: repo,
227 OID: obj.fullHash, 250 OID: obj.fullHash,
228 Size: obj.size, 251 Size: obj.size,
@@ -327,7 +350,7 @@ func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo,
327 } 350 }
328 authz = strings.TrimPrefix(authz, "Bearer ") 351 authz = strings.TrimPrefix(authz, "Bearer ")
329 352
330 var claims uploadObjectCustomClaims 353 var claims handleObjectCustomClaims
331 _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) { 354 _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
332 if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { 355 if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
333 return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"]) 356 return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
@@ -338,6 +361,10 @@ func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo,
338 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) 361 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
339 return 362 return
340 } 363 }
364 if claims.Gitolfs3.Type != "basic-transfer" {
365 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
366 return
367 }
341 if claims.Gitolfs3.Repository != repo { 368 if claims.Gitolfs3.Repository != repo {
342 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) 369 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
343 return 370 return
@@ -346,6 +373,10 @@ func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo,
346 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) 373 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
347 return 374 return
348 } 375 }
376 if claims.Gitolfs3.Operation != operationUpload {
377 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
378 return
379 }
349 380
350 // Check with claims 381 // Check with claims
351 if lengthStr := r.Header.Get("Content-Length"); lengthStr != "" { 382 if lengthStr := r.Header.Get("Content-Length"); lengthStr != "" {
@@ -387,6 +418,85 @@ func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo,
387 } 418 }
388} 419}
389 420
421func (h *handler) handleGetObject(w http.ResponseWriter, r *http.Request, repo, oid string) {
422 ctx := r.Context()
423
424 authz := r.Header.Get("Authorization")
425 if authz == "" {
426 makeRespError(ctx, w, "Missing Authorization header", http.StatusBadRequest)
427 return
428 }
429 if !strings.HasPrefix(authz, "Bearer ") {
430 makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest)
431 return
432 }
433 authz = strings.TrimPrefix(authz, "Bearer ")
434
435 var claims handleObjectCustomClaims
436 _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
437 if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
438 return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
439 }
440 return h.privateKey.Public(), nil
441 })
442 if err != nil {
443 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
444 return
445 }
446 if claims.Gitolfs3.Type != "basic-transfer" {
447 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
448 return
449 }
450 if claims.Gitolfs3.Repository != repo {
451 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
452 return
453 }
454 if claims.Gitolfs3.OID != oid {
455 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
456 return
457 }
458 if claims.Gitolfs3.Operation != operationDownload {
459 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
460 return
461 }
462
463 sha256Raw, err := hex.DecodeString(oid)
464 if err != nil || len(sha256Raw) != sha256.Size {
465 makeRespError(ctx, w, "Invalid OID", http.StatusBadRequest)
466 return
467 }
468
469 fullPath := path.Join(repo+".git", "lfs/objects", oid[:2], oid[2:4], oid)
470 obj, err := h.mc.GetObject(ctx, h.bucket, fullPath, minio.GetObjectOptions{})
471
472 var resp minio.ErrorResponse
473 if errors.As(err, &resp) && resp.StatusCode != http.StatusNotFound {
474 makeRespError(ctx, w, "Not found", http.StatusNotFound)
475 return
476 } else if err != nil {
477 reqlog(ctx, "Failed to get object: %s", err)
478 makeRespError(ctx, w, "Failed to get object", http.StatusInternalServerError)
479 return
480 }
481
482 stat, err := obj.Stat()
483 if err != nil {
484 reqlog(ctx, "Failed to stat: %s", err)
485 makeRespError(ctx, w, "Internal server error", http.StatusInternalServerError)
486 }
487
488 if stat.Size != claims.Gitolfs3.Size {
489 reqlog(ctx, "Claims size does not match S3 object size")
490 makeRespError(ctx, w, "Internal server error", http.StatusInternalServerError)
491 }
492
493 vr := newValidatingReader(claims.Gitolfs3.Size, sha256Raw, obj)
494 _, err = io.Copy(w, vr)
495 if errors.Is(err, errBadSum) {
496 reqlog(ctx, "Bad object checksum")
497 }
498}
499
390type parsedBatchObject struct { 500type parsedBatchObject struct {
391 firstByte string 501 firstByte string
392 secondByte string 502 secondByte string
@@ -414,6 +524,7 @@ var requestIDKey requestID
414 524
415// TODO: make a shared package for this 525// TODO: make a shared package for this
416type lfsAuthGitolfs3Claims struct { 526type lfsAuthGitolfs3Claims struct {
527 Type string `json:"type"`
417 Repository string `json:"repository"` 528 Repository string `json:"repository"`
418 Permission operation `json:"permission"` 529 Permission operation `json:"permission"`
419} 530}
@@ -448,7 +559,7 @@ func (h *handler) getGitoliteAccess(repo, user, gitolitePerm string, refspec *st
448 return true, nil 559 return true, nil
449} 560}
450 561
451func (h *handler) authorize(w http.ResponseWriter, r *http.Request, or operationRequest) bool { 562func (h *handler) authorizeBatchAPI(w http.ResponseWriter, r *http.Request, or operationRequest) bool {
452 user := h.anonUser 563 user := h.anonUser
453 ctx := r.Context() 564 ctx := r.Context()
454 565
@@ -471,6 +582,10 @@ func (h *handler) authorize(w http.ResponseWriter, r *http.Request, or operation
471 return false 582 return false
472 } 583 }
473 584
585 if claims.Gitolfs3.Type != "batch-api" {
586 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
587 return false
588 }
474 if claims.Gitolfs3.Repository != or.repository { 589 if claims.Gitolfs3.Repository != or.repository {
475 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) 590 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
476 return false 591 return false
@@ -539,7 +654,7 @@ func (h *handler) handleBatchAPI(w http.ResponseWriter, r *http.Request, repo st
539 if body.Ref != nil { 654 if body.Ref != nil {
540 or.refspec = &body.Ref.Name 655 or.refspec = &body.Ref.Name
541 } 656 }
542 if !h.authorize(w, r.WithContext(ctx), or) { 657 if !h.authorizeBatchAPI(w, r.WithContext(ctx), or) {
543 return 658 return
544 } 659 }
545 660
@@ -632,12 +747,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
632 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/") 747 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
633 reqlog(ctx, "Handling object PUT for repository: %s, OID: %s", repo, oid) 748 reqlog(ctx, "Handling object PUT for repository: %s, OID: %s", repo, oid)
634 749
635 if r.Method != http.MethodPut { 750 switch r.Method {
751 case http.MethodGet:
752 h.handleGetObject(w, r.WithContext(ctx), repo, oid)
753 case http.MethodPut:
754 h.handlePutObject(w, r.WithContext(ctx), repo, oid)
755 default:
636 makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed) 756 makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed)
637 return
638 } 757 }
639 758
640 h.handlePutObject(w, r.WithContext(ctx), repo, oid)
641 return 759 return
642 } 760 }
643 761