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.

Identify accounts by their registered name

62576fa8157d431dcf2d360f3ebe481de77bda03 · Keenan Nemetz · 14d ago · parent 642e5994

Identify accounts by their registered name

Drop the internal account id and display name. Users, keys, passkeys,
invites, sessions, ACL entries, approvals, locks and CI records now all
use the registered name as the single identity.

The roster migrates in place: old id-keyed tables are rebuilt around the
name, and access-rule/lock references are rewritten through the same
mapping. Display-name editing and whoami --id are removed.

server/ssh/server.go +18−21

@@ -54,7 +54,7 @@ type Config struct {
54 54 LFS *lfs.Store
55 55 // LFSWarnBytes is handed to pre-receive as the threshold for suggesting LFS.
56 56 LFSWarnBytes int64
57 // Locks is the Git LFS lock table, keyed by account id. Nil means this server
57 // Locks is the Git LFS lock table, keyed by account name. Nil means this server
58 58 // offers no locking, and `git-lfs-transfer` does not advertise the capability.
59 59 Locks *lfs.LockStore
60 60 // Lease is how long a runner may hold a job without a heartbeat.
@@ -92,14 +92,13 @@ func New(cfg Config) (*Server, error) {
92 92 MaxAuthTries: 6,
93 93 PublicKeyCallback: func(c xssh.ConnMetadata, key xssh.PublicKey) (*xssh.Permissions, error) {
94 94 fp := xssh.FingerprintSHA256(key)
95 accountID, name, ok := cfg.Identity.Lookup(fp)
95 name, ok := cfg.Identity.Lookup(fp)
96 96 if !ok {
97 97 cfg.Log.Warn("ssh: unknown key", "fingerprint", fp, "remote", c.RemoteAddr().String())
98 98 return nil, fmt.Errorf("key not registered")
99 99 }
100 100 return &xssh.Permissions{Extensions: map[string]string{
101 101 "user": name,
102 "account": accountID,
103 102 "fingerprint": fp,
104 103 }}, nil
105 104 },
@@ -151,10 +150,9 @@ func (s *Server) handleConn(c net.Conn) {
151 150 defer sconn.Close()
152 151 go xssh.DiscardRequests(reqs)
153 152
153 // The account name is the authorization identity, resolved from the key at
154 // the handshake and used unchanged everywhere downstream.
154 155 user := sconn.Permissions.Extensions["user"]
155 // The account id is the authorization identity. The name is only ever used
156 // for messages, so renaming somebody cannot change what they may do.
157 accountID := sconn.Permissions.Extensions["account"]
158 156 fp := sconn.Permissions.Extensions["fingerprint"]
159 157 s.cfg.Log.Info("ssh connect", "user", user, "remote", sconn.RemoteAddr().String())
160 158
@@ -167,18 +165,18 @@ func (s *Server) handleConn(c net.Conn) {
167 165 if err != nil {
168 166 continue
169 167 }
170 go s.handleSession(ch, chReqs, user, accountID, fp, sconn.RemoteAddr().String())
168 go s.handleSession(ch, chReqs, user, fp, sconn.RemoteAddr().String())
171 169 }
172 170 }
173 171
174 func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user, accountID, fp, remote string) {
172 func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user, fp, remote string) {
175 173 defer ch.Close()
176 174 for req := range reqs {
177 175 switch req.Type {
178 176 case "exec":
179 177 var payload struct{ Command string }
180 178 xssh.Unmarshal(req.Payload, &payload)
181 s.exec(ch, req, payload.Command, user, accountID, fp, remote)
179 s.exec(ch, req, payload.Command, user, fp, remote)
182 180 return
183 181 case "shell", "pty-req", "subsystem":
184 182 req.Reply(false, nil)
@@ -194,7 +192,7 @@ func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user,
194 192 }
195 193 }
196 194
197 func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, accountID, fp, remote string) {
195 func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, fp, remote string) {
198 196 // A refusal is still answered on the channel rather than at the request
199 197 // layer: replying false makes the client print "exec request failed on
200 198 // channel 0" and the reason is lost. Nothing is ever executed on these
@@ -215,7 +213,7 @@ func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, account
215 213 // reports results over this channel.
216 214 if len(argv) == 1 && argv[0] == "vole-runner" {
217 215 req.Reply(true, nil)
218 s.serveRunner(ch, user, accountID)
216 s.serveRunner(ch, user)
219 217 exit(ch, 0)
220 218 return
221 219 }
@@ -223,25 +221,25 @@ func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, account
223 221 // accounts marked as instance admins.
224 222 if len(argv) >= 1 && argv[0] == "vole-admin" {
225 223 req.Reply(true, nil)
226 exit(ch, s.serveAdmin(ch, argv[1:], user, accountID, remote))
224 exit(ch, s.serveAdmin(ch, argv[1:], user, remote))
227 225 return
228 226 }
229 227 // git-lfs speaks its own protocol over an SSH remote: the client runs
230 228 // `git-lfs-transfer <repo> upload|download` on the same connection family as
231 229 // the push. No token changes hands; the key is the identity.
232 230 if len(argv) == 3 && argv[0] == "git-lfs-transfer" {
233 s.serveLFS(ch, req, argv[1], argv[2], user, accountID)
231 s.serveLFS(ch, req, argv[1], argv[2], user)
234 232 return
235 233 }
236 234 // The client cannot see the server's database, so the roster is served
237 235 // read-only over the same key-authenticated channel it already uses for git:
238 // one tab-separated id/name line per active account. `vole change` uses it to
239 // turn a typed name into the account id that ACLs and approvals store, and it
240 // is the whole surface — nothing here is writable.
236 // one account name per line. `vole change` uses it to check a typed name
237 // against the active roster, and it is the whole surface — nothing here is
238 // writable.
241 239 if len(argv) == 1 && argv[0] == "vole-roster" {
242 240 req.Reply(true, nil)
243 for _, a := range s.cfg.Identity.Roster() {
244 fmt.Fprintf(ch, "%s %s\n", a.ID, a.Name)
241 for _, name := range s.cfg.Identity.Roster() {
242 fmt.Fprintln(ch, name)
245 243 }
246 244 s.cfg.Log.Info("ssh roster", "user", user, "accounts", s.cfg.Identity.Snapshot().Count)
247 245 exit(ch, 0)
@@ -250,12 +248,12 @@ func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, account
250 248 // `vole whoami` asks the instance which account the presented key is. That
251 249 // mapping exists only here: a client's git config holds a name somebody typed,
252 250 // and guessing from it is how an issue ends up filed by an account that does
253 // not exist. The third field is where a browser reads this instance, empty
251 // not exist. The second field is where a browser reads this instance, empty
254 252 // when the operator has not set public_url — the caller that asks "who am I"
255 253 // is usually the same one that wants to print a link to what it just wrote.
256 254 if len(argv) == 1 && argv[0] == "vole-whoami" {
257 255 req.Reply(true, nil)
258 fmt.Fprintf(ch, "%s %s %s\n", accountID, s.cfg.Identity.Name(accountID), s.cfg.PublicURL)
256 fmt.Fprintf(ch, "%s\t%s\n", user, s.cfg.PublicURL)
259 257 exit(ch, 0)
260 258 return
261 259 }
@@ -289,7 +287,6 @@ func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, account
289 287 cmd.Env = append(os.Environ(),
290 288 "GIT_DIR="+m.Dir,
291 289 "VOLE_ACTOR="+user,
292 "VOLE_ACTOR_ID="+accountID,
293 290 "VOLE_KEY_ID="+fp,
294 291 "VOLE_REPO="+name,
295 292 "VOLE_MAX_BLOB="+strconv.FormatInt(s.cfg.MaxBlobBytes, 10),