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/hook/hook_test.go +29−2

@@ -36,12 +36,15 @@ func newFixture(t *testing.T, blob string) *fixture {
36 36 if err != nil {
37 37 t.Fatal(err)
38 38 }
39 if err := a.SetEntry("writer", "write"); err != nil {
39 if err := a.SetEntry("writer", "maintain"); err != nil {
40 40 t.Fatal(err)
41 41 }
42 42 if err := a.SetEntry("reader", "read"); err != nil {
43 43 t.Fatal(err)
44 44 }
45 if err := a.SetEntry("contributor", "contributor"); err != nil {
46 t.Fatal(err)
47 }
45 48 if err := m.SetACL(a, "test"); err != nil {
46 49 t.Fatal(err)
47 50 }
@@ -120,6 +123,30 @@ func TestOwnerCanPush(t *testing.T) {
120 123 }
121 124 }
122 125
126 func TestContributorCanPushOnlyTheirNamespace(t *testing.T) {
127 t.Parallel()
128 f := newFixture(t, "hello\n")
129 own := "refs/heads/contrib/contributor/topic"
130 if code, out := f.run("contributor", own, 0); code != 0 {
131 t.Fatalf("contributor push to their namespace was rejected: %s", out)
132 }
133 for _, ref := range []string{
134 "refs/heads/main",
135 "refs/heads/contrib/someone-else/topic",
136 "refs/heads/contrib/contributor",
137 "refs/heads/vole",
138 "refs/tags/contrib/contributor/v1",
139 } {
140 code, out := f.run("contributor", ref, 0)
141 if code == 0 {
142 t.Fatalf("contributor was allowed to push %s", ref)
143 }
144 if !strings.Contains(out, "contributors may only push branches under refs/heads/contrib/contributor/") {
145 t.Fatalf("%s: unclear rejection: %s", ref, out)
146 }
147 }
148 }
149
123 150 func TestReadOnlyCannotPush(t *testing.T) {
124 151 t.Parallel()
125 152 f := newFixture(t, "hello\n")
@@ -127,7 +154,7 @@ func TestReadOnlyCannotPush(t *testing.T) {
127 154 if code == 0 {
128 155 t.Fatal("read-only user was allowed to push")
129 156 }
130 if !strings.Contains(out, "does not have write access") {
157 if !strings.Contains(out, "does not have maintain access") {
131 158 t.Fatalf("message should name the reason, got: %s", out)
132 159 }
133 160 if !strings.Contains(out, "reader") {