e2e/git_test.go

v1.40.0
gitbay/e2e/git_test.go history · blame · raw

160 lines · 5387 bytes

  1package e2e
  2
  3import (
  4	"fmt"
  5	"os"
  6	"os/exec"
  7	"path/filepath"
  8	"strings"
  9	"testing"
 10)
 11
 12// gitEnv returns the environment for running the git client against the
 13// instance with the given key.
 14func (i *instance) gitEnv(key string) []string {
 15	sshCmd := fmt.Sprintf(
 16		"ssh -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=%s -o BatchMode=yes",
 17		key, filepath.Join(i.sshDir, "known_hosts"))
 18	return append(os.Environ(),
 19		"GIT_SSH_COMMAND="+sshCmd,
 20		// Isolate from the developer's own git config (signing, helpers).
 21		"GIT_CONFIG_NOSYSTEM=1",
 22		"GIT_CONFIG_GLOBAL=/dev/null",
 23		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
 24		"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test",
 25	)
 26}
 27
 28func (i *instance) sshURL(repo string) string {
 29	return fmt.Sprintf("ssh://git@127.0.0.1:%d/%s.git", i.port, repo)
 30}
 31
 32// git runs a git command; returns combined output and exit code.
 33func gitRun(t *testing.T, dir string, env []string, args ...string) (string, int) {
 34	t.Helper()
 35	cmd := exec.Command("git", args...)
 36	cmd.Dir = dir
 37	cmd.Env = env
 38	out, err := cmd.CombinedOutput()
 39	code := 0
 40	if ee, ok := err.(*exec.ExitError); ok {
 41		code = ee.ExitCode()
 42	} else if err != nil {
 43		t.Fatalf("git %v: %v", args, err)
 44	}
 45	return string(out), code
 46}
 47
 48func mustGit(t *testing.T, dir string, env []string, args ...string) string {
 49	t.Helper()
 50	out, code := gitRun(t, dir, env, args...)
 51	if code != 0 {
 52		t.Fatalf("git %v failed (%d):\n%s", args, code, out)
 53	}
 54	return out
 55}
 56
 57func TestGitOverSSH(t *testing.T) {
 58	t.Parallel()
 59	inst := startInstance(t)
 60
 61	aliceKey := inst.newKey(t, "alice")
 62	bobKey := inst.newKey(t, "bob")
 63	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 64	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
 65
 66	// Alice creates a private repo over bare ssh.
 67	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/proj", "--private")
 68	if code != 0 {
 69		t.Fatalf("repo create: exit %d, %s", code, errOut)
 70	}
 71
 72	// Alice clones (empty), commits, pushes.
 73	work := t.TempDir()
 74	aliceEnv := inst.gitEnv(aliceKey)
 75	mustGit(t, work, aliceEnv, "clone", inst.sshURL("alice/proj"), "proj")
 76	dir := filepath.Join(work, "proj")
 77	if err := os.WriteFile(filepath.Join(dir, "README"), []byte("hello\n"), 0o644); err != nil {
 78		t.Fatal(err)
 79	}
 80	mustGit(t, dir, aliceEnv, "checkout", "-q", "-b", "main")
 81	mustGit(t, dir, aliceEnv, "add", "README")
 82	mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "init")
 83	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
 84
 85	// Bob is denied clone of the private repo, indistinguishable from
 86	// nonexistence.
 87	bobEnv := inst.gitEnv(bobKey)
 88	out, code := gitRun(t, t.TempDir(), bobEnv, "clone", inst.sshURL("alice/proj"), "proj")
 89	if code == 0 {
 90		t.Fatal("bob cloned a private repo without access")
 91	}
 92	if !strings.Contains(out, "repository not found") {
 93		t.Fatalf("denial should read as not-found, got:\n%s", out)
 94	}
 95
 96	// Alice grants bob read; clone succeeds; push is denied.
 97	_, errOut, code = inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/proj", "bob", "read")
 98	if code != 0 {
 99		t.Fatalf("access grant: exit %d, %s", code, errOut)
100	}
101	bobWork := t.TempDir()
102	mustGit(t, bobWork, bobEnv, "clone", inst.sshURL("alice/proj"), "proj")
103	bobDir := filepath.Join(bobWork, "proj")
104	if err := os.WriteFile(filepath.Join(bobDir, "x"), []byte("x\n"), 0o644); err != nil {
105		t.Fatal(err)
106	}
107	mustGit(t, bobDir, bobEnv, "add", "x")
108	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "bob")
109	out, code = gitRun(t, bobDir, bobEnv, "push", "origin", "main")
110	if code == 0 {
111		t.Fatal("bob pushed with read-only access")
112	}
113	if !strings.Contains(out, "write access to alice/proj denied") {
114		t.Fatalf("push denial message:\n%s", out)
115	}
116
117	// Alice protects main: force-push and deletion are refused by the hook,
118	// normal pushes still work.
119	_, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "protect", "alice/proj", "main")
120	if code != 0 {
121		t.Fatalf("protect: exit %d, %s", code, errOut)
122	}
123
124	mustGit(t, dir, aliceEnv, "commit", "-q", "--allow-empty", "-m", "second")
125	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
126
127	mustGit(t, dir, aliceEnv, "reset", "-q", "--hard", "HEAD~1")
128	mustGit(t, dir, aliceEnv, "commit", "-q", "--allow-empty", "-m", "rewritten")
129	out, code = gitRun(t, dir, aliceEnv, "push", "--force", "origin", "main")
130	if code == 0 {
131		t.Fatal("force-push to protected branch succeeded")
132	}
133	if !strings.Contains(out, "force-push refused") {
134		t.Fatalf("force-push denial message:\n%s", out)
135	}
136
137	out, code = gitRun(t, dir, aliceEnv, "push", "origin", ":main")
138	if code == 0 {
139		t.Fatal("deletion of protected branch succeeded")
140	}
141	if !strings.Contains(out, "deletion refused") {
142		t.Fatalf("deletion denial message:\n%s", out)
143	}
144
145	// refs/merge-requests/* is unpushable even by the owner.
146	out, code = gitRun(t, dir, aliceEnv, "push", "origin", "HEAD:refs/merge-requests/1/head")
147	if code == 0 {
148		t.Fatal("client pushed into refs/merge-requests/*")
149	}
150	if !strings.Contains(out, "server-owned") {
151		t.Fatalf("mr-ref denial message:\n%s", out)
152	}
153
154	// Unprotect: force-push now goes through.
155	_, _, code = inst.ssh(t, aliceKey, "", "repo", "settings", "unprotect", "alice/proj", "main")
156	if code != 0 {
157		t.Fatal("unprotect failed")
158	}
159	mustGit(t, dir, aliceEnv, "push", "-q", "--force", "origin", "main")
160}