Skip to content

Commit

Permalink
fix: git lfs endpoint auth
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Jul 25, 2023
1 parent 5ceacaf commit be83043
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 161 deletions.
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (

require (
github.com/caarlos0/env/v8 v8.0.0
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230721203144-64d90e7a36a1
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230724210439-640f4fb1e14e
github.com/charmbracelet/keygen v0.4.3
github.com/charmbracelet/log v0.2.3-0.20230713155356-557335e40e35
github.com/charmbracelet/ssh v0.0.0-20230720143903-5bdd92839155
Expand Down Expand Up @@ -49,21 +49,26 @@ require (
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caarlos0/sshmarshal v0.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/git-lfs/git-lfs/v3 v3.3.0 // indirect
github.com/git-lfs/gitobj/v2 v2.1.1 // indirect
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
github.com/git-lfs/wildmatch/v2 v2.0.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/leonelquinteros/gotext v1.5.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
Expand All @@ -76,6 +81,7 @@ require (
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
Expand Down
88 changes: 86 additions & 2 deletions go.sum

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion server/backend/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func StoreRepoMissingLFSObjects(ctx context.Context, repo proto.Repository, dbx
return db.WrapError(err)
}

return strg.Put(path.Join("objects", p.RelativePath()), content)
_, err := strg.Put(path.Join("objects", p.RelativePath()), content)
return err
})
})
}
Expand Down
7 changes: 6 additions & 1 deletion server/backend/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func (d *Backend) AccessLevelByPublicKey(ctx context.Context, repo string, pk ss
}

// AccessLevelForUser returns the access level of a user for a repository.
// TODO: user repository ownership
func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user proto.User) access.AccessLevel {
var username string
anon := d.AnonAccess(ctx)
Expand All @@ -54,7 +55,11 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot
}

// If the repository exists, check if the user is a collaborator.
r, _ := d.Repository(ctx, repo)
r := proto.RepositoryFromContext(ctx)
if r == nil {
r, _ = d.Repository(ctx, repo)
}

if r != nil {
// If the user is a collaborator, they have read/write access.
isCollab, _ := d.IsCollaborator(ctx, repo, username)
Expand Down
53 changes: 34 additions & 19 deletions server/git/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/charmbracelet/git-lfs-transfer/transfer"
Expand Down Expand Up @@ -99,15 +100,10 @@ func LFSTransfer(ctx context.Context, cmd ServiceCommand) error {
}

// Batch implements transfer.Backend.
func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer) ([]transfer.BatchItem, error) {
repo := proto.RepositoryFromContext(t.ctx)
if repo == nil {
return nil, errors.New("no repository in context")
}

func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ ...string) ([]transfer.BatchItem, error) {
items := make([]transfer.BatchItem, 0)
for _, p := range pointers {
obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, repo.ID(), p.Oid)
obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), p.Oid)
if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
return items, db.WrapError(err)
}
Expand All @@ -118,7 +114,7 @@ func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer) ([]transfer.B
}

if exist && obj.ID == 0 {
if err := t.store.CreateLFSObject(t.ctx, t.dbx, repo.ID(), p.Oid, p.Size); err != nil {
if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), p.Oid, p.Size); err != nil {
return items, db.WrapError(err)
}
}
Expand All @@ -143,6 +139,7 @@ func (t *lfsTransfer) Download(oid string, _ ...string) (fs.File, error) {

type uploadObject struct {
oid string
size int64
object storage.Object
}

Expand All @@ -161,7 +158,8 @@ func (t *lfsTransfer) StartUpload(oid string, r io.Reader, _ ...string) (interfa
tempName := fmt.Sprintf("%s%x", oid, randBytes)
tempName = path.Join(tempDir, tempName)

if err := t.storage.Put(tempName, r); err != nil {
written, err := t.storage.Put(tempName, r)
if err != nil {
t.logger.Errorf("error putting object: %v", err)
return nil, err
}
Expand All @@ -176,24 +174,43 @@ func (t *lfsTransfer) StartUpload(oid string, r io.Reader, _ ...string) (interfa

return uploadObject{
oid: oid,
size: written,
object: obj,
}, nil
}

// FinishUpload implements transfer.Backend.
func (t *lfsTransfer) FinishUpload(state interface{}, _ ...string) error {
func (t *lfsTransfer) FinishUpload(state interface{}, args ...string) error {
upl, ok := state.(uploadObject)
if !ok {
return errors.New("invalid state")
}

var size int64
for _, arg := range args {
if strings.HasPrefix(arg, "size=") {
size, _ = strconv.ParseInt(strings.TrimPrefix(arg, "size="), 10, 64)
break
}
}

pointer := transfer.Pointer{
Oid: upl.oid,
}
if size > 0 {
pointer.Size = size
} else {
pointer.Size = upl.size
}

if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), pointer.Oid, pointer.Size); err != nil {
return db.WrapError(err)
}

expectedPath := path.Join("objects", pointer.RelativePath())
if err := t.storage.Rename(upl.object.Name(), expectedPath); err != nil {
t.logger.Errorf("error renaming object: %v", err)
_ = t.store.DeleteLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), pointer.Oid)
return err
}

Expand All @@ -215,19 +232,17 @@ func (t *lfsTransfer) Verify(oid string, args map[string]string) (transfer.Statu
return transfer.NewFailureStatus(transfer.StatusBadRequest, "invalid size argument"), nil
}

pointer := transfer.Pointer{
Oid: oid,
Size: expectedSize,
}
expectedPath := path.Join("objects", pointer.RelativePath())
stat, err := t.storage.Stat(expectedPath)
obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), oid)
if err != nil {
t.logger.Errorf("error stating object: %v", err)
if errors.Is(err, db.ErrRecordNotFound) {
return transfer.NewFailureStatus(transfer.StatusNotFound, "object not found"), nil
}
t.logger.Errorf("error getting object: %v", err)
return nil, err
}

if stat.Size() != expectedSize {
t.logger.Errorf("size mismatch: %d != %d", stat.Size(), expectedSize)
if obj.Size != expectedSize {
t.logger.Errorf("size mismatch: %d != %d", obj.Size, expectedSize)
return transfer.NewFailureStatus(transfer.StatusConflict, "size mismatch"), nil
}

Expand Down
17 changes: 14 additions & 3 deletions server/git/lfs_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"time"

"github.com/charmbracelet/log"
"github.com/charmbracelet/soft-serve/server/config"
"github.com/charmbracelet/soft-serve/server/jwk"
"github.com/charmbracelet/soft-serve/server/lfs"
Expand All @@ -22,29 +23,35 @@ func LFSAuthenticate(ctx context.Context, cmd ServiceCommand) error {
return errors.New("missing args")
}

logger := log.FromContext(ctx).WithPrefix("ssh.lfs-authenticate")
operation := cmd.Args[1]
if operation != lfs.OperationDownload && operation != lfs.OperationUpload {
logger.Errorf("invalid operation: %s", operation)
return errors.New("invalid operation")
}

user := proto.UserFromContext(ctx)
if user == nil {
logger.Errorf("missing user")
return proto.ErrUserNotFound
}

repo := proto.RepositoryFromContext(ctx)
if repo == nil {
logger.Errorf("missing repository")
return proto.ErrRepoNotFound
}

cfg := config.FromContext(ctx)
kp, err := jwk.NewPair(cfg)
if err != nil {
logger.Error("failed to get JWK pair", "err", err)
return err
}

now := time.Now()
expiresAt := now.Add(time.Hour)
expiresIn := time.Minute * 5
expiresAt := now.Add(expiresIn)
claims := jwt.RegisteredClaims{
Subject: fmt.Sprintf("%s#%d", user.Username(), user.ID()),
ExpiresAt: jwt.NewNumericDate(expiresAt), // expire in an hour
Expand All @@ -60,15 +67,19 @@ func LFSAuthenticate(ctx context.Context, cmd ServiceCommand) error {
token.Header["kid"] = kp.JWK().KeyID
j, err := token.SignedString(kp.PrivateKey())
if err != nil {
logger.Error("failed to sign token", "err", err)
return err
}

href := fmt.Sprintf("%s/%s.git/info/lfs", cfg.HTTP.PublicURL, repo.Name())
logger.Debug("generated token", "token", j, "href", href, "expires_at", expiresAt)

return json.NewEncoder(cmd.Stdout).Encode(lfs.AuthenticateResponse{
Header: map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", j),
},
Href: fmt.Sprintf("%s/%s.git/info/lfs", cfg.HTTP.PublicURL, repo.Name()),
Href: href,
ExpiresAt: expiresAt,
ExpiresIn: time.Hour,
ExpiresIn: expiresIn,
})
}
14 changes: 6 additions & 8 deletions server/ssh/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ func cmdName(args []string) string {
func RootCommand(s ssh.Session) *cobra.Command {
ctx := s.Context()
cfg := config.FromContext(ctx)
be := backend.FromContext(ctx)

args := s.Command()
cliCommandCounter.WithLabelValues(cmdName(args)).Inc()
Expand Down Expand Up @@ -140,7 +139,7 @@ func RootCommand(s ssh.Session) *cobra.Command {
rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.SetErr(s.Stderr())

user, _ := be.UserByPublicKey(s.Context(), s.PublicKey())
user := proto.UserFromContext(ctx)
isAdmin := isPublicKeyAdmin(cfg, s.PublicKey()) || (user != nil && user.IsAdmin())
if user != nil || isAdmin {
if isAdmin {
Expand Down Expand Up @@ -170,8 +169,8 @@ func checkIfReadable(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
be := backend.FromContext(ctx)
rn := utils.SanitizeRepo(repo)
pk := sshutils.PublicKeyFromContext(ctx)
auth := be.AccessLevelByPublicKey(cmd.Context(), rn, pk)
user := proto.UserFromContext(ctx)
auth := be.AccessLevelForUser(cmd.Context(), rn, user)
if auth < access.ReadOnlyAccess {
return proto.ErrUnauthorized
}
Expand All @@ -189,14 +188,13 @@ func isPublicKeyAdmin(cfg *config.Config, pk ssh.PublicKey) bool {

func checkIfAdmin(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
be := backend.FromContext(ctx)
cfg := config.FromContext(ctx)
pk := sshutils.PublicKeyFromContext(ctx)
if isPublicKeyAdmin(cfg, pk) {
return nil
}

user, _ := be.UserByPublicKey(ctx, pk)
user := proto.UserFromContext(ctx)
if user == nil {
return proto.ErrUnauthorized
}
Expand All @@ -216,9 +214,9 @@ func checkIfCollab(cmd *cobra.Command, args []string) error {

ctx := cmd.Context()
be := backend.FromContext(ctx)
pk := sshutils.PublicKeyFromContext(ctx)
rn := utils.SanitizeRepo(repo)
auth := be.AccessLevelByPublicKey(ctx, rn, pk)
user := proto.UserFromContext(ctx)
auth := be.AccessLevelForUser(cmd.Context(), rn, user)
if auth < access.ReadWriteAccess {
return proto.ErrUnauthorized
}
Expand Down
1 change: 1 addition & 0 deletions server/ssh/cmd/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func jwtCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "jwt [repository1 repository2...]",
Short: "Generate a JSON Web Token",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg := config.FromContext(ctx)
Expand Down
12 changes: 7 additions & 5 deletions server/ssh/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,8 @@ func handleGit(s ssh.Session) {
return
}

handler := git.UploadPack
switch service {
case git.UploadArchiveService:
handler = git.UploadArchive
uploadArchiveCounter.WithLabelValues(name).Inc()
defer func() {
uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
Expand All @@ -124,7 +122,7 @@ func handleGit(s ssh.Session) {
}()
}

err := handler(ctx, cmd)
err := service.Handler(ctx, cmd)
if errors.Is(err, git.ErrInvalidRepo) {
sshFatal(s, git.ErrInvalidRepo)
} else if err != nil {
Expand All @@ -133,7 +131,11 @@ func handleGit(s ssh.Session) {
}

return
case git.LFSTransferService:
case git.LFSTransferService, git.LFSAuthenticateService:
if service == git.LFSTransferService {
return
}

if accessLevel < access.ReadWriteAccess {
sshFatal(s, git.ErrNotAuthed)
return
Expand All @@ -150,7 +152,7 @@ func handleGit(s ssh.Session) {
cmdLine[2],
}

if err := git.LFSTransfer(ctx, cmd); err != nil {
if err := service.Handler(ctx, cmd); err != nil {
logger.Error("git middleware", "err", err)
sshFatal(s, git.ErrSystemMalfunction)
return
Expand Down
9 changes: 4 additions & 5 deletions server/storage/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,18 @@ func (l *LocalStorage) Stat(name string) (fs.FileInfo, error) {
}

// Put implements Storage.
func (l *LocalStorage) Put(name string, r io.Reader) error {
func (l *LocalStorage) Put(name string, r io.Reader) (int64, error) {
name = l.fixPath(name)
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
return err
return 0, err
}

f, err := os.Create(name)
if err != nil {
return err
return 0, err
}
defer f.Close() // nolint: errcheck
_, err = io.Copy(f, r)
return err
return io.Copy(f, r)
}

// Exists implements Storage.
Expand Down
Loading

0 comments on commit be83043

Please sign in to comment.