e2e/runner_scope_test.go

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

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