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/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.