diff options
Diffstat (limited to 'cmd/git-lfs-authenticate')
-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 | ||