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

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/ssh/repos_filter_test.go

Line attribution on main · 171 line(s)

c5902451
Keenan Nemetz
16d ago
1 package ssh
b238ac24
Keenan Nemetz
17d ago
2
3 // --repos lets one runner narrow what it will take, and the narrowing is done on the
4 // server against what the account's allowlist grants. These tests are about the
5 // direction of that: an operator's filter may shrink a grant, and a client's request
6 // may never grow it. The pure half (narrow) is pinned directly; the wire half is pinned
7 // through a real session, because the thing that could regress is a request field that
8 // nobody remembers to apply.
9
10 import (
11 "strings"
12 "testing"
13 "time"
14
d37d301f
Keenan Nemetz
20h ago
15 "rodent.software/vole/ci/runs"
16 "rodent.software/vole/core/id"
b238ac24
Keenan Nemetz
17d ago
17 )
18
19 func TestNarrowCanOnlySubtract(t *testing.T) {
44530e74
Keenan Nemetz
17d ago
20 t.Parallel()
b238ac24
Keenan Nemetz
17d ago
21 granted := []string{"a", "b", "c"}
22 cases := []struct {
23 name string
24 asked []string
25 want []string
26 }{
27 {"no filter keeps the whole grant", nil, []string{"a", "b", "c"}},
28 {"a filter keeps only what it names", []string{"b"}, []string{"b"}},
29 {"order follows the grant, not the filter", []string{"c", "a"}, []string{"a", "c"}},
30 {"duplicates are harmless", []string{"a", "a"}, []string{"a"}},
31 {"a name that was never granted is dropped", []string{"a", "z"}, []string{"a"}},
32 {"names outside the grant yield nothing", []string{"z"}, nil},
33 {"an empty grant yields nothing however it is asked for", []string{"a"}, nil},
34 }
35 for _, c := range cases {
36 t.Run(c.name, func(t *testing.T) {
37 g := granted
38 if strings.Contains(c.name, "empty grant") {
39 g = nil
40 }
41 got := narrow(g, c.asked)
42 if strings.Join(got, ",") != strings.Join(c.want, ",") {
43 t.Fatalf("narrow(%v, %v) = %v, want %v", g, c.asked, got, c.want)
44 }
45 })
46 }
47 }
48
49 // A lease request that names a repository the account may not run for gets nothing,
50 // rather than that repository. The allowlist is what decides; the flag only filters
51 // what it produced.
52 func TestARequestCannotLeaseBeyondTheAllowlist(t *testing.T) {
44530e74
Keenan Nemetz
17d ago
53 t.Parallel()
b238ac24
Keenan Nemetz
17d ago
54 h := newHarness(t)
55 c := h.runner(t, h.keyPath)
56 if rep := c.call(runnerRequest{Op: "ping"}); rep.Error != "" {
57 t.Fatal(rep.Error)
58 }
59
60 // A second repository the writer's account is not a runner for. Its ACL gives the
61 // writer write access, which is exactly the distinction that matters: writing to a
62 // repository is not permission to execute its code.
63 other, err := h.repos.Create("other", h.writerID, "")
64 if err != nil {
65 t.Fatal(err)
66 }
67 a, err := other.ACL()
68 if err != nil {
69 t.Fatal(err)
70 }
6c84d387
Keenan Nemetz
16d ago
71 if err := a.SetEntry(h.writerID, "maintain"); err != nil {
b238ac24
Keenan Nemetz
17d ago
72 t.Fatal(err)
73 }
74 if err := other.SetACL(a, "test"); err != nil {
75 t.Fatal(err)
76 }
77 runID, err := id.New(time.Now().UTC())
78 if err != nil {
79 t.Fatal(err)
80 }
81 if _, err := h.runs.Enqueue(runs.Run{
82 ID: runID, Repo: "other", Ref: "refs/heads/main", Branch: "main",
83 SHA: strings.Repeat("ab", 20), Workflow: ".vole/workflows/ci.yaml",
84 WorkflowSHA: strings.Repeat("cd", 20), Job: "forbidden",
85 Steps: []runs.Step{{Name: "test", Run: "true"}}, Created: time.Now().UTC(),
6c03e461
Keenan Nemetz
17d ago
86 }, nil); err != nil {
b238ac24
Keenan Nemetz
17d ago
87 t.Fatal(err)
88 }
89
90 // Asking for it explicitly must not help: the grant for demo does not include
91 // other, so the intersection is empty and nothing is leased.
92 rep := c.call(runnerRequest{Op: "lease", Repos: []string{"other"}})
93 if rep.Error != "" {
94 t.Fatal(rep.Error)
95 }
96 if rep.Run != nil {
97 t.Fatalf("leased %q from %q, which the account may not run for", rep.Run.Job, rep.Run.Repo)
98 }
99
100 // The work is still there for somebody who is allowed: an admin is implicit.
101 admin, err := h.runs.Lease(runs.LeaseRequest{
102 Owner: h.writerID, Repos: []string{"other"}, TTL: time.Minute,
103 }, time.Now().UTC())
104 if err != nil {
105 t.Fatal(err)
106 }
107 if admin == nil {
108 t.Fatal("nobody could lease the job; it should still be queued")
109 }
110 }
111
112 // A filter naming one repository takes that repository's jobs and leaves the rest.
113 func TestARepoFilterTakesOnlyWhatItNames(t *testing.T) {
44530e74
Keenan Nemetz
17d ago
114 t.Parallel()
b238ac24
Keenan Nemetz
17d ago
115 h := newHarness(t)
116 c := h.runner(t, h.keyPath)
117 if rep := c.call(runnerRequest{Op: "ping"}); rep.Error != "" {
118 t.Fatal(rep.Error)
119 }
120
121 // Two repositories the account may run for, one job in each.
122 for _, name := range []string{"demo", "second"} {
123 m, err := h.repos.Open(name)
124 if err != nil {
125 m, err = h.repos.Create(name, h.writerID, "")
126 if err != nil {
127 t.Fatal(err)
128 }
129 }
130 a, err := m.ACL()
131 if err != nil {
132 t.Fatal(err)
133 }
134 a.SetRunners([]string{h.writerID})
135 if err := m.SetACL(a, "test"); err != nil {
136 t.Fatal(err)
137 }
138 runID, err := id.New(time.Now().UTC())
139 if err != nil {
140 t.Fatal(err)
141 }
142 if _, err := h.runs.Enqueue(runs.Run{
143 ID: runID, Repo: name, Ref: "refs/heads/main", Branch: "main",
144 SHA: strings.Repeat("ab", 20), Workflow: ".vole/workflows/ci.yaml",
145 WorkflowSHA: strings.Repeat("cd", 20), Job: "job-" + name,
146 Steps: []runs.Step{{Name: "test", Run: "true"}}, Created: time.Now().UTC(),
6c03e461
Keenan Nemetz
17d ago
147 }, nil); err != nil {
b238ac24
Keenan Nemetz
17d ago
148 t.Fatal(err)
149 }
150 }
151
152 // The ping reports the grant so the client can check its own filter against it.
153 if rep := c.call(runnerRequest{Op: "ping"}); len(rep.Repos) != 2 {
154 t.Fatalf("ping reported %v, want both repositories the account may run for", rep.Repos)
155 }
156
157 rep := c.call(runnerRequest{Op: "lease", Repos: []string{"second"}})
158 if rep.Error != "" {
159 t.Fatal(rep.Error)
160 }
161 if rep.Run == nil {
162 t.Fatal("the filtered repository's job was not leased")
163 }
164 if rep.Run.Repo != "second" {
165 t.Fatalf("leased %q from %q; the filter named only second", rep.Run.Job, rep.Run.Repo)
166 }
167 // demo's job is untouched by that filter: a second, unfiltered runner takes it.
168 if again := c.call(runnerRequest{Op: "lease"}); again.Run == nil || again.Run.Repo != "demo" {
169 t.Fatalf("unfiltered lease = %+v, want demo's job", again.Run)
170 }
171 }