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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 19m ago 4b8c9e1d
history blame raw 3.2 KiB
1package ssh
2
3import (
4 "fmt"
5 "strconv"
6 "time"
7
8 xssh "golang.org/x/crypto/ssh"
9
10 "rodent.software/vole/core/lfs"
11 "rodent.software/vole/forge/access"
12)
13
14// serveLFS runs a git-lfs transfer session: `ssh host git-lfs-transfer <repo>
15// upload|download`. Uploads require maintain access to the repository, downloads
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.
19func (s *Server) serveLFS(ch xssh.Channel, req *xssh.Request, pathArg, op, user string) {
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 }
50 role := a.EffectiveRole(user, s.cfg.Identity.IsAdmin(user))
51 canRead := role.AtLeast(access.Read)
52 canMaintain := role.AtLeast(access.Maintain)
53 if op == "upload" && !canMaintain {
54 s.cfg.Log.Warn("ssh: refused lfs upload", "user", user, "repo", name, "role", role)
55 refuse("%s does not have maintain access to %s (role: %s)", user, name, role)
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{
70 Store: s.cfg.LFS,
71 Repo: name,
72 RepoID: m.ID,
73 User: user,
74 CanRead: canRead,
75 CanWrite: canMaintain,
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.
78 IsAdmin: role.AtLeast(access.Admin),
79 Locks: s.cfg.Locks,
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,
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.
98func 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.
102func lfsRoot(s *lfs.Store) string {
103 if s == nil {
104 return ""
105 }
106 return s.Root
107}
108
109func lfsMax(s *lfs.Store) int64 {
110 if s == nil {
111 return 0
112 }
113 return s.MaxBytes
114}
115