From be830436dae0a723cd38164dc6af87861ced0334 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 24 Jul 2023 17:05:30 -0400 Subject: [PATCH] fix: git lfs endpoint auth --- go.mod | 8 +- go.sum | 88 +++++++++++++++++++- server/backend/lfs.go | 3 +- server/backend/user.go | 7 +- server/git/lfs.go | 53 +++++++----- server/git/lfs_auth.go | 17 +++- server/ssh/cmd/cmd.go | 14 ++-- server/ssh/cmd/jwt.go | 1 + server/ssh/git.go | 12 +-- server/storage/local.go | 9 +-- server/storage/storage.go | 2 +- server/web/auth.go | 76 +++++++++++------- server/web/git.go | 71 ++++++++++------ server/web/git_lfs.go | 165 ++++++++++++++++++++++++-------------- server/web/http.go | 3 + 15 files changed, 368 insertions(+), 161 deletions(-) diff --git a/go.mod b/go.mod index dad3f48e5..9c7069233 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -49,6 +49,7 @@ 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 @@ -56,7 +57,10 @@ require ( 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 @@ -64,6 +68,7 @@ require ( 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 @@ -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 diff --git a/go.sum b/go.sum index 4138d1b47..863fb20e2 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,16 @@ +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/avast/retry-go v2.4.2+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= @@ -21,8 +28,8 @@ github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5 github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230721203144-64d90e7a36a1 h1:/QzZzTDdlDYGZeC2O2y/Qw+AiHqh3vCsO4yrKDWXtqs= -github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230721203144-64d90e7a36a1/go.mod h1:eXJuVicxnjRgRMokmutZdistxoMRjBjjfqvrYq7bCIU= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230724210439-640f4fb1e14e h1:PoAjCrpdHDShzxaV8Aa9wXJ71jbA0Ji4rDPlV3uoycA= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230724210439-640f4fb1e14e/go.mod h1:eXJuVicxnjRgRMokmutZdistxoMRjBjjfqvrYq7bCIU= github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= github.com/charmbracelet/keygen v0.4.3 h1:ywOZRwkDlpmkawl0BgLTxaYWDSqp6Y4nfVVmgyyO1Mg= @@ -43,12 +50,24 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430/go.mod h1:AVSs/gZKt1bOd2AhkhbS7Qh56Hv7klde22yXVbwYJhc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/git-lfs/git-lfs/v3 v3.3.0 h1:cbRy9akD9/hDD7BaVifyNkWkURwC8RSPLzX9+siS+OE= +github.com/git-lfs/git-lfs/v3 v3.3.0/go.mod h1:5y2vfVQpxUmceMlraOmmaQ83pYptQYCvPl32ybO2IVw= +github.com/git-lfs/gitobj/v2 v2.1.1 h1:tf/VU6zL1kxa3he+nf6FO/syX+LGkm6WGDsMpfuXV7Q= +github.com/git-lfs/gitobj/v2 v2.1.1/go.mod h1:q6aqxl6Uu3gWsip5GEKpw+7459F97er8COmU45ncAxw= +github.com/git-lfs/go-netrc v0.0.0-20210914205454-f0c862dd687a/go.mod h1:70O4NAtvWn1jW8V8V+OKrJJYcxDLTmIozfi2fmSz5SI= +github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= +github.com/git-lfs/wildmatch/v2 v2.0.1 h1:Ds+aobrV5bK0wStILUOn9irllPyf9qrFETbKzwzoER8= +github.com/git-lfs/wildmatch/v2 v2.0.1/go.mod h1:EVqonpk9mXbREP3N8UkwoWdrF249uHpCUo5CPXY81gw= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= @@ -71,25 +90,46 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0= github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0= +github.com/leonelquinteros/gotext v1.5.2 h1:T2y6ebHli+rMBCjcJlHTXyUrgXqsKBhl/ormgvt7lPo= +github.com/leonelquinteros/gotext v1.5.2/go.mod h1:AT4NpQrOmyj1L/+hLja6aR0lk81yYYL4ePnj2kp7d6M= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -99,6 +139,7 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -116,6 +157,8 @@ github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -133,8 +176,12 @@ github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -166,10 +213,13 @@ github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ssgelm/cookiejarparser v1.0.1/go.mod h1:DUfC0mpjIzlDN7DzKjXpHj0qMI5m9VrZuz3wSlI+OEI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -181,7 +231,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20170210233622-6b67b3fab74d/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= @@ -192,25 +247,44 @@ goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -224,14 +298,22 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= @@ -239,10 +321,12 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/backend/lfs.go b/server/backend/lfs.go index dfc21ea69..eac557ecf 100644 --- a/server/backend/lfs.go +++ b/server/backend/lfs.go @@ -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 }) }) } diff --git a/server/backend/user.go b/server/backend/user.go index fcd3781e3..ff07660b0 100644 --- a/server/backend/user.go +++ b/server/backend/user.go @@ -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) @@ -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) diff --git a/server/git/lfs.go b/server/git/lfs.go index d85be8b71..287ad09cb 100644 --- a/server/git/lfs.go +++ b/server/git/lfs.go @@ -10,6 +10,7 @@ import ( "path" "path/filepath" "strconv" + "strings" "time" "github.com/charmbracelet/git-lfs-transfer/transfer" @@ -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) } @@ -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) } } @@ -143,6 +139,7 @@ func (t *lfsTransfer) Download(oid string, _ ...string) (fs.File, error) { type uploadObject struct { oid string + size int64 object storage.Object } @@ -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 } @@ -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 } @@ -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 } diff --git a/server/git/lfs_auth.go b/server/git/lfs_auth.go index 55f10dabe..0568d2907 100644 --- a/server/git/lfs_auth.go +++ b/server/git/lfs_auth.go @@ -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" @@ -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 @@ -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, }) } diff --git a/server/ssh/cmd/cmd.go b/server/ssh/cmd/cmd.go index 4fda5cece..b965409de 100644 --- a/server/ssh/cmd/cmd.go +++ b/server/ssh/cmd/cmd.go @@ -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() @@ -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 { @@ -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 } @@ -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 } @@ -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 } diff --git a/server/ssh/cmd/jwt.go b/server/ssh/cmd/jwt.go index d77097d4f..b574889f5 100644 --- a/server/ssh/cmd/jwt.go +++ b/server/ssh/cmd/jwt.go @@ -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) diff --git a/server/ssh/git.go b/server/ssh/git.go index b01b6df77..ec639f68f 100644 --- a/server/ssh/git.go +++ b/server/ssh/git.go @@ -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()) @@ -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 { @@ -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 @@ -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 diff --git a/server/storage/local.go b/server/storage/local.go index 8a51157af..496f20e37 100644 --- a/server/storage/local.go +++ b/server/storage/local.go @@ -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. diff --git a/server/storage/storage.go b/server/storage/storage.go index dc435dbbb..9f8467262 100644 --- a/server/storage/storage.go +++ b/server/storage/storage.go @@ -16,7 +16,7 @@ type Object interface { type Storage interface { Open(name string) (Object, error) Stat(name string) (fs.FileInfo, error) - Put(name string, r io.Reader) error + Put(name string, r io.Reader) (int64, error) Delete(name string) error Exists(name string) (bool, error) Rename(oldName, newName string) error diff --git a/server/web/auth.go b/server/web/auth.go index 40c9e730b..f8ff047ed 100644 --- a/server/web/auth.go +++ b/server/web/auth.go @@ -7,6 +7,7 @@ import ( "net/http" "strings" + "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/server/backend" "github.com/charmbracelet/soft-serve/server/config" "github.com/charmbracelet/soft-serve/server/proto" @@ -14,45 +15,58 @@ import ( ) // authenticate authenticates the user from the request. -func authenticate(ctx context.Context, r *http.Request) (proto.User, error) { - header := r.Header.Get("Authorization") - if header == "" { - return nil, errors.New("missing authorization header") - } - - parts := strings.SplitN(header, " ", 2) - if len(parts) != 2 { - return nil, errors.New("invalid authorization header") - } +func authenticate(r *http.Request) (proto.User, error) { + ctx := r.Context() + logger := log.FromContext(ctx) - // TODO: add basic, and token types - be := backend.FromContext(ctx) - switch strings.ToLower(parts[0]) { - case "bearer": - claims, err := getJWTClaims(ctx, parts[1]) - if err != nil { - return nil, err - } + // Check for auth header + header := r.Header.Get("Authorization") + if header != "" { + logger.Debug("authorization", "header", header) - // Find the user - parts := strings.SplitN(claims.Subject, "#", 2) + parts := strings.SplitN(header, " ", 2) if len(parts) != 2 { - return nil, errors.New("invalid jwt subject") + return nil, errors.New("invalid authorization header") } - user, err := be.User(ctx, parts[0]) - if err != nil { - return nil, err - } + // TODO: add basic, and token types + be := backend.FromContext(ctx) + switch strings.ToLower(parts[0]) { + case "bearer": + claims, err := getJWTClaims(ctx, parts[1]) + if err != nil { + logger.Error("failed to get jwt claims", "err", err) + return nil, err + } - if fmt.Sprintf("%s#%d", user.Username(), user.ID()) != claims.Subject { - return nil, errors.New("invalid jwt subject") - } + // Find the user + parts := strings.SplitN(claims.Subject, "#", 2) + if len(parts) != 2 { + logger.Error("invalid jwt subject", "subject", claims.Subject) + return nil, errors.New("invalid jwt subject") + } + + user, err := be.User(ctx, parts[0]) + if err != nil { + logger.Error("failed to get user", "err", err) + return nil, err + } + + expectedSubject := fmt.Sprintf("%s#%d", user.Username(), user.ID()) + if expectedSubject != claims.Subject { + logger.Error("invalid jwt subject", "subject", claims.Subject, "expected", expectedSubject) + return nil, errors.New("invalid jwt subject") + } - return user, nil - default: - return nil, errors.New("invalid authorization header") + return user, nil + default: + return nil, errors.New("invalid authorization header") + } } + + logger.Debug("no authorization header") + + return nil, proto.ErrUserNotFound } func getJWTClaims(ctx context.Context, bearer string) (*jwt.RegisteredClaims, error) { diff --git a/server/web/git.go b/server/web/git.go index bb5cd2036..8ed317a48 100644 --- a/server/web/git.go +++ b/server/web/git.go @@ -60,7 +60,7 @@ func (g GitRoute) Match(r *http.Request) *http.Request { oid = m[2] } fallthrough - case strings.HasPrefix(r.URL.Path, m[1]+"/info/lfs"): + case strings.HasPrefix(file, "info/lfs"): service = gitLfsService } @@ -217,7 +217,7 @@ func withAccess(next http.Handler) http.HandlerFunc { ctx = proto.WithRepositoryContext(ctx, repo) r = r.WithContext(ctx) - user, err := authenticate(ctx, r) + user, err := authenticate(r) if err != nil { if !errors.Is(err, proto.ErrUserNotFound) { logger.Error("failed to authenticate", "err", err) @@ -233,11 +233,23 @@ func withAccess(next http.Handler) http.HandlerFunc { ctx = proto.WithUserContext(ctx, user) r = r.WithContext(ctx) + if user != nil { + logger.Info("found user", "username", user.Username()) + } + service := git.Service(pat.Param(r, "service")) + if service == "" { + // Get service from request params + service = getServiceType(r) + } + accessLevel := be.AccessLevelForUser(ctx, repoName, user) ctx = access.WithContext(ctx, accessLevel) r = r.WithContext(ctx) + logger.Info("access level", "repo", repoName, "level", accessLevel) + + file := pat.Param(r, "file") switch service { case git.ReceivePackService: if accessLevel < access.ReadWriteAccess { @@ -245,18 +257,12 @@ func withAccess(next http.Handler) http.HandlerFunc { return } case gitLfsService: - switch r.Method { - case http.MethodPut, http.MethodDelete: - if accessLevel < access.ReadWriteAccess { - renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{ - Message: "write access required", - }) - return - } - case http.MethodPost: - // Locks verify requires write access - // https://github.com/git-lfs/git-lfs/blob/main/docs/api/locking.md#unauthorized-response-2 - if strings.HasSuffix(r.URL.Path, "/locks/verify") { + switch { + case strings.HasPrefix(file, "info/lfs/locks"): + switch { + case strings.HasSuffix(file, "locks/verify"): + // Locks verify requires write access + // https://github.com/git-lfs/git-lfs/blob/main/docs/api/locking.md#unauthorized-response-2 if accessLevel < access.ReadWriteAccess { renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{ Message: "write access required", @@ -264,17 +270,31 @@ func withAccess(next http.Handler) http.HandlerFunc { return } } - fallthrough - default: - if accessLevel < access.ReadOnlyAccess { - hdr := `Basic realm="Git LFS" charset="UTF-8", Token, Bearer` - w.Header().Set("LFS-Authenticate", hdr) - w.Header().Set("WWW-Authenticate", hdr) - renderJSON(w, http.StatusUnauthorized, lfs.ErrorResponse{ - Message: "credentials needed", - }) - return + case strings.HasPrefix(file, "info/lfs/objects/basic"): + switch r.Method { + case http.MethodPut: + // Basic upload + if accessLevel < access.ReadWriteAccess { + renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{ + Message: "write access required", + }) + return + } + case http.MethodGet: + // Basic download + case http.MethodPost: + // Basic verify } + case strings.HasPrefix(file, "info/lfs/objects/batch"): + } + if accessLevel < access.ReadOnlyAccess { + hdr := `Basic realm="Git LFS" charset="UTF-8", Token, Bearer` + w.Header().Set("LFS-Authenticate", hdr) + w.Header().Set("WWW-Authenticate", hdr) + renderJSON(w, http.StatusUnauthorized, lfs.ErrorResponse{ + Message: "credentials needed", + }) + return } default: if accessLevel < access.ReadOnlyAccess { @@ -505,7 +525,8 @@ func getServiceType(r *http.Request) git.Service { } func isSmart(r *http.Request, service git.Service) bool { - return r.Header.Get("Content-Type") == fmt.Sprintf("application/x-%s-request", service) + contentType := r.Header.Get("Content-Type") + return strings.HasPrefix(contentType, fmt.Sprintf("application/x-%s-request", service)) } func updateServerInfo(ctx context.Context, dir string) error { diff --git a/server/web/git_lfs.go b/server/web/git_lfs.go index 6a3793887..d17fbc073 100644 --- a/server/web/git_lfs.go +++ b/server/web/git_lfs.go @@ -10,6 +10,7 @@ import ( "path" "path/filepath" "strconv" + "strings" "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/server/backend" @@ -17,6 +18,7 @@ import ( "github.com/charmbracelet/soft-serve/server/db" "github.com/charmbracelet/soft-serve/server/git" "github.com/charmbracelet/soft-serve/server/lfs" + "github.com/charmbracelet/soft-serve/server/proto" "github.com/charmbracelet/soft-serve/server/storage" "github.com/charmbracelet/soft-serve/server/store" "goji.io/pat" @@ -30,15 +32,16 @@ const gitLfsService git.Service = "git-lfs-service" // TODO: support refname & authentication // POST: /.git/info/lfs/objects/batch func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Content-Type") != lfs.MediaType { + ctx := r.Context() + logger := log.FromContext(ctx).WithPrefix("http.lfs") + + if !isLfs(r) { + logger.Errorf("invalid content type: %s", r.Header.Get("Content-Type")) renderNotAcceptable(w) return } var batchRequest lfs.BatchRequest - ctx := r.Context() - logger := log.FromContext(ctx).WithPrefix("http.lfs") - defer r.Body.Close() // nolint: errcheck if err := json.NewDecoder(r.Body).Decode(&batchRequest); err != nil { logger.Errorf("error decoding json: %s", err) @@ -64,10 +67,9 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { } } - be := backend.FromContext(ctx) name := pat.Param(r, "repo") - repo, err := be.Repository(ctx, name) - if err != nil { + repo := proto.RepositoryFromContext(ctx) + if repo == nil { renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{ Message: "repository not found", }) @@ -92,7 +94,7 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { switch batchRequest.Operation { case lfs.OperationDownload: for _, o := range batchRequest.Objects { - stat, err := strg.Stat(path.Join("objects", o.RelativePath())) + exist, err := strg.Exists(path.Join("objects", o.RelativePath())) if err != nil && !errors.Is(err, fs.ErrNotExist) { logger.Error("error getting object stat", "oid", o.Oid, "repo", name, "err", err) renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ @@ -110,7 +112,7 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { return } - if stat == nil { + if !exist { objects = append(objects, &lfs.ObjectResponse{ Pointer: o, Error: &lfs.ObjectError{ @@ -118,7 +120,7 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { Message: "object not found", }, }) - } else if stat.Size() != o.Size { + } else if obj.Size != o.Size { objects = append(objects, &lfs.ObjectResponse{ Pointer: o, Error: &lfs.ObjectError{ @@ -127,18 +129,25 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { }, }) } else if o.IsValid() { + download := &lfs.Link{ + Href: fmt.Sprintf("%s/%s", baseHref, o.Oid), + } + if auth := r.Header.Get("Authorization"); auth != "" { + download.Header = map[string]string{ + "Authorization": auth, + } + } + objects = append(objects, &lfs.ObjectResponse{ Pointer: o, Actions: map[string]*lfs.Link{ - lfs.ActionDownload: { - Href: fmt.Sprintf("%s/%s", baseHref, o.Oid), - }, + lfs.ActionDownload: download, }, }) // If the object doesn't exist in the database, create it - if stat != nil && obj.ID == 0 { - if err := datastore.CreateLFSObject(ctx, dbx, repo.ID(), o.Oid, stat.Size()); err != nil { + if exist && obj.ID == 0 { + if err := datastore.CreateLFSObject(ctx, dbx, repo.ID(), o.Oid, o.Size); err != nil { logger.Error("error creating object in datastore", "oid", o.Oid, "repo", name, "err", err) renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ Message: "internal server error", @@ -147,6 +156,7 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { } } } else { + logger.Error("invalid object", "oid", o.Oid, "repo", name) objects = append(objects, &lfs.ObjectResponse{ Pointer: o, Error: &lfs.ObjectError{ @@ -168,17 +178,31 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { }, }) } else { + upload := &lfs.Link{ + Href: fmt.Sprintf("%s/%s", baseHref, o.Oid), + Header: map[string]string{ + // NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file. + // This ensures that the client always uses the designated value for the header. + "Content-Type": "application/octet-stream", + }, + } + verify := &lfs.Link{ + Href: fmt.Sprintf("%s/verify", baseHref), + } + if auth := r.Header.Get("Authorization"); auth != "" { + upload.Header["Authorization"] = auth + verify.Header = map[string]string{ + "Authorization": auth, + } + } + objects = append(objects, &lfs.ObjectResponse{ Pointer: o, Actions: map[string]*lfs.Link{ - lfs.ActionUpload: { - Href: fmt.Sprintf("%s/%s", baseHref, o.Oid), - }, + lfs.ActionUpload: upload, // Verify uploaded objects // https://github.com/git-lfs/git-lfs/blob/main/docs/api/basic-transfers.md#verification - lfs.ActionVerify: { - Href: fmt.Sprintf("%s/verify", baseHref), - }, + lfs.ActionVerify: verify, }, }) } @@ -209,45 +233,47 @@ func serviceLfsBasic(w http.ResponseWriter, r *http.Request) { func serviceLfsBasicDownload(w http.ResponseWriter, r *http.Request) { ctx := r.Context() oid := pat.Param(r, "oid") + repo := proto.RepositoryFromContext(ctx) cfg := config.FromContext(ctx) logger := log.FromContext(ctx).WithPrefix("http.lfs-basic") + datastore := store.FromContext(ctx) + dbx := db.FromContext(ctx) strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) - obj, err := strg.Open(path.Join("objects", oid)) - if err != nil { - logger.Error("error opening object", "oid", oid, "err", err) - renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{ - Message: "object not found", + obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), oid) + if err != nil && !errors.Is(err, db.ErrRecordNotFound) { + logger.Error("error getting object from database", "oid", oid, "repo", repo.Name(), "err", err) + renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ + Message: "internal server error", }) return } - stat, err := obj.Stat() + pointer := lfs.Pointer{Oid: oid} + f, err := strg.Open(path.Join("objects", pointer.RelativePath())) if err != nil { - logger.Error("error getting object stat", "oid", oid, "err", err) - renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ - Message: "internal server error", + logger.Error("error opening object", "oid", oid, "err", err) + renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{ + Message: "object not found", }) return } - defer obj.Close() // nolint: errcheck - if _, err := io.Copy(w, obj); err != nil { + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Length", strconv.FormatInt(obj.Size, 10)) + defer f.Close() // nolint: errcheck + if _, err := io.Copy(w, f); err != nil { logger.Error("error copying object to response", "oid", oid, "err", err) renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ Message: "internal server error", }) return } - - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10)) - renderStatus(http.StatusOK)(w, nil) } // PUT: /.git/info/lfs/objects/basic/ func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Content-Type") != "application/octet-stream" { + if !isBinary(r) { renderJSON(w, http.StatusUnsupportedMediaType, lfs.ErrorResponse{ Message: "invalid content type", }) @@ -288,7 +314,8 @@ func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) { return } - if err := strg.Put(path.Join("objects", oid), r.Body); err != nil { + pointer := lfs.Pointer{Oid: oid} + if _, err := strg.Put(path.Join("objects", pointer.RelativePath()), r.Body); err != nil { logger.Error("error writing object", "oid", oid, "err", err) renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ Message: "internal server error", @@ -318,13 +345,17 @@ func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) { // POST: /.git/info/lfs/objects/basic/verify func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) { + if !isLfs(r) { + renderNotAcceptable(w) + return + } + var pointer lfs.Pointer ctx := r.Context() logger := log.FromContext(ctx).WithPrefix("http.lfs-basic") - be := backend.FromContext(ctx) - name := pat.Param(r, "repo") - repo, err := be.Repository(ctx, name) - if err != nil { + repo := proto.RepositoryFromContext(ctx) + if repo == nil { + logger.Error("error getting repository from context") renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{ Message: "repository not found", }) @@ -344,25 +375,29 @@ func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) { dbx := db.FromContext(ctx) datastore := store.FromContext(ctx) strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) - if stat, err := strg.Stat(path.Join("objects", pointer.Oid)); err == nil { + if stat, err := strg.Stat(path.Join("objects", pointer.RelativePath())); err == nil { // Verify object is in the database. - if _, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), pointer.Oid); err != nil { + obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), pointer.Oid) + if err != nil { if errors.Is(err, db.ErrRecordNotFound) { - // Create missing object. - if err := datastore.CreateLFSObject(ctx, dbx, repo.ID(), pointer.Oid, stat.Size()); err != nil { - logger.Error("error creating object", "oid", pointer.Oid, "err", err) - renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ - Message: "internal server error", - }) - return - } - } else { - logger.Error("error getting object", "oid", pointer.Oid, "err", err) - renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ - Message: "internal server error", + logger.Error("object not found", "oid", pointer.Oid) + renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{ + Message: "object not found", }) return } + logger.Error("error getting object", "oid", pointer.Oid, "err", err) + renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{ + Message: "internal server error", + }) + return + } + + if obj.Size != pointer.Size { + renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{ + Message: "object size mismatch", + }) + return } if pointer.IsValid() && stat.Size() == pointer.Size { @@ -370,6 +405,7 @@ func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) { return } } else if errors.Is(err, fs.ErrNotExist) { + logger.Error("file not found", "oid", pointer.Oid) renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{ Message: "object not found", }) @@ -385,7 +421,7 @@ func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) { // POST: /.git/info/lfs/objects/locks func serviceLfsLocksCreate(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Content-Type") != lfs.MediaType { + if !isLfs(r) { renderNotAcceptable(w) return } @@ -396,8 +432,8 @@ func serviceLfsLocksCreate(w http.ResponseWriter, r *http.Request) { // renderJSON renders a JSON response with the given status code and value. It // also sets the Content-Type header to the JSON LFS media type (application/vnd.git-lfs+json). func renderJSON(w http.ResponseWriter, statusCode int, v interface{}) { - w.Header().Set("Content-Type", lfs.MediaType) - renderStatus(statusCode)(w, nil) + hdrLfs(w) + w.WriteHeader(statusCode) if err := json.NewEncoder(w).Encode(v); err != nil { log.Error("error encoding json", "err", err) } @@ -407,6 +443,17 @@ func renderNotAcceptable(w http.ResponseWriter) { renderStatus(http.StatusNotAcceptable)(w, nil) } +func isLfs(r *http.Request) bool { + contentType := r.Header.Get("Content-Type") + accept := r.Header.Get("Accept") + return strings.HasPrefix(contentType, lfs.MediaType) && strings.HasPrefix(accept, lfs.MediaType) +} + +func isBinary(r *http.Request) bool { + contentType := r.Header.Get("Content-Type") + return strings.HasPrefix(contentType, "application/octet-stream") +} + func hdrLfs(w http.ResponseWriter) { w.Header().Set("Content-Type", lfs.MediaType) w.Header().Set("Accept", lfs.MediaType) diff --git a/server/web/http.go b/server/web/http.go index 538e9fa37..e4588c45d 100644 --- a/server/web/http.go +++ b/server/web/http.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/server/config" ) @@ -18,6 +19,7 @@ type HTTPServer struct { // NewHTTPServer creates a new HTTP server. func NewHTTPServer(ctx context.Context) (*HTTPServer, error) { cfg := config.FromContext(ctx) + logger := log.FromContext(ctx).WithPrefix("http") s := &HTTPServer{ ctx: ctx, cfg: cfg, @@ -28,6 +30,7 @@ func NewHTTPServer(ctx context.Context) (*HTTPServer, error) { ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, MaxHeaderBytes: http.DefaultMaxHeaderBytes, + ErrorLog: logger.StandardLog(log.StandardLogOptions{ForceLevel: log.ErrorLevel}), }, }