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/integration_test.go +6−6

@@ -167,7 +167,7 @@ func keypair(t *testing.T, path string) (pubLine, privPath string) {
167 167 return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub))), path
168 168 }
169 169
170 // addWriter creates a second user with write access and returns their key path.
170 // addWriter creates a second user with maintain access and returns their key path.
171 171 func (f *fixture) addWriter(t *testing.T) string {
172 172 t.Helper()
173 173 pub, priv := keypair(t, filepath.Join(f.cfg.DataDir, "id_writer"))
@@ -181,7 +181,7 @@ func (f *fixture) addWriter(t *testing.T) string {
181 181 t.Fatal(err)
182 182 }
183 183 // ACL entries are keyed by account id, not by name.
184 if err := a.SetEntry(writer.ID, "write"); err != nil {
184 if err := a.SetEntry(writer.ID, "maintain"); err != nil {
185 185 t.Fatal(err)
186 186 }
187 187 if err := m.SetACL(a, "test"); err != nil {
@@ -556,7 +556,7 @@ func issueFiles(id, title, desc string) map[string]string {
556 556 }
557 557
558 558 // TestIssuePushedThenRendered is the whole option-A loop: a user with an editor
559 // and a key files a bug, and the read-only web shows it.
559 // and a key files a bug, and the web shows it.
560 560 func TestIssuePushedThenRendered(t *testing.T) {
561 561 t.Parallel()
562 562 f := newFixture(t)
@@ -589,7 +589,7 @@ func TestIssuePushedThenRendered(t *testing.T) {
589 589 if !strings.Contains(body, "<code>code</code>") {
590 590 t.Fatal("inline code was not rendered")
591 591 }
592 // An empty thread prints the command that fills it: the web is read-only, so
592 // An empty thread prints the command that fills it: the web has no comment, so
593 593 // telling somebody "no comments" and stopping is a dead end.
594 594 if !strings.Contains(body, "vole issue comment "+testIssueID) {
595 595 t.Fatalf("the empty comment state does not print the command:\n%s", body)
@@ -1198,7 +1198,7 @@ func TestRequestPageSaysWhenItWouldConflict(t *testing.T) {
1198 1198 func TestRequestDiscussionIsPushedAndOwned(t *testing.T) {
1199 1199 t.Parallel()
1200 1200 f := newFixture(t)
1201 writerKey := f.addWriter(t) // wilbur: write access, not admin
1201 writerKey := f.addWriter(t) // wilbur: maintain access, not admin
1202 1202 f.publish(f.repo)
1203 1203
1204 1204 base := workRepo(t, map[string]string{"readme.md": "v1\n"})
@@ -1550,7 +1550,7 @@ func commentFile(author, created, body string) string {
1550 1550 func TestCommentEditsBelongToTheirAuthor(t *testing.T) {
1551 1551 t.Parallel()
1552 1552 f := newFixture(t)
1553 writerKey := f.addWriter(t) // wilbur: write access, not admin
1553 writerKey := f.addWriter(t) // wilbur: maintain access, not admin
1554 1554 f.publish(f.repo)
1555 1555 const (
1556 1556 issueID = "01M5JR2JK29KKMAA1T9MDNTPD1"