internal/sshd/lfs.go
78 lines · 2559 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.
24func runLFSAuthenticate(cfg config.Config, st *store.Store, user store.User, scope string,
25 argv []string, stdout, stderr io.Writer) int {
26 if len(argv) != 3 || (argv[2] != "download" && argv[2] != "upload") {
27 fmt.Fprintln(stderr, "usage: git-lfs-authenticate <path> download|upload")
28 return protocol.ExitUsage
29 }
30 op := argv[2]
31 write := op == "upload"
32 repo, err := st.RepoByPath(argv[1])
33 if err != nil {
34 fmt.Fprintln(stderr, "repository not found")
35 return protocol.ExitNotFound
36 }
37 if policy.IsDeployScope(scope) {
38 if !policy.DeployScopeAllows(scope, repo.ID, write) {
39 fmt.Fprintln(stderr, "repository not found")
40 return protocol.ExitNotFound
41 }
42 } else {
43 grant, err := st.AccessRole(repo.ID, user.ID)
44 if err != nil {
45 fmt.Fprintln(stderr, "internal error")
46 return protocol.ExitFailure
47 }
48 if !policy.CanRead(user, repo, grant) {
49 fmt.Fprintln(stderr, "repository not found")
50 return protocol.ExitNotFound
51 }
52 if !policy.ScopeAllowsGit(scope, repo.Path(), write) {
53 fmt.Fprintf(stderr, "this key's scope (%s) does not allow lfs %s on %s\n", scope, op, repo.Path())
54 return protocol.ExitDenied
55 }
56 if write && !policy.CanWrite(user, repo, grant) {
57 fmt.Fprintf(stderr, "write access to %s denied\n", repo.Path())
58 return protocol.ExitDenied
59 }
60 }
61 if write && repo.Settings.Archived {
62 fmt.Fprintf(stderr, "%s is archived and read-only\n", repo.Path())
63 return protocol.ExitDenied
64 }
65 secret, err := st.LFSSecret(lfs.NewSecret)
66 if err != nil {
67 fmt.Fprintln(stderr, "internal error")
68 return protocol.ExitFailure
69 }
70 token := lfs.Sign([]byte(secret), repo.ID, op, time.Now())
71 json.NewEncoder(stdout).Encode(map[string]any{
72 "href": fmt.Sprintf("%s/%s/%s.git/info/lfs",
73 cfg.Server.SiteURL, repo.OwnerName, repo.Name),
74 "header": map[string]string{"Authorization": "Bearer " + token},
75 "expires_in": int(lfs.TokenTTL.Seconds()),
76 })
77 return protocol.ExitOK
78}