diff options
| -rw-r--r-- | cmd/git-lfs-authenticate/main.go | 80 | 
1 files changed, 70 insertions, 10 deletions
diff --git a/cmd/git-lfs-authenticate/main.go b/cmd/git-lfs-authenticate/main.go index 56b9d78..e4fa67d 100644 --- a/cmd/git-lfs-authenticate/main.go +++ b/cmd/git-lfs-authenticate/main.go  | |||
| @@ -7,14 +7,57 @@ import ( | |||
| 7 | "encoding/json" | 7 | "encoding/json" | 
| 8 | "errors" | 8 | "errors" | 
| 9 | "fmt" | 9 | "fmt" | 
| 10 | "io" | ||
| 10 | "os" | 11 | "os" | 
| 11 | "os/exec" | 12 | "os/exec" | 
| 12 | "strings" | 13 | "strings" | 
| 14 | "sync" | ||
| 15 | "sync/atomic" | ||
| 13 | "time" | 16 | "time" | 
| 14 | 17 | ||
| 15 | "github.com/golang-jwt/jwt/v5" | 18 | "github.com/golang-jwt/jwt/v5" | 
| 19 | "github.com/rs/xid" | ||
| 16 | ) | 20 | ) | 
| 17 | 21 | ||
| 22 | type logger struct { | ||
| 23 | reqID string | ||
| 24 | time time.Time | ||
| 25 | m sync.Mutex | ||
| 26 | // Contained value must implement io.WriteCloser | ||
| 27 | wc atomic.Value | ||
| 28 | } | ||
| 29 | |||
| 30 | func newLogger(reqID string) *logger { | ||
| 31 | return &logger{reqID: reqID, time: time.Now()} | ||
| 32 | } | ||
| 33 | |||
| 34 | func (l *logger) writer() io.WriteCloser { | ||
| 35 | w := l.wc.Load() | ||
| 36 | if w == nil { | ||
| 37 | l.m.Lock() | ||
| 38 | if l.wc.Load() == nil { | ||
| 39 | os.MkdirAll(".gitolfs3/logs/", 0o600) // drw------- | ||
| 40 | path := fmt.Sprintf(".gitolfs3/logs/gitolfs3-%s-%s.log", l.time, l.reqID) | ||
| 41 | var err error | ||
| 42 | if w, err = os.Create(path); err == nil { | ||
| 43 | l.wc.Store(w) | ||
| 44 | } | ||
| 45 | } | ||
| 46 | l.m.Unlock() | ||
| 47 | } | ||
| 48 | return w.(io.WriteCloser) | ||
| 49 | } | ||
| 50 | |||
| 51 | func (l *logger) logf(msg string, args ...any) { | ||
| 52 | fmt.Fprintf(l.writer(), msg, args...) | ||
| 53 | } | ||
| 54 | |||
| 55 | func (l *logger) close() { | ||
| 56 | if wc := l.wc.Load(); wc != nil { | ||
| 57 | wc.(io.Closer).Close() | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 18 | func die(msg string, args ...any) { | 61 | func die(msg string, args ...any) { | 
| 19 | fmt.Fprint(os.Stderr, "Error: ") | 62 | fmt.Fprint(os.Stderr, "Error: ") | 
| 20 | fmt.Fprintf(os.Stderr, msg, args...) | 63 | fmt.Fprintf(os.Stderr, msg, args...) | 
| @@ -22,14 +65,22 @@ func die(msg string, args ...any) { | |||
| 22 | os.Exit(1) | 65 | os.Exit(1) | 
| 23 | } | 66 | } | 
| 24 | 67 | ||
| 25 | func getGitoliteAccess(path, user, gitolitePerm string) bool { | 68 | func dieReqID(reqID string, msg string, args ...any) { | 
| 69 | fmt.Fprint(os.Stderr, "Error: ") | ||
| 70 | fmt.Fprintf(os.Stderr, msg, args...) | ||
| 71 | fmt.Fprintf(os.Stderr, "(request ID: %s)\n", reqID) | ||
| 72 | os.Exit(1) | ||
| 73 | } | ||
| 74 | |||
| 75 | func getGitoliteAccess(logger *logger, reqID, path, user, gitolitePerm string) bool { | ||
| 26 | // gitolite access -q: returns only exit code | 76 | // gitolite access -q: returns only exit code | 
| 27 | cmd := exec.Command("gitolite", "access", "-q", path, user, gitolitePerm) | 77 | cmd := exec.Command("gitolite", "access", "-q", path, user, gitolitePerm) | 
| 28 | err := cmd.Run() | 78 | err := cmd.Run() | 
| 29 | permGranted := err == nil | 79 | permGranted := err == nil | 
| 30 | var exitErr *exec.ExitError | 80 | var exitErr *exec.ExitError | 
| 31 | if err != nil && !errors.As(err, &exitErr) { | 81 | if err != nil && !errors.As(err, &exitErr) { | 
| 32 | die("failed to query access information") | 82 | logger.logf("Failed to query access information (%s): %s", cmd, err) | 
| 83 | dieReqID(reqID, "failed to query access information") | ||
| 33 | } | 84 | } | 
| 34 | return permGranted | 85 | return permGranted | 
| 35 | } | 86 | } | 
| @@ -66,6 +117,9 @@ func main() { | |||
| 66 | // code and print the error message in plain text to standard error. See | 117 | // code and print the error message in plain text to standard error. See | 
| 67 | // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/lfshttp/ssh.go#L76-L117 | 118 | // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/lfshttp/ssh.go#L76-L117 | 
| 68 | 119 | ||
| 120 | reqID := xid.New().String() | ||
| 121 | logger := newLogger(reqID) | ||
| 122 | |||
| 69 | if len(os.Args) != 3 { | 123 | if len(os.Args) != 3 { | 
| 70 | die("expected 2 arguments (path, operation), got %d", len(os.Args)-1) | 124 | die("expected 2 arguments (path, operation), got %d", len(os.Args)-1) | 
| 71 | } | 125 | } | 
| @@ -78,35 +132,40 @@ func main() { | |||
| 78 | 132 | ||
| 79 | user := os.Getenv("GL_USER") | 133 | user := os.Getenv("GL_USER") | 
| 80 | if user == "" { | 134 | if user == "" { | 
| 81 | die("internal error") | 135 | logger.logf("Environment variable GL_USER is not set") | 
| 136 | dieReqID(reqID, "internal error") | ||
| 82 | } | 137 | } | 
| 83 | keyPath := os.Getenv("GITOLFS3_KEY_PATH") | 138 | keyPath := os.Getenv("GITOLFS3_KEY_PATH") | 
| 84 | if keyPath == "" { | 139 | if keyPath == "" { | 
| 85 | die("internal error") | 140 | logger.logf("Environment variable GITOLFS3_KEY_PATH is not set") | 
| 141 | dieReqID(reqID, "internal error") | ||
| 86 | } | 142 | } | 
| 87 | keyStr, err := os.ReadFile(keyPath) | 143 | keyStr, err := os.ReadFile(keyPath) | 
| 88 | if err != nil { | 144 | if err != nil { | 
| 89 | die("internal error") | 145 | logger.logf("Cannot read key in GITOLFS3_KEY_PATH: %s", err) | 
| 146 | dieReqID(reqID, "internal error") | ||
| 90 | } | 147 | } | 
| 91 | keyStr = bytes.TrimSpace(keyStr) | 148 | keyStr = bytes.TrimSpace(keyStr) | 
| 92 | defer wipe(keyStr) | 149 | defer wipe(keyStr) | 
| 93 | 150 | ||
| 94 | if hex.DecodedLen(len(keyStr)) != ed25519.SeedSize { | 151 | if hex.DecodedLen(len(keyStr)) != ed25519.SeedSize { | 
| 95 | die("internal error") | 152 | logger.logf("Fatal: provided private key (seed) is invalid: does not have expected length") | 
| 153 | dieReqID(reqID, "internal error") | ||
| 96 | } | 154 | } | 
| 97 | 155 | ||
| 98 | seed := make([]byte, ed25519.SeedSize) | 156 | seed := make([]byte, ed25519.SeedSize) | 
| 99 | defer wipe(seed) | 157 | defer wipe(seed) | 
| 100 | if _, err = hex.Decode(seed, keyStr); err != nil { | 158 | if _, err = hex.Decode(seed, keyStr); err != nil { | 
| 101 | die("internal error") | 159 | logger.logf("Fatal: cannot decode provided private key (seed): %s", err) | 
| 160 | dieReqID(reqID, "internal error") | ||
| 102 | } | 161 | } | 
| 103 | privateKey := ed25519.NewKeyFromSeed(seed) | 162 | privateKey := ed25519.NewKeyFromSeed(seed) | 
| 104 | 163 | ||
| 105 | if !getGitoliteAccess(path, user, "R") { | 164 | if !getGitoliteAccess(logger, reqID, path, user, "R") { | 
| 106 | die("repository not found") | 165 | die("repository not found") | 
| 107 | } | 166 | } | 
| 108 | if operation == "upload" && !getGitoliteAccess(path, user, "W") { | 167 | if operation == "upload" && !getGitoliteAccess(logger, reqID, path, user, "W") { | 
| 109 | // User has read access but not write access | 168 | // User has read access but no write access | 
| 110 | die("forbidden") | 169 | die("forbidden") | 
| 111 | } | 170 | } | 
| 112 | 171 | ||
| @@ -126,6 +185,7 @@ func main() { | |||
| 126 | token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) | 185 | token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) | 
| 127 | ss, err := token.SignedString(privateKey) | 186 | ss, err := token.SignedString(privateKey) | 
| 128 | if err != nil { | 187 | if err != nil { | 
| 188 | logger.logf("Fatal: failed to generate JWT: %s", err) | ||
| 129 | die("failed to generate token") | 189 | die("failed to generate token") | 
| 130 | } | 190 | } | 
| 131 | 191 | ||