e2e/runner_scope_test.go
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}