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/auth_test.go +50

@@ -0,0 +1,50 @@
1 package web
2
3 import (
4 "testing"
5
6 "github.com/rodent-software/vole/forge/config"
7 )
8
9 func TestNewAuthRequiresHTTPS(t *testing.T) {
10 t.Parallel()
11 for _, c := range []struct {
12 url string
13 want bool
14 }{
15 {"", false},
16 {"http://forge.example.com", false},
17 {"https://forge.example.com", true},
18 {"https://forge.example.com:8443", true},
19 } {
20 cfg := &config.Config{PublicURL: c.url}
21 a, err := newAuth(cfg)
22 if err != nil {
23 t.Fatalf("%q: %v", c.url, err)
24 }
25 if a.Enabled() != c.want {
26 t.Fatalf("%q: Enabled=%v want %v (%s)", c.url, a.Enabled(), c.want, a.Disabled)
27 }
28 }
29 a, err := newAuth(&config.Config{PublicURL: "https://Forge.Example.com:8443/path"})
30 if err != nil {
31 t.Fatal(err)
32 }
33 if a.RPID != "forge.example.com" || a.Origin != "https://Forge.Example.com:8443" {
34 t.Fatalf("RPID/Origin = %q/%q", a.RPID, a.Origin)
35 }
36 }
37
38 func TestSafeNextOnlyAllowsLocalPaths(t *testing.T) {
39 t.Parallel()
40 for _, in := range []string{"", "//evil.example", "https://evil.example", `/\evil`, "/\r\n"} {
41 if got := safeNext(in); got != "/" {
42 t.Errorf("safeNext(%q)=%q, want /", in, got)
43 }
44 }
45 for _, in := range []string{"/", "/r/demo", "/r/demo?x=1#c-2"} {
46 if got := safeNext(in); got != in {
47 t.Errorf("safeNext(%q)=%q, want itself", in, got)
48 }
49 }
50 }