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/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/web/ci.go +57

@@ -0,0 +1,57 @@
1 package web
2
3 import (
4 "net/http"
5 "strings"
6
7 "github.com/rodent-software/vole/forge/access"
8 )
9
10 // AllowContributorCI records a maintainer's decision that the current tip of one
11 // contributor branch may run. The approval is per commit: a new push waits again.
12 // The queue-side operation is supplied by the server (Web.AllowCI), because the
13 // web package does not own workflow discovery or the job queue.
14 func (w *Web) AllowContributorCI(rw http.ResponseWriter, r *http.Request) {
15 pr, ok := w.requireHTMLUser(rw, r)
16 if !ok {
17 return
18 }
19 r.Body = http.MaxBytesReader(rw, r.Body, 64<<10)
20 if !w.checkCSRF(r) {
21 http.Error(rw, "bad request", http.StatusForbidden)
22 return
23 }
24 m, ok := w.open(rw, r)
25 if !ok {
26 return
27 }
28 acl, err := m.ACL()
29 if err != nil || !acl.CanApprove(pr.account.ID) {
30 http.Error(rw, "only a maintainer or admin may allow CI", http.StatusForbidden)
31 return
32 }
33 if err := r.ParseForm(); err != nil {
34 http.Error(rw, "cannot read the form", http.StatusBadRequest)
35 return
36 }
37 ref := strings.TrimSpace(r.PostFormValue("ref"))
38 if _, ok := access.ContributorRefAccount(ref); !ok {
39 http.Error(rw, "that is not a contributor branch", http.StatusBadRequest)
40 return
41 }
42 tip, err := m.Git().RevParseRaw(ref)
43 if err != nil || tip == "" {
44 http.Error(rw, "that branch does not exist", http.StatusNotFound)
45 return
46 }
47 if w.AllowCI == nil {
48 http.Error(rw, "CI is not wired into this server", http.StatusInternalServerError)
49 return
50 }
51 if err := w.AllowCI(m.Name, ref, tip, pr.account.ID); err != nil {
52 http.Error(rw, err.Error(), http.StatusConflict)
53 return
54 }
55 w.audit("web.ci.allow", pr.account.ID, m.Name+" "+ref+" "+tip)
56 http.Redirect(rw, r, "/r/"+m.Name, http.StatusSeeOther)
57 }