vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Add browser auth, contributor access, and gated CI
6c84d387e5f608360e30484a1a5b1cff41076850 · Keenan Nemetz · 16d ago · parent c36d5073
Add browser auth, contributor access, and gated CI Browser sign-in now uses passkeys. Enrollment links are one-time, sessions are hashed in users.db, and issue/comment/approval writes use the session account. HTTPS public_url is required for WebAuthn. Add the contributor role between read and maintain. Contributors can discuss and open change requests, and may push only refs/heads/contrib/<account-id>/. Contributor CI is held per commit until a maintainer allows the exact tip from the repository page. Rename the write role to maintain. write remains accepted as the historical spelling, and access.Write remains as a Go alias so existing callers and ACL rows keep working. Update the web, CLI, queue schema, and guides for the new flows, and cover the role, namespace, auth, and CI-gating paths in tests.
server/ssh/lfs.go +5−5
| @@ -12,7 +12,7 @@ import ( | |||
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | 14 | // serveLFS runs a git-lfs transfer session: `ssh host git-lfs-transfer <repo> | |
| 15 | // upload|download`. Uploads require write access to the repository, downloads | ||
| 15 | // upload|download`. Uploads require maintain access to the repository, downloads | ||
| 16 | 16 | // the same read rule the web and clone use — but resolved against the account | |
| 17 | 17 | // that owns the key, so a private repository's members can pull objects over | |
| 18 | 18 | // SSH while anonymous readers cannot. | |
| @@ -49,10 +49,10 @@ func (s *Server) serveLFS(ch xssh.Channel, req *xssh.Request, pathArg, op, user, | |||
| 49 | 49 | } | |
| 50 | 50 | role := a.RoleFor(accountID) | |
| 51 | 51 | canRead := a.Can(accountID, access.Read) | |
| 52 | canWrite := role.AtLeast(access.Write) | ||
| 53 | if op == "upload" && !canWrite { | ||
| 52 | canMaintain := role.AtLeast(access.Maintain) | ||
| 53 | if op == "upload" && !canMaintain { | ||
| 54 | 54 | s.cfg.Log.Warn("ssh: refused lfs upload", "user", user, "repo", name, "role", role) | |
| 55 | refuse("%s does not have write access to %s (role: %s)", user, name, role) | ||
| 55 | refuse("%s does not have maintain access to %s (role: %s)", user, name, role) | ||
| 56 | 56 | return | |
| 57 | 57 | } | |
| 58 | 58 | if op == "download" && !canRead { | |
| @@ -72,7 +72,7 @@ func (s *Server) serveLFS(ch xssh.Channel, req *xssh.Request, pathArg, op, user, | |||
| 72 | 72 | RepoID: m.ID, | |
| 73 | 73 | AccountID: accountID, | |
| 74 | 74 | CanRead: canRead, | |
| 75 | CanWrite: canWrite, | ||
| 75 | CanWrite: canMaintain, | ||
| 76 | 76 | // Releasing somebody else's lock is an admin act, the same rule a force-push | |
| 77 | 77 | // follows: the alternative is a lock nobody can clear once its holder leaves. | |
| 78 | 78 | IsAdmin: role.AtLeast(access.Admin), | |