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}