internal/hookd/socket_test.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/internal/hookd/socket_test.go history · blame · raw

231 lines · 7371 bytes

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