From 00da73bb9db70d59134981195bc15208477033ae Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 7 Dec 2022 13:55:01 -0500 Subject: [PATCH] feat(ssh): create repo metadata on new repo push --- server/git/daemon/daemon_test.go | 37 +++++++------ server/git/ssh/ssh.go | 26 ++++++--- server/git/ssh/ssh_test.go | 95 ++++++++++++++++++++++++++++---- 3 files changed, 122 insertions(+), 36 deletions(-) diff --git a/server/git/daemon/daemon_test.go b/server/git/daemon/daemon_test.go index 50d7be7f2b66f5052de39eb8315fe827a48115ba..bdc2e0b0bd153320a80c1557957bb0eacafe6f52 100644 --- a/server/git/daemon/daemon_test.go +++ b/server/git/daemon/daemon_test.go @@ -6,11 +6,10 @@ import ( "log" "net" "os" + "strconv" "testing" - "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/server/config" - "github.com/charmbracelet/soft-serve/server/db/fakedb" "github.com/charmbracelet/soft-serve/server/git" "github.com/go-git/go-git/v5/plumbing/format/pktline" ) @@ -23,21 +22,13 @@ func TestMain(m *testing.M) { log.Fatal(err) } defer os.RemoveAll(tmp) - cfg := &config.Config{ - Host: "", - DataPath: tmp, - AnonAccess: proto.ReadOnlyAccess, - Git: config.GitConfig{ - // Reduce the max read timeout to 3 second so we can test the timeout. - IdleTimeout: 3, - // Reduce the max timeout to 100 second so we can test the timeout. - MaxTimeout: 100, - // Reduce the max connections to 3 so we can test the timeout. - MaxConnections: 3, - Port: 9418, - }, - } - cfg = cfg.WithDB(&fakedb.FakeDB{}) + os.Setenv("SOFT_SERVE_DATA_PATH", tmp) + os.Setenv("SOFT_SERVE_ANON_ACCESS", "read-only") + os.Setenv("SOFT_SERVE_GIT_MAX_CONNECTIONS", "3") + os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100") + os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "3") + os.Setenv("SOFT_SERVE_GIT_PORT", strconv.Itoa(randomPort())) + cfg := config.DefaultConfig() d, err := NewDaemon(cfg) if err != nil { log.Fatal(err) @@ -50,6 +41,12 @@ func TestMain(m *testing.M) { }() defer d.Close() os.Exit(m.Run()) + os.Unsetenv("SOFT_SERVE_DATA_PATH") + os.Unsetenv("SOFT_SERVE_ANON_ACCESS") + os.Unsetenv("SOFT_SERVE_GIT_MAX_CONNECTIONS") + os.Unsetenv("SOFT_SERVE_GIT_MAX_TIMEOUT") + os.Unsetenv("SOFT_SERVE_GIT_IDLE_TIMEOUT") + os.Unsetenv("SOFT_SERVE_GIT_PORT") } func TestIdleTimeout(t *testing.T) { @@ -94,3 +91,9 @@ func readPktline(c net.Conn) (string, error) { } return string(pktout.Bytes()), nil } + +func randomPort() int { + addr, _ := net.Listen("tcp", ":0") //nolint:gosec + _ = addr.Close() + return addr.Addr().(*net.TCPAddr).Port +} diff --git a/server/git/ssh/ssh.go b/server/git/ssh/ssh.go index 3aff78d4e931b091c5f9a23e3c1a7e693ca766b2..2a9d6cffbf69632f0b80d29bc0fd542b532fcde8 100644 --- a/server/git/ssh/ssh.go +++ b/server/git/ssh/ssh.go @@ -12,12 +12,18 @@ import ( "github.com/gliderlabs/ssh" ) +// Auth is the interface that wraps both Access and Provider interfaces. +type Auth interface { + proto.Access + proto.Provider +} + // Middleware adds Git server functionality to the ssh.Server. Repos are stored // in the specified repo directory. The provided Hooks implementation will be // checked for access on a per repo basis for a ssh.Session public key. // Hooks.Push and Hooks.Fetch will be called on successful completion of // their commands. -func Middleware(repoDir string, auth proto.Access) wish.Middleware { +func Middleware(repoDir string, auth Auth) wish.Middleware { return func(sh ssh.Handler) ssh.Handler { return func(s ssh.Session) { func() { @@ -27,24 +33,29 @@ func Middleware(repoDir string, auth proto.Access) wish.Middleware { // repo should be in the form of "repo.git" repo := strings.TrimPrefix(cmd[1], "/") repo = filepath.Clean(repo) + name := repo if strings.Contains(repo, "/") { log.Printf("invalid repo: %s", repo) Fatal(s, fmt.Errorf("%s: %s", git.ErrInvalidRepo, "user repos not supported")) return } pk := s.PublicKey() - access := auth.AuthRepo(strings.TrimSuffix(repo, ".git"), pk) + access := auth.AuthRepo(name, pk) // git bare repositories should end in ".git" // https://git-scm.com/docs/gitrepository-layout - if !strings.HasSuffix(repo, ".git") { - repo += ".git" - } + repo = strings.TrimSuffix(repo, ".git") + ".git" switch gc { case "git-receive-pack": switch access { case proto.ReadWriteAccess, proto.AdminAccess: - err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo) - if err != nil { + if _, err := auth.Open(name); err != nil { + if err := auth.Create(name, "", "", false); err != nil { + log.Printf("failed to create repo: %s", err) + Fatal(s, err) + return + } + } + if err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo); err != nil { Fatal(s, git.ErrSystemMalfunction) } default: @@ -52,6 +63,7 @@ func Middleware(repoDir string, auth proto.Access) wish.Middleware { } return case "git-upload-archive", "git-upload-pack": + log.Printf("access %s", access) switch access { case proto.ReadOnlyAccess, proto.ReadWriteAccess, proto.AdminAccess: // try to upload .git first, then diff --git a/server/git/ssh/ssh_test.go b/server/git/ssh/ssh_test.go index b400a5865ed893180737cd067084e601be190c29..3e361fcdb7c941ac102f5e7fe1233f56e47035d6 100644 --- a/server/git/ssh/ssh_test.go +++ b/server/git/ssh/ssh_test.go @@ -3,6 +3,7 @@ package ssh import ( "fmt" "net" + "net/mail" "os" "os/exec" "path/filepath" @@ -14,6 +15,7 @@ import ( "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" + gossh "golang.org/x/crypto/ssh" ) func TestGitMiddleware(t *testing.T) { @@ -25,8 +27,6 @@ func TestGitMiddleware(t *testing.T) { repoDir := t.TempDir() hooks := &testHooks{ - pushes: []action{}, - fetches: []action{}, access: []accessDetails{ {pubkey, "repo1", proto.AdminAccess}, {pubkey, "repo2", proto.AdminAccess}, @@ -53,7 +53,6 @@ func TestGitMiddleware(t *testing.T) { requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo1")) requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "master")) - requireHasAction(t, hooks.pushes, pubkey, "repo1") }) t.Run("create repo on main", func(t *testing.T) { @@ -62,7 +61,6 @@ func TestGitMiddleware(t *testing.T) { requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo2")) requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main")) - requireHasAction(t, hooks.pushes, pubkey, "repo2") }) t.Run("create and clone repo", func(t *testing.T) { @@ -74,9 +72,6 @@ func TestGitMiddleware(t *testing.T) { cwd = t.TempDir() requireNoError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo3")) - - requireHasAction(t, hooks.pushes, pubkey, "repo3") - requireHasAction(t, hooks.fetches, pubkey, "repo3") }) t.Run("clone repo that doesn't exist", func(t *testing.T) { @@ -106,9 +101,6 @@ func TestGitMiddleware(t *testing.T) { cwd = t.TempDir() requireNoError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo7")) - - requireHasAction(t, hooks.pushes, pubkey, "repo7") - requireHasAction(t, hooks.fetches, pubkey, "repo7") }) } @@ -153,6 +145,7 @@ func requireHasAction(t *testing.T, actions []action, key ssh.PublicKey, repo st t.Helper() for _, action := range actions { + t.Logf("action: %q", action.repo) if repo == strings.TrimSuffix(action.repo, ".git") && ssh.KeysEqual(key, action.key) { return } @@ -195,20 +188,98 @@ type testHooks struct { access []accessDetails } +func (h *testHooks) Open(string) (proto.Repository, error) { + return nil, nil +} + +func (h *testHooks) ListRepos() ([]proto.Metadata, error) { + return nil, nil +} + func (h *testHooks) AuthRepo(repo string, key ssh.PublicKey) proto.AccessLevel { for _, dets := range h.access { - if dets.repo == repo && ssh.KeysEqual(key, dets.key) { + if strings.TrimSuffix(dets.repo, ".git") == repo && ssh.KeysEqual(key, dets.key) { return dets.level } } return proto.NoAccess } +type testUser struct{} + +func (u *testUser) Name() string { + return "test" +} + +func (u *testUser) Email() *mail.Address { + return &mail.Address{ + Name: "test", + Address: "test@wish", + } +} + +func (u *testUser) IsAdmin() bool { + return false +} + +func (u *testUser) Login() *string { + l := "test" + return &l +} + +func (u *testUser) Password() *string { + return nil +} + +func (u *testUser) PublicKeys() []gossh.PublicKey { + return nil +} + +func (h *testHooks) User(pk ssh.PublicKey) (proto.User, error) { + return &testUser{}, nil +} + +func (h *testHooks) IsAdmin(pk ssh.PublicKey) bool { + return false +} + +func (h *testHooks) IsCollab(repo string, pk ssh.PublicKey) bool { + return false +} + +func (h *testHooks) Create(name, projectName, description string, isPrivate bool) error { + return nil +} + +func (h *testHooks) Delete(repo string) error { + return nil +} + +func (h *testHooks) Rename(repo, name string) error { + return nil +} + +func (h *testHooks) SetProjectName(repo, projectName string) error { + return nil +} + +func (h *testHooks) SetDescription(repo, description string) error { + return nil +} + +func (h *testHooks) SetPrivate(repo string, isPrivate bool) error { + return nil +} + +func (h *testHooks) SetDefaultBranch(repo, branch string) error { + return nil +} + func (h *testHooks) Push(repo string, key ssh.PublicKey) { h.Lock() defer h.Unlock() - h.pushes = append(h.pushes, action{key, repo}) + h.pushes = append(h.pushes, action{key, strings.TrimSuffix(repo, ".git")}) } func (h *testHooks) Fetch(repo string, key ssh.PublicKey) {