internal/sshd/lfs.go
80 lines · 2682 bytes
1package sshd
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "time"
8
9 "gitbay.org/gitbay/internal/config"
10 "gitbay.org/gitbay/internal/lfs"
11 "gitbay.org/gitbay/internal/policy"
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14)
15
16// runLFSAuthenticate answers the git-lfs client's SSH probe:
17//
18// git-lfs-authenticate <path> download|upload
19//
20// with the HTTP endpoint and a short-lived repo- and operation-scoped
21// token. Access rules mirror the git transports: download needs read,
22// upload needs write; deploy keys authorize by their binding alone, and
23// every denial on an invisible repo reads as nonexistence. The token
24// names the key, so it stops working when the key does (#285).
25func runLFSAuthenticate(cfg config.Config, st *store.Store, user store.User, key store.SSHKey,
26 argv []string, stdout, stderr io.Writer) int {
27 scope := key.Scope
28 if len(argv) != 3 || (argv[2] != "download" && argv[2] != "upload") {
29 fmt.Fprintln(stderr, "usage: git-lfs-authenticate <path> download|upload")
30 return protocol.ExitUsage
31 }
32 op := argv[2]
33 write := op == "upload"
34 repo, err := st.RepoByPath(argv[1])
35 if err != nil {
36 fmt.Fprintln(stderr, "repository not found")
37 return protocol.ExitNotFound
38 }
39 if policy.IsDeployScope(scope) {
40 if !policy.DeployScopeAllows(scope, repo.ID, write) {
41 fmt.Fprintln(stderr, "repository not found")
42 return protocol.ExitNotFound
43 }
44 } else {
45 grant, err := st.AccessRole(repo.ID, user.ID)
46 if err != nil {
47 fmt.Fprintln(stderr, "internal error")
48 return protocol.ExitFailure
49 }
50 if !policy.CanRead(user, repo, grant) {
51 fmt.Fprintln(stderr, "repository not found")
52 return protocol.ExitNotFound
53 }
54 if !policy.ScopeAllowsGit(scope, repo.Path(), write) {
55 fmt.Fprintf(stderr, "this key's scope (%s) does not allow lfs %s on %s\n", scope, op, repo.Path())
56 return protocol.ExitDenied
57 }
58 if write && !policy.CanWrite(user, repo, grant) {
59 fmt.Fprintf(stderr, "write access to %s denied\n", repo.Path())
60 return protocol.ExitDenied
61 }
62 }
63 if write && repo.Settings.Archived {
64 fmt.Fprintf(stderr, "%s is archived and read-only\n", repo.Path())
65 return protocol.ExitDenied
66 }
67 secret, err := st.LFSSecret(lfs.NewSecret)
68 if err != nil {
69 fmt.Fprintln(stderr, "internal error")
70 return protocol.ExitFailure
71 }
72 token := lfs.Sign([]byte(secret), repo.ID, key.ID, key.Fingerprint, op, time.Now())
73 json.NewEncoder(stdout).Encode(map[string]any{
74 "href": fmt.Sprintf("%s/%s/%s.git/info/lfs",
75 cfg.Server.SiteURL, repo.OwnerName, repo.Name),
76 "header": map[string]string{"Authorization": "Bearer " + token},
77 "expires_in": int(lfs.TokenTTL.Seconds()),
78 })
79 return protocol.ExitOK
80}