Parent directory

git_lfs_locks.go

13186 bytes
  1package web
  2
  3import (
  4	"encoding/json"
  5	"errors"
  6	"net/http"
  7	"net/url"
  8	"strconv"
  9	"strings"
 10
 11	"charm.land/log/v2"
 12	"github.com/charmbracelet/soft-serve/pkg/access"
 13	"github.com/charmbracelet/soft-serve/pkg/db"
 14	"github.com/charmbracelet/soft-serve/pkg/db/models"
 15	"github.com/charmbracelet/soft-serve/pkg/lfs"
 16	"github.com/charmbracelet/soft-serve/pkg/proto"
 17	"github.com/charmbracelet/soft-serve/pkg/store"
 18	"github.com/gorilla/mux"
 19)
 20
 21func serviceLfsLocks(w http.ResponseWriter, r *http.Request) {
 22	switch r.Method {
 23	case http.MethodGet:
 24		serviceLfsLocksGet(w, r)
 25	case http.MethodPost:
 26		serviceLfsLocksCreate(w, r)
 27	default:
 28		renderMethodNotAllowed(w, r)
 29	}
 30}
 31
 32// POST: /<repo>.git/info/lfs/objects/locks
 33func serviceLfsLocksCreate(w http.ResponseWriter, r *http.Request) {
 34	if !isLfs(r) {
 35		renderNotAcceptable(w)
 36		return
 37	}
 38
 39	ctx := r.Context()
 40	logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
 41
 42	// Checked in withAccess too. Repeated here so a routing or middleware
 43	// mistake cannot turn into an unauthorized lock.
 44	if access.FromContext(ctx) < access.ReadWriteAccess {
 45		askCredentials(w, r)
 46		renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
 47			Message: "write access required",
 48		})
 49		return
 50	}
 51
 52	var req lfs.LockCreateRequest
 53	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 54		logger.Error("error decoding json", "err", err)
 55		renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
 56			Message: "invalid request: " + err.Error(),
 57		})
 58		return
 59	}
 60
 61	repo, ok := lfsRepository(w, r)
 62	if !ok {
 63		return
 64	}
 65
 66	user := proto.UserFromContext(ctx)
 67	if user == nil {
 68		logger.Error("error getting user from context")
 69		renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
 70			Message: "user not found",
 71		})
 72		return
 73	}
 74
 75	dbx := db.FromContext(ctx)
 76	datastore := store.FromContext(ctx)
 77	if err := datastore.CreateLFSLockForUser(ctx, dbx, repo.ID(), user.ID(), req.Path, req.Ref.Name); err != nil {
 78		err = db.WrapError(err)
 79		if errors.Is(err, db.ErrDuplicateKey) {
 80			errResp := lfs.LockResponse{
 81				ErrorResponse: lfs.ErrorResponse{
 82					Message: "lock already exists",
 83				},
 84			}
 85			lock, err := datastore.GetLFSLockForUserPath(ctx, dbx, repo.ID(), user.ID(), req.Path)
 86			if err == nil {
 87				errResp.Lock = lfs.Lock{
 88					ID:       strconv.FormatInt(lock.ID, 10),
 89					Path:     lock.Path,
 90					LockedAt: lock.CreatedAt,
 91				}
 92				lockOwner := lfs.Owner{
 93					Name: user.Username(),
 94				}
 95				if lock.UserID != user.ID() {
 96					owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
 97					if err != nil {
 98						logger.Error("error getting lock owner", "err", err)
 99						renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
100							Message: "internal server error",
101						})
102						return
103					}
104					lockOwner.Name = owner.Username
105				}
106				errResp.Lock.Owner = lockOwner
107			}
108			renderJSON(w, http.StatusConflict, errResp)
109			return
110		}
111		logger.Error("error creating lock", "err", err)
112		renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
113			Message: "internal server error",
114		})
115		return
116	}
117
118	lock, err := datastore.GetLFSLockForUserPath(ctx, dbx, repo.ID(), user.ID(), req.Path)
119	if err != nil {
120		logger.Error("error getting lock", "err", err)
121		renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
122			Message: "internal server error",
123		})
124		return
125	}
126
127	renderJSON(w, http.StatusCreated, lfs.LockResponse{
128		Lock: lfs.Lock{
129			ID:       strconv.FormatInt(lock.ID, 10),
130			Path:     lock.Path,
131			LockedAt: lock.CreatedAt,
132			Owner: lfs.Owner{
133				Name: user.Username(),
134			},
135		},
136	})
137}
138
139// GET: /<repo>.git/info/lfs/objects/locks
140func serviceLfsLocksGet(w http.ResponseWriter, r *http.Request) {
141	accept := r.Header.Get("Accept")
142	if !strings.HasPrefix(accept, lfs.MediaType) {
143		renderNotAcceptable(w)
144		return
145	}
146
147	parseLocksQuery := func(values url.Values) (path string, id int64, cursor int, limit int, refspec string) {
148		path = values.Get("path")
149		idStr := values.Get("id")
150		if idStr != "" {
151			id, _ = strconv.ParseInt(idStr, 10, 64)
152		}
153		cursorStr := values.Get("cursor")
154		if cursorStr != "" {
155			cursor, _ = strconv.Atoi(cursorStr)
156		}
157		limitStr := values.Get("limit")
158		if limitStr != "" {
159			limit, _ = strconv.Atoi(limitStr)
160		}
161		refspec = values.Get("refspec")
162		return
163	}
164
165	ctx := r.Context()
166	// TODO: respect refspec
167	path, id, cursor, limit, _ := parseLocksQuery(r.URL.Query())
168	if limit > 100 {
169		limit = 100
170	} else if limit <= 0 {
171		limit = lfs.DefaultLocksLimit
172	}
173
174	// cursor is the page number
175	if cursor <= 0 {
176		cursor = 1
177	}
178
179	logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
180	dbx := db.FromContext(ctx)
181	datastore := store.FromContext(ctx)
182	repo, ok := lfsRepository(w, r)
183	if !ok {
184		return
185	}
186
187	if id > 0 {
188		lock, err := datastore.GetLFSLockByID(ctx, dbx, repo.ID(), id)
189		if err != nil {
190			if errors.Is(err, db.ErrRecordNotFound) {
191				renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
192					Message: "lock not found",
193				})
194				return
195			}
196			logger.Error("error getting lock", "err", err)
197			renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
198				Message: "internal server error",
199			})
200			return
201		}
202
203		owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
204		if err != nil {
205			logger.Error("error getting lock owner", "err", err)
206			renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
207				Message: "internal server error",
208			})
209			return
210		}
211
212		renderJSON(w, http.StatusOK, lfs.LockListResponse{
213			Locks: []lfs.Lock{
214				{
215					ID:       strconv.FormatInt(lock.ID, 10),
216					Path:     lock.Path,
217					LockedAt: lock.CreatedAt,
218					Owner: lfs.Owner{
219						Name: owner.Username,
220					},
221				},
222			},
223		})
224		return
225	} else if path != "" {
226		lock, err := datastore.GetLFSLockForPath(ctx, dbx, repo.ID(), path)
227		if err != nil {
228			if errors.Is(err, db.ErrRecordNotFound) {
229				renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
230					Message: "lock not found",
231				})
232				return
233			}
234			logger.Error("error getting lock", "err", err)
235			renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
236				Message: "internal server error",
237			})
238			return
239		}
240
241		owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
242		if err != nil {
243			logger.Error("error getting lock owner", "err", err)
244			renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
245				Message: "internal server error",
246			})
247			return
248		}
249
250		renderJSON(w, http.StatusOK, lfs.LockListResponse{
251			Locks: []lfs.Lock{
252				{
253					ID:       strconv.FormatInt(lock.ID, 10),
254					Path:     lock.Path,
255					LockedAt: lock.CreatedAt,
256					Owner: lfs.Owner{
257						Name: owner.Username,
258					},
259				},
260			},
261		})
262		return
263	}
264
265	locks, err := datastore.GetLFSLocks(ctx, dbx, repo.ID(), cursor, limit)
266	if err != nil {
267		logger.Error("error getting locks", "err", err)
268		renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
269			Message: "internal server error",
270		})
271		return
272	}
273
274	lockList := make([]lfs.Lock, len(locks))
275	users := map[int64]models.User{}
276	for i, lock := range locks {
277		owner, ok := users[lock.UserID]
278		if !ok {
279			owner, err = datastore.GetUserByID(ctx, dbx, lock.UserID)
280			if err != nil {
281				logger.Error("error getting lock owner", "err", err)
282				renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
283					Message: "internal server error",
284				})
285				return
286			}
287			users[lock.UserID] = owner
288		}
289
290		lockList[i] = lfs.Lock{
291			ID:       strconv.FormatInt(lock.ID, 10),
292			Path:     lock.Path,
293			LockedAt: lock.CreatedAt,
294			Owner: lfs.Owner{
295				Name: owner.Username,
296			},
297		}
298	}
299
300	resp := lfs.LockListResponse{
301		Locks: lockList,
302	}
303	if len(locks) == limit {
304		resp.NextCursor = strconv.Itoa(cursor + 1)
305	}
306
307	renderJSON(w, http.StatusOK, resp)
308}
309
310// POST: /<repo>.git/info/lfs/objects/locks/verify
311func serviceLfsLocksVerify(w http.ResponseWriter, r *http.Request) {
312	if !isLfs(r) {
313		renderNotAcceptable(w)
314		return
315	}
316
317	ctx := r.Context()
318	logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
319	repo, ok := lfsRepository(w, r)
320	if !ok {
321		return
322	}
323
324	var req lfs.LockVerifyRequest
325	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
326		logger.Error("error decoding request", "err", err)
327		renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
328			Message: "invalid request: " + err.Error(),
329		})
330		return
331	}
332
333	// TODO: refspec
334	cursor, _ := strconv.Atoi(req.Cursor)
335	if cursor <= 0 {
336		cursor = 1
337	}
338
339	limit := req.Limit
340	if limit > 100 {
341		limit = 100
342	} else if limit <= 0 {
343		limit = lfs.DefaultLocksLimit
344	}
345
346	dbx := db.FromContext(ctx)
347	datastore := store.FromContext(ctx)
348	user := proto.UserFromContext(ctx)
349	ours := make([]lfs.Lock, 0)
350	theirs := make([]lfs.Lock, 0)
351
352	var resp lfs.LockVerifyResponse
353	locks, err := datastore.GetLFSLocks(ctx, dbx, repo.ID(), cursor, limit)
354	if err != nil {
355		logger.Error("error getting locks", "err", err)
356		renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
357			Message: "internal server error",
358		})
359		return
360	}
361
362	users := map[int64]models.User{}
363	for _, lock := range locks {
364		owner, ok := users[lock.UserID]
365		if !ok {
366			owner, err = datastore.GetUserByID(ctx, dbx, lock.UserID)
367			if err != nil {
368				logger.Error("error getting lock owner", "err", err)
369				renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
370					Message: "internal server error",
371				})
372				return
373			}
374			users[lock.UserID] = owner
375		}
376
377		l := lfs.Lock{
378			ID:       strconv.FormatInt(lock.ID, 10),
379			Path:     lock.Path,
380			LockedAt: lock.CreatedAt,
381			Owner: lfs.Owner{
382				Name: owner.Username,
383			},
384		}
385
386		if user != nil && user.ID() == lock.UserID {
387			ours = append(ours, l)
388		} else {
389			theirs = append(theirs, l)
390		}
391	}
392
393	resp.Ours = ours
394	resp.Theirs = theirs
395
396	if len(locks) == limit {
397		resp.NextCursor = strconv.Itoa(cursor + 1)
398	}
399
400	renderJSON(w, http.StatusOK, resp)
401}
402
403// POST: /<repo>.git/info/lfs/objects/locks/:lockID/unlock
404func serviceLfsLocksDelete(w http.ResponseWriter, r *http.Request) {
405	if !isLfs(r) {
406		renderNotAcceptable(w)
407		return
408	}
409
410	ctx := r.Context()
411	logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
412	lockIDStr := mux.Vars(r)["lock_id"]
413	if lockIDStr == "" {
414		logger.Error("error getting lock id")
415		renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
416			Message: "invalid request",
417		})
418		return
419	}
420
421	lockID, err := strconv.ParseInt(lockIDStr, 10, 64)
422	if err != nil {
423		logger.Error("error parsing lock id", "err", err)
424		renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
425			Message: "invalid request",
426		})
427		return
428	}
429
430	var req lfs.LockDeleteRequest
431	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
432		logger.Error("error decoding request", "err", err)
433		renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
434			Message: "invalid request: " + err.Error(),
435		})
436		return
437	}
438
439	dbx := db.FromContext(ctx)
440	datastore := store.FromContext(ctx)
441	repo, ok := lfsRepository(w, r)
442	if !ok {
443		return
444	}
445
446	// The lock being deleted. Looked up scoped to this repository so that a
447	// lock ID belonging to another repository cannot be read through it.
448	lock, err := datastore.GetLFSLockByID(ctx, dbx, repo.ID(), lockID)
449	if err != nil {
450		logger.Error("error getting lock", "err", err)
451		renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
452			Message: "lock not found",
453		})
454		return
455	}
456
457	owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
458	if err != nil {
459		logger.Error("error getting lock owner", "err", err)
460		renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
461			Message: "internal server error",
462		})
463		return
464	}
465
466	l := lfs.Lock{
467		ID:       strconv.FormatInt(lock.ID, 10),
468		Path:     lock.Path,
469		LockedAt: lock.CreatedAt,
470		Owner: lfs.Owner{
471			Name: owner.Username,
472		},
473	}
474
475	// Retrieve user context first for authorization checks
476	user := proto.UserFromContext(ctx)
477	if user == nil {
478		logger.Error("error getting user from context")
479		renderJSON(w, http.StatusUnauthorized, lfs.ErrorResponse{
480			Message: "unauthorized",
481		})
482		return
483	}
484
485	// Force delete another user's lock (requires admin privileges)
486	if req.Force {
487		if !user.IsAdmin() {
488			logger.Error("non-admin user attempted force delete", "user", user.Username())
489			renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
490				Message: "admin access required for force delete",
491			})
492			return
493		}
494
495		if err := datastore.DeleteLFSLock(ctx, dbx, repo.ID(), lockID); err != nil {
496			logger.Error("error deleting lock", "err", err)
497			renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
498				Message: "internal server error",
499			})
500			return
501		}
502
503		renderJSON(w, http.StatusOK, l)
504		return
505	}
506
507	// Delete our own lock - verify ownership
508	if owner.ID != user.ID() {
509		logger.Error("error deleting another user's lock")
510		renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
511			Message: "lock belongs to another user",
512		})
513		return
514	}
515
516	if err := datastore.DeleteLFSLock(ctx, dbx, repo.ID(), lockID); err != nil {
517		logger.Error("error deleting lock", "err", err)
518		renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
519			Message: "internal server error",
520		})
521		return
522	}
523
524	renderJSON(w, http.StatusOK, lfs.LockResponse{Lock: l})
525}