internal/hookd/socket_test.go
200 lines · 6371 bytes
1package hookd
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "net"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "gitbay.org/gitbay/internal/config"
14 "gitbay.org/gitbay/internal/policy"
15 "gitbay.org/gitbay/internal/store"
16)
17
18func serveSocket(t *testing.T) (sock string, st *store.Store, repoID, uid int64) {
19 t.Helper()
20 st, err := store.Open(filepath.Join(t.TempDir(), "gitbay.db"))
21 if err != nil {
22 t.Fatal(err)
23 }
24 t.Cleanup(func() { st.Close() })
25 if err := st.MigrateUp(); err != nil {
26 t.Fatal(err)
27 }
28 if uid, err = st.CreateUser("alice", false); err != nil {
29 t.Fatal(err)
30 }
31 if repoID, err = st.CreateRepo("user", uid, "app", "public"); err != nil {
32 t.Fatal(err)
33 }
34 var cfg config.Config
35 cfg.Server.Root = t.TempDir()
36 stop, err := Serve(cfg, st)
37 if err != nil {
38 t.Fatal(err)
39 }
40 t.Cleanup(func() { stop() })
41 return SocketPath(cfg.Server.Root), st, repoID, uid
42}
43
44func TestSocketIsOwnerOnly(t *testing.T) {
45 sock, _, _, _ := serveSocket(t)
46 fi, err := os.Stat(sock)
47 if err != nil {
48 t.Fatal(err)
49 }
50 if fi.Mode().Perm() != 0o600 {
51 t.Fatalf("mode %v, want 0600", fi.Mode().Perm())
52 }
53}
54
55// A request speaks for a receive-pack sshd started, and only for the
56// repository, account and scope that push was started with (#282).
57func TestHookRequestNeedsItsPushToken(t *testing.T) {
58 sock, st, repoID, uid := serveSocket(t)
59 req := Request{Hook: "pre-receive", RepoID: repoID, UserID: uid, Scope: "full"}
60
61 resp, err := Ask(sock, req, nil)
62 if err != nil {
63 t.Fatal(err)
64 }
65 if resp.Allow || !strings.Contains(resp.Message, "not started by this server") {
66 t.Fatalf("no token: %+v", resp)
67 }
68
69 token, err := st.CreatePushToken(repoID, uid, "full")
70 if err != nil {
71 t.Fatal(err)
72 }
73 req.Token = token
74 if resp, err = Ask(sock, req, nil); err != nil || !resp.Allow {
75 t.Fatalf("with token: %+v, %v", resp, err)
76 }
77
78 other, err := st.CreateUser("mallory", false)
79 if err != nil {
80 t.Fatal(err)
81 }
82 forged := req
83 forged.UserID = other
84 if resp, err = Ask(sock, forged, nil); err != nil || resp.Allow {
85 t.Fatalf("token for another account: %+v, %v", resp, err)
86 }
87
88 otherRepo, err := st.CreateRepo("user", uid, "lib", "public")
89 if err != nil {
90 t.Fatal(err)
91 }
92 forged = req
93 forged.RepoID = otherRepo
94 if resp, err = Ask(sock, forged, nil); err != nil || resp.Allow {
95 t.Fatalf("token for another repository: %+v, %v", resp, err)
96 }
97
98 forged = req
99 forged.Scope = "read"
100 if resp, err = Ask(sock, forged, nil); err != nil || resp.Allow {
101 t.Fatalf("token for another scope: %+v, %v", resp, err)
102 }
103
104 if err := st.DeletePushToken(token); err != nil {
105 t.Fatal(err)
106 }
107 if resp, err = Ask(sock, req, nil); err != nil || resp.Allow {
108 t.Fatalf("finished push: %+v, %v", resp, err)
109 }
110}
111
112func refusedRows(t *testing.T, st *store.Store, action string) []store.AuditEntry {
113 t.Helper()
114 rows, err := st.AuditEntries(store.AuditFilter{ActionPrefix: action, Limit: 10})
115 if err != nil {
116 t.Fatal(err)
117 }
118 return rows
119}
120
121// Refused hook requests and refused pushes are audited; the token never
122// lands in a row (#275).
123func TestHookRefusalsAreAudited(t *testing.T) {
124 sock, st, repoID, uid := serveSocket(t)
125
126 forged := Request{Hook: "pre-receive", RepoID: repoID, UserID: uid, Scope: "full", Token: "not-a-live-token"}
127 if resp, err := Ask(sock, forged, nil); err != nil || resp.Allow {
128 t.Fatalf("forged: %+v, %v", resp, err)
129 }
130 rows := refusedRows(t, st, "refused hook")
131 if len(rows) != 1 || rows[0].Actor != "" || strings.Contains(rows[0].Data, forged.Token) ||
132 !strings.Contains(rows[0].Data, "not started by this server") || !strings.Contains(rows[0].Data, `"hook":"pre-receive"`) {
133 t.Fatalf("refused hook rows: %+v", rows)
134 }
135
136 token, err := st.CreatePushToken(repoID, uid, "full")
137 if err != nil {
138 t.Fatal(err)
139 }
140 req := Request{Hook: "pre-receive", RepoID: repoID, UserID: uid, Scope: "full", Token: token,
141 Updates: []policy.RefUpdate{{Ref: "refs/merge-requests/1/head", Old: zeroSHA40, New: strings.Repeat("a", 40)}}}
142 if resp, err := Ask(sock, req, nil); err != nil || resp.Allow {
143 t.Fatalf("push to a server-owned ref: %+v, %v", resp, err)
144 }
145 rows = refusedRows(t, st, "refused push")
146 if len(rows) != 1 || rows[0].Actor != "alice" || strings.Contains(rows[0].Data, token) ||
147 !strings.Contains(rows[0].Data, "alice/app") || !strings.Contains(rows[0].Data, "refs/merge-requests/1/head") {
148 t.Fatalf("refused push rows: %+v", rows)
149 }
150}
151
152// A connection from another uid is audited with no actor.
153func TestPeerRefusalIsAudited(t *testing.T) {
154 old := peerCheck
155 peerCheck = func(net.Conn) error { return errors.New("peer uid not permitted") }
156 t.Cleanup(func() { peerCheck = old })
157 sock, st, repoID, uid := serveSocket(t)
158 // The server answers and closes without reading the request, so the
159 // client may see the refusal or a broken pipe; either way it is not
160 // allowed, and the row is written before the connection closes.
161 if resp, err := Ask(sock, Request{Hook: "pre-receive", RepoID: repoID, UserID: uid}, nil); err == nil && resp.Allow {
162 t.Fatalf("refused peer was allowed: %+v", resp)
163 }
164 rows := refusedRows(t, st, "refused hook")
165 if len(rows) != 1 || rows[0].Actor != "" || !strings.Contains(rows[0].Data, "peer uid not permitted") {
166 t.Fatalf("refused hook rows: %+v", rows)
167 }
168}
169
170// A refused push of many refs records the first auditedRefs names and
171// a count of the rest.
172func TestRefusedPushCapsRefs(t *testing.T) {
173 sock, st, repoID, uid := serveSocket(t)
174 token, err := st.CreatePushToken(repoID, uid, "full")
175 if err != nil {
176 t.Fatal(err)
177 }
178 req := Request{Hook: "pre-receive", RepoID: repoID, UserID: uid, Scope: "full", Token: token}
179 for i := range 500 {
180 req.Updates = append(req.Updates, policy.RefUpdate{Ref: fmt.Sprintf("refs/merge-requests/%d/head", i),
181 Old: zeroSHA40, New: strings.Repeat("a", 40)})
182 }
183 if resp, err := Ask(sock, req, nil); err != nil || resp.Allow {
184 t.Fatalf("push: %+v, %v", resp, err)
185 }
186 rows := refusedRows(t, st, "refused push")
187 if len(rows) != 1 {
188 t.Fatalf("rows: %+v", rows)
189 }
190 var data struct {
191 Refs []string `json:"refs"`
192 MoreRefs int `json:"more_refs"`
193 }
194 if err := json.Unmarshal([]byte(rows[0].Data), &data); err != nil {
195 t.Fatal(err)
196 }
197 if len(data.Refs) != auditedRefs || data.MoreRefs != 500-auditedRefs || data.Refs[0] != "refs/merge-requests/0/head" {
198 t.Fatalf("refs %d, more %d", len(data.Refs), data.MoreRefs)
199 }
200}