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.
Add remote administration and local workflow runs
3abbe749096f4d7ff76f4d468e4427829b8a6f95 · Keenan Nemetz · 16d ago · parent 520470e1
Add remote administration and local workflow runs Make `vole admin` the single namespace for user, repository, and access management, runnable locally or over SSH by instance admins. Instance-admin grants live in users.db, and the audit log names the actor separately from the account being changed. Add `vole workflow run` to execute workflow jobs in the working tree without a server or runner. Positional arguments select workflows, --job selects jobs, and the command reuses the push-time validator. Update the README and guides for the new command shapes.
server/ssh/server.go +14−5
| @@ -28,6 +28,7 @@ import ( | |||
| 28 | 28 | ||
| 29 | 29 | "github.com/rodent-software/vole/ci/runs" | |
| 30 | 30 | "github.com/rodent-software/vole/core/lfs" | |
| 31 | "github.com/rodent-software/vole/forge" | ||
| 31 | 32 | "github.com/rodent-software/vole/forge/identity" | |
| 32 | 33 | "github.com/rodent-software/vole/forge/repo" | |
| 33 | 34 | ) | |
| @@ -37,6 +38,7 @@ const serverVersion = "SSH-2.0-vole" | |||
| 37 | 38 | type Config struct { | |
| 38 | 39 | Identity *identity.Store | |
| 39 | 40 | Repos *repo.Store | |
| 41 | Instance *forge.Instance | ||
| 40 | 42 | Log *slog.Logger | |
| 41 | 43 | HostKeyPath string | |
| 42 | 44 | MaxBlobBytes int64 | |
| @@ -165,18 +167,18 @@ func (s *Server) handleConn(c net.Conn) { | |||
| 165 | 167 | if err != nil { | |
| 166 | 168 | continue | |
| 167 | 169 | } | |
| 168 | go s.handleSession(ch, chReqs, user, accountID, fp) | ||
| 170 | go s.handleSession(ch, chReqs, user, accountID, fp, sconn.RemoteAddr().String()) | ||
| 169 | 171 | } | |
| 170 | 172 | } | |
| 171 | 173 | ||
| 172 | func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user, accountID, fp string) { | ||
| 174 | func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user, accountID, fp, remote string) { | ||
| 173 | 175 | defer ch.Close() | |
| 174 | 176 | for req := range reqs { | |
| 175 | 177 | switch req.Type { | |
| 176 | 178 | case "exec": | |
| 177 | 179 | var payload struct{ Command string } | |
| 178 | 180 | xssh.Unmarshal(req.Payload, &payload) | |
| 179 | s.exec(ch, req, payload.Command, user, accountID, fp) | ||
| 181 | s.exec(ch, req, payload.Command, user, accountID, fp, remote) | ||
| 180 | 182 | return | |
| 181 | 183 | case "shell", "pty-req", "subsystem": | |
| 182 | 184 | req.Reply(false, nil) | |
| @@ -192,7 +194,7 @@ func (s *Server) handleSession(ch xssh.Channel, reqs <-chan *xssh.Request, user, | |||
| 192 | 194 | } | |
| 193 | 195 | } | |
| 194 | 196 | ||
| 195 | func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, accountID, fp string) { | ||
| 197 | func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, accountID, fp, remote string) { | ||
| 196 | 198 | // A refusal is still answered on the channel rather than at the request | |
| 197 | 199 | // layer: replying false makes the client print "exec request failed on | |
| 198 | 200 | // channel 0" and the reason is lost. Nothing is ever executed on these | |
| @@ -217,6 +219,13 @@ func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, account | |||
| 217 | 219 | exit(ch, 0) | |
| 218 | 220 | return | |
| 219 | 221 | } | |
| 222 | // The admin API is a fixed command set, not a shell. It is available only to | ||
| 223 | // accounts marked as instance admins. | ||
| 224 | if len(argv) >= 1 && argv[0] == "vole-admin" { | ||
| 225 | req.Reply(true, nil) | ||
| 226 | exit(ch, s.serveAdmin(ch, argv[1:], user, accountID, remote)) | ||
| 227 | return | ||
| 228 | } | ||
| 220 | 229 | // git-lfs speaks its own protocol over an SSH remote: the client runs | |
| 221 | 230 | // `git-lfs-transfer <repo> upload|download` on the same connection family as | |
| 222 | 231 | // the push. No token changes hands; the key is the identity. | |
| @@ -251,7 +260,7 @@ func (s *Server) exec(ch xssh.Channel, req *xssh.Request, command, user, account | |||
| 251 | 260 | return | |
| 252 | 261 | } | |
| 253 | 262 | if len(argv) != 2 { | |
| 254 | refuse("only git-upload-pack, git-receive-pack, vole-roster, vole-whoami and vole-runner are available") | ||
| 263 | refuse("only git-upload-pack, git-receive-pack, vole-admin, vole-roster, vole-whoami and vole-runner are available") | ||
| 255 | 264 | return | |
| 256 | 265 | } | |
| 257 | 266 | verb := argv[0] | |