vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Let the CLI do what the browser does for the same account
7e0b5950c190caf11089e1aaac10f18678074fed · Keenan Nemetz · 6d ago · parent 6e1fca51
Let the CLI do what the browser does for the same account A contributor could file an issue, comment, manage the item it wrote, and open a change from its own branch — but only in the browser, because a push to `refs/heads/vole` required `maintain`. The role was not the reason: the hook had no per-path authorization for that branch, so the role gate stood in for one. The per-path checker is now applied to every session, not only to run credentials. It asks the ACL the questions the browser's forms ask — CanDiscuss, CanOpenRequest, CanTriage, CanApprove — so a contributor's key can write what a contributor may write, and a maintainer's key is unchanged. A non-maintainer's new file must name that account as its author, which is what the browser does for it; a maintainer keeps writing an imported issue under its original author, because that is a trust decision rather than a capability. Creating the issues branch still needs a maintainer, as it does from the browser, and an approval still cannot approve its author's own change — the tree validator already refused that, and now every transport gets the same answer. The one rule that moved is self-approval for a maintainer's push, which is now refused earlier rather than only at the landing gate. With the branch no longer bundled into `maintain`, CONTRIBUTING and the guides stop telling contributors to use the web. Both transports are one authorization model. Part of #168, stacked on #167, which built the checker this generalises.
server/hook/hook.go +9−13
| @@ -153,11 +153,9 @@ func PreReceive(cfg Config) int { | |||
| 153 | 153 | } | |
| 154 | 154 | ||
| 155 | 155 | // A session opened with a run credential acts for one job, in one repository, | |
| 156 | // until that job ends. The role is still the account's; the credential only | ||
| 157 | // changes what a push to the issues branch may contain, because writing there | ||
| 158 | // otherwise requires maintain — a grant that is about vouching for everything, | ||
| 159 | // which a job cannot do. | ||
| 160 | var cred *runs.RunCredential | ||
| 156 | // until that job ends. The role is still the account's and the per-path rules below | ||
| 157 | // are the same ones a key gets; what the credential adds is the binding: this | ||
| 158 | // repository, and no other. | ||
| 161 | 159 | if keyID := strings.TrimSpace(cfg.KeyID); keyID != "" { | |
| 162 | 160 | found, err := lookupRunCredential(cfg, keyID) | |
| 163 | 161 | if err != nil { | |
| @@ -173,7 +171,6 @@ func PreReceive(cfg Config) int { | |||
| 173 | 171 | reject(cfg.Out, "this job's credential is for %s, not %s", found.Repo, m.Name) | |
| 174 | 172 | return 1 | |
| 175 | 173 | } | |
| 176 | cred = found | ||
| 177 | 174 | } | |
| 178 | 175 | } | |
| 179 | 176 | ||
| @@ -219,9 +216,9 @@ func PreReceive(cfg Config) int { | |||
| 219 | 216 | // access. The check is here rather than at the role gate because the gate | |
| 220 | 217 | // only knows that the account may push something. | |
| 221 | 218 | if !role.AtLeast(access.Maintain) && !access.IsContributorRef(actor, c.ref) && | |
| 222 | !(cred != nil && c.ref == metadata.Ref) { | ||
| 219 | c.ref != metadata.Ref { | ||
| 223 | 220 | problems = append(problems, fmt.Sprintf( | |
| 224 | "%s: the contribute role may only push branches under %s (role: %s)", | ||
| 221 | "%s: the contribute role may only push branches under %s and the issues branch (role: %s)", | ||
| 225 | 222 | c.ref, access.ContributorBranchPrefix(actor), role)) | |
| 226 | 223 | continue | |
| 227 | 224 | } | |
| @@ -429,11 +426,10 @@ func PreReceive(cfg Config) int { | |||
| 429 | 426 | for _, prob := range commentOwnership(g, c, actor, role, cfg.AuditPath, cfg.Out, m.Name) { | |
| 430 | 427 | problems = append(problems, prob) | |
| 431 | 428 | } | |
| 432 | // A job acting with a run credential may write to the issues branch with the | ||
| 433 | // capabilities its account has, and nothing more. | ||
| 434 | if cred != nil { | ||
| 435 | problems = append(problems, scopedMetadata(g, c, a, actor)...) | ||
| 436 | } | ||
| 429 | // What this push may write there is the account's capabilities, per path. | ||
| 430 | // The browser applies the same rules one action at a time, and the two | ||
| 431 | // transports have to keep offering the same account the same things. | ||
| 432 | problems = append(problems, writableMetadata(g, c, a, actor)...) | ||
| 437 | 433 | } | |
| 438 | 434 | } | |
| 439 | 435 | ||