Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/ssh/lfs.go

Line attribution on main · 114 line(s)

c5902451
Keenan Nemetz
16d ago
1 package ssh
64ac5071
Keenan Nemetz
17d ago
2
3 import (
4 "fmt"
5 "strconv"
6 "time"
7
c5902451
Keenan Nemetz
16d ago
8 xssh "golang.org/x/crypto/ssh"
64ac5071
Keenan Nemetz
17d ago
9
d37d301f
Keenan Nemetz
15h ago
10 "rodent.software/vole/core/lfs"
11 "rodent.software/vole/forge/access"
64ac5071
Keenan Nemetz
17d ago
12 )
13
14 // serveLFS runs a git-lfs transfer session: `ssh host git-lfs-transfer <repo>
6c84d387
Keenan Nemetz
15d ago
15 // upload|download`. Uploads require maintain access to the repository, downloads
64ac5071
Keenan Nemetz
17d ago
16 // the same read rule the web and clone use — but resolved against the account
17 // that owns the key, so a private repository's members can pull objects over
18 // SSH while anonymous readers cannot.
62576fa8
Keenan Nemetz
14d ago
19 func (s *Server) serveLFS(ch xssh.Channel, req *xssh.Request, pathArg, op, user string) {
64ac5071
Keenan Nemetz
17d ago
20 refuse := func(format string, args ...any) {
21 req.Reply(true, nil)
22 fmt.Fprintf(ch, "vole: "+format+"\n", args...)
23 ch.CloseWrite()
24 ch.Close()
25 }
26
27 if s.cfg.LFS == nil {
28 refuse("this instance has no LFS store")
29 return
30 }
31 if op != "upload" && op != "download" {
32 refuse("%q is not an LFS operation; expected upload or download", op)
33 return
34 }
35 name, ok := repoName(pathArg)
36 if !ok {
37 refuse("invalid repository path")
38 return
39 }
40 m, err := s.cfg.Repos.Open(name)
41 if err != nil {
42 refuse("repository %q not found", name)
43 return
44 }
45 a, err := m.ACL()
46 if err != nil {
47 refuse("cannot read access rules for %s", name)
48 return
49 }
381d6aac
Keenan Nemetz
14d ago
50 role := a.EffectiveRole(user, s.cfg.Identity.IsAdmin(user))
51 canRead := role.AtLeast(access.Read)
6c84d387
Keenan Nemetz
15d ago
52 canMaintain := role.AtLeast(access.Maintain)
53 if op == "upload" && !canMaintain {
64ac5071
Keenan Nemetz
17d ago
54 s.cfg.Log.Warn("ssh: refused lfs upload", "user", user, "repo", name, "role", role)
6c84d387
Keenan Nemetz
15d ago
55 refuse("%s does not have maintain access to %s (role: %s)", user, name, role)
64ac5071
Keenan Nemetz
17d ago
56 return
57 }
58 if op == "download" && !canRead {
59 s.cfg.Log.Warn("ssh: refused lfs download", "user", user, "repo", name)
60 // Same shape as clone: do not confirm that the repository exists.
61 refuse("repository %q not found", name)
62 return
63 }
64
65 if err := req.Reply(true, nil); err != nil {
66 return
67 }
68 start := time.Now()
69 t := &lfs.Transfer{
62576fa8
Keenan Nemetz
14d ago
70 Store: s.cfg.LFS,
71 Repo: name,
72 RepoID: m.ID,
73 User: user,
74 CanRead: canRead,
75 CanWrite: canMaintain,
bf1e027c
Keenan Nemetz
16d ago
76 // Releasing somebody else's lock is an admin act, the same rule a force-push
77 // follows: the alternative is a lock nobody can clear once its holder leaves.
c5902451
Keenan Nemetz
16d ago
78 IsAdmin: role.AtLeast(access.Admin),
bf1e027c
Keenan Nemetz
16d ago
79 Locks: s.cfg.Locks,
9e8cc155
Keenan Nemetz
12d ago
80 // The same per-repository object bound pre-receive uses for metadata:
81 // artifacts and logs arrive through this transfer, so it is where their
82 // bytes can be refused rather than counted after the fact.
83 RepoMaxBytes: s.cfg.MaxLFSBytes,
84 Log: s.cfg.Log,
64ac5071
Keenan Nemetz
17d ago
85 }
86 serveErr := t.Serve(op, ch, ch)
87 code := 0
88 if serveErr != nil {
89 code = 1
90 fmt.Fprintf(ch.Stderr(), "vole: lfs %s: %v\n", op, serveErr)
91 }
92 s.cfg.Log.Info("ssh lfs", "user", user, "op", op, "repo", name,
93 "exit", code, "duration", time.Since(start).String())
94 exit(ch, code)
95 }
96
97 // lfsObjectSize is a small helper for messages and tests.
98 func lfsObjectSize(n int64) string { return strconv.FormatInt(n, 10) }
99
100 // lfsRoot and lfsMax keep the hook environment well-defined on an instance built
101 // without a store (tests, embedders): pre-receive then has nothing to check.
102 func lfsRoot(s *lfs.Store) string {
103 if s == nil {
104 return ""
105 }
106 return s.Root
107 }
108
109 func lfsMax(s *lfs.Store) int64 {
110 if s == nil {
111 return 0
112 }
113 return s.MaxBytes
114 }