internal/hookd/socket_test.go

v1.39.0
gitbay/internal/hookd/socket_test.go history · blame · raw

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}