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.
Phase 4: a runner can narrow what it takes, and cannot widen it
b238ac2448290fb706d0503c2e65798fc618f994 · Keenan Nemetz · 16d ago · parent 8d336083
Phase 4: a runner can narrow what it takes, and cannot widen it `vole runner --repos name` (repeatable) restricts a session to the repositories it names. The restriction is applied by the server as an intersection with what the account's runner allowlist grants, which is the whole design: the flag is a filter, not a grant, so the worst a client can do is run less than it was allowed to. Honouring the client's list directly would have made an SSH flag an access-control decision. An empty filter means "no narrowing" rather than "nothing", which keeps an unfiltered runner following allowlist edits instead of freezing its list at connect time. The filter has a failure mode with no symptom: a runner whose --repos matches nothing connects, waits, and never sees work, which looks exactly like a quiet instance. So "ping" now reports the account's grant and the runner refuses at startup, naming what it asked for and what it was allowed — the same class of check as the signing key, cheaper before a build than after one that never happened. An account with an empty grant and no filter is only a warning, because an allowlist nobody has written yet takes effect without a restart. runs.Store.Lease now takes a LeaseRequest struct. It gained a second list of names, and two adjacent []string arguments are exactly the shape that gets transposed in silence. Measured: TestARequestCannotLeaseBeyondTheAllowlist — a repository the account may *write* to but is not a runner for is refused even when asked for by name, and an admin can still lease it. TestARepoFilterTakesOnlyWhatItNames, TestNarrowCanOnlySubtract, TestRepoFilterThatCanNeverMatchIsRefused and TestARepoFilterOutsideTheGrantIsRefusedAtStartup cover the rest.
internal/server/integration_test.go +22
| @@ -2677,6 +2677,28 @@ jobs: | |||
| 2677 | 2677 | } | |
| 2678 | 2678 | } | |
| 2679 | 2679 | ||
| 2680 | // A runner started with --repos for something its account may not run refuses at | ||
| 2681 | // startup. That is the whole point of checking it before the loop: a filter that | ||
| 2682 | // matches nothing looks exactly like a quiet instance, so a runner that started | ||
| 2683 | // happily and idled would be indistinguishable from a healthy one. | ||
| 2684 | func TestARepoFilterOutsideTheGrantIsRefusedAtStartup(t *testing.T) { | ||
| 2685 | f := newFixture(t) | ||
| 2686 | f.publish(f.repo) | ||
| 2687 | |||
| 2688 | cmd := exec.Command(binPath, "runner", "--remote", f.sshBase(), "--once", | ||
| 2689 | "--dir", t.TempDir(), "--repos", "some-other-repo") | ||
| 2690 | cmd.Env = f.ciEnv() | ||
| 2691 | out, err := cmd.CombinedOutput() | ||
| 2692 | if err == nil { | ||
| 2693 | t.Fatalf("a runner whose --repos matches nothing started anyway:\n%s", out) | ||
| 2694 | } | ||
| 2695 | for _, want := range []string{"some-other-repo", "matches nothing", f.repo} { | ||
| 2696 | if !strings.Contains(string(out), want) { | ||
| 2697 | t.Fatalf("the refusal does not mention %q:\n%s", want, out) | ||
| 2698 | } | ||
| 2699 | } | ||
| 2700 | } | ||
| 2701 | |||
| 2680 | 2702 | // TestRunRecordIsPortable is the promise the record makes: the result of a run is | |
| 2681 | 2703 | // on the metadata branch, so a plain clone carries it, and the queue's finished | |
| 2682 | 2704 | // half can be rebuilt from git after the database is deleted. | |