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), | |