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

@@ -121,8 +121,8 @@ func PreReceive(cfg Config) int {
121 121 who := func(accountID string) string { return names(accountID) }
122 122
123 123 role := a.RoleFor(actorID)
124 if !role.AtLeast(access.Write) {
125 reject(cfg.Out, "%s does not have write access to %s (role: %s)", actor, m.Name, role)
124 if !role.AtLeast(access.Contributor) {
125 reject(cfg.Out, "%s does not have maintain access or contributor access to %s (role: %s)", actor, m.Name, role)
126 126 return 1
127 127 }
128 128
@@ -158,6 +158,16 @@ func PreReceive(cfg Config) int {
158 158 "%s: only refs/heads/* and refs/tags/* may be pushed", c.ref))
159 159 continue
160 160 }
161 // A contributor gets one branch namespace, named after the account id.
162 // Everything else — metadata, shared branches, tags — belongs to write or
163 // admin access. The check is here rather than at the role gate because the
164 // gate only knows that the account may push something.
165 if !role.AtLeast(access.Maintain) && !access.IsContributorRef(actorID, c.ref) {
166 problems = append(problems, fmt.Sprintf(
167 "%s: contributors may only push branches under %s (role: %s)",
168 c.ref, access.ContributorBranchPrefix(actorID), role))
169 continue
170 }
161 171 // The metadata branch is append-only in shape: linear history, no merge
162 172 // commits. A merge node in metadata says nothing and makes the log harder
163 173 // to read than the change it describes.
@@ -177,12 +187,16 @@ func PreReceive(cfg Config) int {
177 187 if err != nil {
178 188 problems = append(problems, fmt.Sprintf("%s: cannot check fast-forward: %v", c.ref, err))
179 189 } else if !ff {
180 if !role.AtLeast(access.Admin) {
181 problems = append(problems, fmt.Sprintf(
182 "%s: rewriting history requires admin access; %s is %s", c.ref, actor, role))
183 } else {
190 switch {
191 case role.AtLeast(access.Admin):
184 192 auditLine(cfg.AuditPath, fmt.Sprintf("force-push %s %s -> %s by %s in %s",
185 193 c.ref, format.SHA(c.old), format.SHA(c.new), actor, m.Name))
194 case role == access.Contributor && access.IsContributorRef(actorID, c.ref):
195 // The namespace is theirs: rebasing or deleting their own branch is
196 // not a rewrite of shared history.
197 default:
198 problems = append(problems, fmt.Sprintf(
199 "%s: rewriting history requires admin access; %s is %s", c.ref, actor, role))
186 200 }
187 201 }
188 202 }