e2e/runner_scope_test.go

f327db6192d9a0877606a40385b24c0cda29fd2d
gitbay/e2e/runner_scope_test.go history · blame · raw

114 lines · 4911 bytes

  1package e2e
  2
  3import (
  4	"os"
  5	"os/exec"
  6	"path/filepath"
  7	"strings"
  8	"testing"
  9)
 10
 11// A runner names the repositories it will take builds for. Without that, any
 12// runner claims whatever is next in the global queue, so a runner on a machine
 13// that should only build one project ends up executing every repository's
 14// steps — including those of a repository it has nothing to do with.
 15func TestRunnerNextScopedToRepos(t *testing.T) {
 16	inst := startInstance(t)
 17	aliceKey := inst.newKey(t, "alice")
 18	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 19	runnerKey := inst.newKey(t, "ci")
 20	inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
 21
 22	// Two repositories, each with a build queued. "other" is pushed first, so
 23	// an unscoped claim would take it.
 24	for _, name := range []string{"other", "site"} {
 25		if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/"+name); code != 0 {
 26			t.Fatalf("repo create %s: %s", name, errOut)
 27		}
 28		work := t.TempDir()
 29		env := inst.gitEnv(aliceKey)
 30		mustGit(t, work, env, "clone", inst.sshURL("alice/"+name), "w")
 31		dir := filepath.Join(work, "w")
 32		os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
 33		os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte(
 34			"jobs:\n  "+name+":\n    steps:\n      - echo hi\n"), 0o644)
 35		mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 36		mustGit(t, dir, env, "add", ".")
 37		mustGit(t, dir, env, "commit", "-q", "-m", "base")
 38		mustGit(t, dir, env, "push", "-q", "origin", "main")
 39	}
 40
 41	// Scoped to alice/site: takes the site build, not the older other build.
 42	out, errOut, code := inst.ssh(t, runnerKey, "", "runner", "next", "alice/site", "--json")
 43	if code != 0 {
 44		t.Fatalf("runner next: %s", errOut)
 45	}
 46	if !strings.Contains(out, `"repo":"alice/site"`) {
 47		t.Fatalf("scoped claim took the wrong repo:\n%s", out)
 48	}
 49
 50	// That scope is now empty, though alice/other is still pending.
 51	out, _, code = inst.ssh(t, runnerKey, "", "runner", "next", "alice/site", "--json")
 52	if code != 0 || strings.Contains(out, `"repo":`) {
 53		t.Fatalf("scoped claim took a build outside its scope:\n%s", out)
 54	}
 55
 56	// An unscoped runner still takes it, so the default is unchanged.
 57	out, _, code = inst.ssh(t, runnerKey, "", "runner", "next", "--json")
 58	if code != 0 || !strings.Contains(out, `"repo":"alice/other"`) {
 59		t.Fatalf("unscoped claim did not take the remaining build:\n%s", out)
 60	}
 61}
 62
 63// A runner host holds a key added with --scope runner: it can claim and
 64// report builds and clone what it builds, and nothing else. Neither the
 65// same account's full key nor the runner key reaches the wrong side, so a
 66// key stolen from a build step cannot administer the instance (#92).
 67func TestRunnerScopedKey(t *testing.T) {
 68	inst := startInstance(t)
 69	aliceKey := inst.newKey(t, "alice")
 70	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 71	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
 72		t.Fatalf("repo create: %s", errOut)
 73	}
 74
 75	// ci is an ordinary account. Its runner key is self-added.
 76	ciKey := inst.newKey(t, "ci")
 77	inst.admin(t, "admin", "user", "create", "ci", "--key", ciKey+".pub")
 78	runnerKey := inst.newKey(t, "ci-runner")
 79	pub, _ := os.ReadFile(runnerKey + ".pub")
 80	if _, errOut, code := inst.ssh(t, ciKey, string(pub), "keys", "add", "--scope", "runner"); code != 0 {
 81		t.Fatalf("keys add --scope runner: %s", errOut)
 82	}
 83
 84	// The runner key claims (nothing is queued, which is a success).
 85	out, errOut, code := inst.ssh(t, runnerKey, "", "runner", "next", "--json")
 86	if code != 0 || !strings.Contains(out, "{}") {
 87		t.Fatalf("runner key cannot claim: exit %d\n%s%s", code, out, errOut)
 88	}
 89	// The runner key reaches no other command, whoami included.
 90	for _, argv := range [][]string{{"whoami"}, {"repo", "create", "ci/evil"}, {"admin", "user", "list"}} {
 91		if _, _, code := inst.ssh(t, runnerKey, "", argv...); code != 4 {
 92			t.Fatalf("runner key ran %v: exit %d, want 4", argv, code)
 93		}
 94	}
 95	// The account's full key is not a runner.
 96	if _, _, code := inst.ssh(t, ciKey, "", "runner", "next"); code != 4 {
 97		t.Fatalf("full key of a non-admin claimed a build: exit %d, want 4", code)
 98	}
 99
100	// Git: the runner key clones and cannot push.
101	work := t.TempDir()
102	env := inst.gitEnv(runnerKey)
103	mustGit(t, work, env, "clone", "-q", inst.sshURL("alice/app"), "w")
104	dir := filepath.Join(work, "w")
105	os.WriteFile(filepath.Join(dir, "f"), []byte("x\n"), 0o644)
106	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
107	mustGit(t, dir, env, "add", ".")
108	mustGit(t, dir, env, "commit", "-q", "-m", "x")
109	push := exec.Command("git", "push", "-q", "origin", "main")
110	push.Dir, push.Env = dir, env
111	if pushOut, err := push.CombinedOutput(); err == nil || !strings.Contains(string(pushOut), "scope") {
112		t.Fatalf("runner key pushed, or was refused for another reason: err=%v\n%s", err, pushOut)
113	}
114}