aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/git-lfs-authenticate/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/git-lfs-authenticate/main.go')
-rw-r--r--cmd/git-lfs-authenticate/main.go155
1 files changed, 123 insertions, 32 deletions
diff --git a/cmd/git-lfs-authenticate/main.go b/cmd/git-lfs-authenticate/main.go
index 41e2dbc..56b9d78 100644
--- a/cmd/git-lfs-authenticate/main.go
+++ b/cmd/git-lfs-authenticate/main.go
@@ -1,8 +1,18 @@
1package main 1package main
2 2
3import ( 3import (
4 "bytes"
5 "crypto/ed25519"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
4 "fmt" 9 "fmt"
5 "os" 10 "os"
11 "os/exec"
12 "strings"
13 "time"
14
15 "github.com/golang-jwt/jwt/v5"
6) 16)
7 17
8func die(msg string, args ...any) { 18func die(msg string, args ...any) {
@@ -12,37 +22,118 @@ func die(msg string, args ...any) {
12 os.Exit(1) 22 os.Exit(1)
13} 23}
14 24
25func getGitoliteAccess(path, user, gitolitePerm string) bool {
26 // gitolite access -q: returns only exit code
27 cmd := exec.Command("gitolite", "access", "-q", path, user, gitolitePerm)
28 err := cmd.Run()
29 permGranted := err == nil
30 var exitErr *exec.ExitError
31 if err != nil && !errors.As(err, &exitErr) {
32 die("failed to query access information")
33 }
34 return permGranted
35}
36
37type gitolfs3Claims struct {
38 Repository string `json:"repository"`
39 Permission string `json:"permission"`
40}
41
42type customClaims struct {
43 Gitolfs3 gitolfs3Claims `json:"gitolfs3"`
44 *jwt.RegisteredClaims
45}
46
47type authenticateResponse struct {
48 Header map[string]string `json:"header"`
49 // In seconds.
50 ExpiresIn int64 `json:"expires_in,omitempty"`
51 // expires_at (RFC3339) could also be used, but we leave it out since we
52 // don't use it.
53}
54
55func wipe(b []byte) {
56 for i := range b {
57 b[i] = 0
58 }
59}
60
15func main() { 61func main() {
16 // if len(os.Args) != 3 { 62 // Even though not explicitly described in the Git LFS documentation, the
17 // die("expected 2 arguments [path, operation], got %d", len(os.Args)-1) 63 // git-lfs-authenticate command is expected to either exit succesfully with
18 // } 64 // exit code 0 and to then print credentials in the prescribed JSON format
19 // 65 // to standard out. On errors, the command should exit with a non-zero exit
20 // path := strings.TrimPrefix(strings.TrimSuffix(os.Args[1], ".git"), "/") 66 // code and print the error message in plain text to standard error. See
21 // operation := os.Args[2] 67 // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/lfshttp/ssh.go#L76-L117
22 // 68
23 // if operation != "download" && operation != "upload" { 69 if len(os.Args) != 3 {
24 // die("expected operation to be in {upload, download}, got %s", operation) 70 die("expected 2 arguments (path, operation), got %d", len(os.Args)-1)
25 // } 71 }
26 // 72
27 // user := os.Getenv("GL_USER") 73 path := strings.TrimPrefix(strings.TrimSuffix(os.Args[1], ".git"), "/")
28 // 74 operation := os.Args[2]
29 // if user == "" { 75 if operation != "download" && operation != "upload" {
30 // die("expected Gitolite user env (GL_USER) to be set") 76 die("expected operation to be upload or download, got %s", operation)
31 // } 77 }
32 // 78
33 // gitolitePerm := "R" 79 user := os.Getenv("GL_USER")
34 // 80 if user == "" {
35 // if operation == "upload" { 81 die("internal error")
36 // gitolitePerm = "W" 82 }
37 // } 83 keyPath := os.Getenv("GITOLFS3_KEY_PATH")
38 // 84 if keyPath == "" {
39 // // gitolite access -q: returns only exit code 85 die("internal error")
40 // cmd := exec.Command("gitolite", "access", "-q", path, user, gitolitePerm) 86 }
41 // err := cmd.Run() 87 keyStr, err := os.ReadFile(keyPath)
42 // permGranted := err == nil 88 if err != nil {
43 // var exitErr *exec.ExitError 89 die("internal error")
44 // 90 }
45 // if err != nil && !errors.As(err, &exitErr) { 91 keyStr = bytes.TrimSpace(keyStr)
46 // die("failed to query Gitolite access information") 92 defer wipe(keyStr)
47 // } 93
94 if hex.DecodedLen(len(keyStr)) != ed25519.SeedSize {
95 die("internal error")
96 }
97
98 seed := make([]byte, ed25519.SeedSize)
99 defer wipe(seed)
100 if _, err = hex.Decode(seed, keyStr); err != nil {
101 die("internal error")
102 }
103 privateKey := ed25519.NewKeyFromSeed(seed)
104
105 if !getGitoliteAccess(path, user, "R") {
106 die("repository not found")
107 }
108 if operation == "upload" && !getGitoliteAccess(path, user, "W") {
109 // User has read access but not write access
110 die("forbidden")
111 }
112
113 expiresIn := time.Hour * 24
114 claims := customClaims{
115 Gitolfs3: gitolfs3Claims{
116 Repository: path,
117 Permission: operation,
118 },
119 RegisteredClaims: &jwt.RegisteredClaims{
120 Subject: user,
121 IssuedAt: jwt.NewNumericDate(time.Now()),
122 ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
123 },
124 }
125
126 token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
127 ss, err := token.SignedString(privateKey)
128 if err != nil {
129 die("failed to generate token")
130 }
131
132 response := authenticateResponse{
133 Header: map[string]string{
134 "Authorization": "Bearer " + ss,
135 },
136 ExpiresIn: int64(expiresIn.Seconds()),
137 }
138 json.NewEncoder(os.Stdout).Encode(response)
48} 139}