refactor(server): use a fixed repos path & drop AccessMethod

Ayman Bagabas created

Change summary

server/backend/access.go    |  8 --------
server/backend/file/file.go | 38 ++++++++++++++------------------------
server/backend/noop/noop.go |  7 -------
server/backend/repo.go      |  4 ++--
server/cmd/cmd.go           | 10 +++++-----
server/cmd/list.go          | 14 +++++++-------
server/cmd/show.go          |  7 ++-----
server/config/config.go     | 22 ++++++----------------
server/daemon.go            |  7 ++++---
server/daemon_test.go       |  8 +++++++-
server/http.go              | 13 +++++++------
server/server.go            | 13 +++++++++++--
server/session.go           |  2 +-
server/session_test.go      |  9 ++++++++-
server/ssh.go               | 16 +++++-----------
server/utils/utils.go       | 14 ++++++++++++++
16 files changed, 93 insertions(+), 99 deletions(-)

Detailed changes

server/backend/access.go 🔗

@@ -1,7 +1,5 @@
 package backend
 
-import "golang.org/x/crypto/ssh"
-
 // AccessLevel is the level of access allowed to a repo.
 type AccessLevel int
 
@@ -34,9 +32,3 @@ func (a AccessLevel) String() string {
 		return "unknown"
 	}
 }
-
-// AccessMethod is an interface that handles repository authorization.
-type AccessMethod interface {
-	// AccessLevel returns the access level for the given repository and key.
-	AccessLevel(repo string, pk ssh.PublicKey) AccessLevel
-}

server/backend/file/file.go 🔗

@@ -31,6 +31,7 @@ import (
 	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/server/backend"
+	"github.com/charmbracelet/soft-serve/server/utils"
 	"github.com/charmbracelet/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
@@ -59,8 +60,6 @@ var (
 
 var _ backend.Backend = &FileBackend{}
 
-var _ backend.AccessMethod = &FileBackend{}
-
 // FileBackend is a backend that uses the filesystem.
 type FileBackend struct { // nolint:revive
 	// path is the path to the directory containing the repositories and config
@@ -78,11 +77,6 @@ func (fb *FileBackend) reposPath() string {
 	return filepath.Join(fb.path, repos)
 }
 
-// RepositoryStorePath returns the path to the repository store.
-func (fb *FileBackend) RepositoryStorePath() string {
-	return fb.reposPath()
-}
-
 func (fb *FileBackend) settingsPath() string {
 	return filepath.Join(fb.path, settings)
 }
@@ -95,10 +89,6 @@ func (fb *FileBackend) collabsPath(repo string) string {
 	return filepath.Join(fb.path, collabs, repo, collabs)
 }
 
-func sanatizeRepo(repo string) string {
-	return strings.TrimSuffix(repo, ".git")
-}
-
 func readOneLine(path string) (string, error) {
 	f, err := os.Open(path)
 	if err != nil {
@@ -221,7 +211,7 @@ func (fb *FileBackend) AddAdmin(pk gossh.PublicKey, memo string) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) AddCollaborator(pk gossh.PublicKey, memo string, repo string) error {
-	name := sanatizeRepo(repo)
+	name := utils.SanitizeRepo(repo)
 	repo = name + ".git"
 	// Check if repo exists
 	if !exists(filepath.Join(fb.reposPath(), repo)) {
@@ -278,7 +268,7 @@ func (fb *FileBackend) Admins() ([]string, error) {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) Collaborators(repo string) ([]string, error) {
-	name := sanatizeRepo(repo)
+	name := utils.SanitizeRepo(repo)
 	repo = name + ".git"
 	// Check if repo exists
 	if !exists(filepath.Join(fb.reposPath(), repo)) {
@@ -359,7 +349,7 @@ func (fb *FileBackend) RemoveAdmin(pk gossh.PublicKey) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) RemoveCollaborator(pk gossh.PublicKey, repo string) error {
-	name := sanatizeRepo(repo)
+	name := utils.SanitizeRepo(repo)
 	repo = name + ".git"
 	// Check if repo exists
 	if !exists(filepath.Join(fb.reposPath(), repo)) {
@@ -458,7 +448,7 @@ func (fb *FileBackend) AnonAccess() backend.AccessLevel {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) Description(repo string) string {
-	repo = sanatizeRepo(repo) + ".git"
+	repo = utils.SanitizeRepo(repo) + ".git"
 	r := &Repo{path: filepath.Join(fb.reposPath(), repo), root: fb.reposPath()}
 	return r.Description()
 }
@@ -501,7 +491,7 @@ func (fb *FileBackend) IsAdmin(pk gossh.PublicKey) bool {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, repo string) bool {
-	repo = sanatizeRepo(repo) + ".git"
+	repo = utils.SanitizeRepo(repo) + ".git"
 	_, err := os.Stat(fb.collabsPath(repo))
 	if err != nil {
 		return false
@@ -532,7 +522,7 @@ func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, repo string) bool {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) IsPrivate(repo string) bool {
-	repo = sanatizeRepo(repo) + ".git"
+	repo = utils.SanitizeRepo(repo) + ".git"
 	r := &Repo{path: filepath.Join(fb.reposPath(), repo), root: fb.reposPath()}
 	return r.IsPrivate()
 }
@@ -569,7 +559,7 @@ func (fb *FileBackend) SetAnonAccess(level backend.AccessLevel) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) SetDescription(repo string, desc string) error {
-	repo = sanatizeRepo(repo) + ".git"
+	repo = utils.SanitizeRepo(repo) + ".git"
 	f, err := os.OpenFile(filepath.Join(fb.reposPath(), repo, description), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
 	if err != nil {
 		return fmt.Errorf("failed to open description file: %w", err)
@@ -584,7 +574,7 @@ func (fb *FileBackend) SetDescription(repo string, desc string) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) SetPrivate(repo string, priv bool) error {
-	repo = sanatizeRepo(repo) + ".git"
+	repo = utils.SanitizeRepo(repo) + ".git"
 	daemonExport := filepath.Join(fb.reposPath(), repo, exportOk)
 	if priv {
 		_ = os.Remove(daemonExport)
@@ -612,7 +602,7 @@ func (fb *FileBackend) SetPrivate(repo string, priv bool) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) CreateRepository(repo string, private bool) (backend.Repository, error) {
-	name := sanatizeRepo(repo)
+	name := utils.SanitizeRepo(repo)
 	repo = name + ".git"
 	rp := filepath.Join(fb.reposPath(), repo)
 	if _, err := os.Stat(rp); err == nil {
@@ -637,7 +627,7 @@ func (fb *FileBackend) CreateRepository(repo string, private bool) (backend.Repo
 //
 // It implements backend.Backend.
 func (fb *FileBackend) DeleteRepository(repo string) error {
-	name := sanatizeRepo(repo)
+	name := utils.SanitizeRepo(repo)
 	delete(fb.repos, name)
 	repo = name + ".git"
 	return os.RemoveAll(filepath.Join(fb.reposPath(), repo))
@@ -647,8 +637,8 @@ func (fb *FileBackend) DeleteRepository(repo string) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) RenameRepository(oldName string, newName string) error {
-	oldName = filepath.Join(fb.reposPath(), sanatizeRepo(oldName)+".git")
-	newName = filepath.Join(fb.reposPath(), sanatizeRepo(newName)+".git")
+	oldName = filepath.Join(fb.reposPath(), utils.SanitizeRepo(oldName)+".git")
+	newName = filepath.Join(fb.reposPath(), utils.SanitizeRepo(newName)+".git")
 	if _, err := os.Stat(oldName); errors.Is(err, os.ErrNotExist) {
 		return fmt.Errorf("repository %q does not exist", strings.TrimSuffix(filepath.Base(oldName), ".git"))
 	}
@@ -663,7 +653,7 @@ func (fb *FileBackend) RenameRepository(oldName string, newName string) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) Repository(repo string) (backend.Repository, error) {
-	name := sanatizeRepo(repo)
+	name := utils.SanitizeRepo(repo)
 	if r, ok := fb.repos[name]; ok {
 		return r, nil
 	}

server/backend/noop/noop.go 🔗

@@ -14,18 +14,11 @@ var ErrNotImpl = fmt.Errorf("not implemented")
 
 var _ backend.Backend = (*Noop)(nil)
 
-var _ backend.AccessMethod = (*Noop)(nil)
-
 // Noop is a backend that does nothing. It's used for testing.
 type Noop struct {
 	Port string
 }
 
-// RepositoryStorePath implements backend.Backend
-func (*Noop) RepositoryStorePath() string {
-	return ""
-}
-
 // Admins implements backend.Backend
 func (*Noop) Admins() ([]string, error) {
 	return nil, nil

server/backend/repo.go 🔗

@@ -11,8 +11,6 @@ type RepositoryStore interface {
 	Repository(repo string) (Repository, error)
 	// Repositories returns a list of all repositories.
 	Repositories() ([]Repository, error)
-	// RepositoryStorePath returns the path to the repository store.
-	RepositoryStorePath() string
 	// CreateRepository creates a new repository.
 	CreateRepository(name string, private bool) (Repository, error)
 	// DeleteRepository deletes a repository.
@@ -35,6 +33,8 @@ type RepositoryMetadata interface {
 
 // RepositoryAccess is an interface for managing repository access.
 type RepositoryAccess interface {
+	// AccessLevel returns the access level for the given repository and key.
+	AccessLevel(repo string, pk ssh.PublicKey) AccessLevel
 	// IsCollaborator returns true if the authorized key is a collaborator on the repository.
 	IsCollaborator(pk ssh.PublicKey, repo string) bool
 	// AddCollaborator adds the authorized key as a collaborator on the repository.

server/cmd/cmd.go 🔗

@@ -3,10 +3,10 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"strings"
 
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/server/config"
+	"github.com/charmbracelet/soft-serve/server/utils"
 	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	"github.com/spf13/cobra"
@@ -75,8 +75,8 @@ func checkIfReadable(cmd *cobra.Command, args []string) error {
 		repo = args[0]
 	}
 	cfg, s := fromContext(cmd)
-	rn := strings.TrimSuffix(repo, ".git")
-	auth := cfg.Access.AccessLevel(rn, s.PublicKey())
+	rn := utils.SanitizeRepo(repo)
+	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
 	if auth < backend.ReadOnlyAccess {
 		return ErrUnauthorized
 	}
@@ -97,8 +97,8 @@ func checkIfCollab(cmd *cobra.Command, args []string) error {
 		repo = args[0]
 	}
 	cfg, s := fromContext(cmd)
-	rn := strings.TrimSuffix(repo, ".git")
-	auth := cfg.Access.AccessLevel(rn, s.PublicKey())
+	rn := utils.SanitizeRepo(repo)
+	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
 	if auth < backend.ReadWriteAccess {
 		return ErrUnauthorized
 	}

server/cmd/list.go 🔗

@@ -13,21 +13,21 @@ import (
 // listCommand returns a command that list file or directory at path.
 func listCommand() *cobra.Command {
 	listCmd := &cobra.Command{
-		Use:               "list PATH",
-		Aliases:           []string{"ls"},
-		Short:             "List files at repository.",
-		Args:              cobra.RangeArgs(0, 1),
-		PersistentPreRunE: checkIfReadable,
+		Use:     "list PATH",
+		Aliases: []string{"ls"},
+		Short:   "List files at repository.",
+		Args:    cobra.RangeArgs(0, 1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, s := fromContext(cmd)
 			rn := ""
 			path := ""
 			ps := []string{}
 			if len(args) > 0 {
+				// FIXME: nested repos are not supported.
 				path = filepath.Clean(args[0])
 				ps = strings.Split(path, "/")
 				rn = strings.TrimSuffix(ps[0], ".git")
-				auth := cfg.Access.AccessLevel(rn, s.PublicKey())
+				auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
 				if auth < backend.ReadOnlyAccess {
 					return ErrUnauthorized
 				}
@@ -38,7 +38,7 @@ func listCommand() *cobra.Command {
 					return err
 				}
 				for _, r := range repos {
-					if cfg.Access.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
+					if cfg.Backend.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
 						cmd.Println(r.Name())
 					}
 				}

server/cmd/show.go 🔗

@@ -33,14 +33,11 @@ func showCommand() *cobra.Command {
 		Args:              cobra.ExactArgs(1),
 		PersistentPreRunE: checkIfReadable,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			cfg, s := fromContext(cmd)
+			cfg, _ := fromContext(cmd)
+			// FIXME: nested repos are not supported.
 			ps := strings.Split(args[0], "/")
 			rn := strings.TrimSuffix(ps[0], ".git")
 			fp := strings.Join(ps[1:], "/")
-			auth := cfg.Access.AccessLevel(rn, s.PublicKey())
-			if auth < backend.ReadOnlyAccess {
-				return ErrUnauthorized
-			}
 			var repo backend.Repository
 			repoExists := false
 			repos, err := cfg.Backend.Repositories()

server/config/config.go 🔗

@@ -6,7 +6,6 @@ import (
 	"github.com/caarlos0/env/v6"
 	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/server/backend"
-	"github.com/charmbracelet/soft-serve/server/backend/file"
 )
 
 // SSHConfig is the configuration for the SSH server.
@@ -20,6 +19,9 @@ type SSHConfig struct {
 	// KeyPath is the path to the SSH server's private key.
 	KeyPath string `env:"KEY_PATH"`
 
+	// InternalKeyPath is the path to the SSH server's internal private key.
+	InternalKeyPath string `env:"INTERNAL_KEY_PATH"`
+
 	// MaxTimeout is the maximum number of seconds a connection can take.
 	MaxTimeout int `env:"MAX_TIMEOUT" envDefault:"0"`
 
@@ -73,9 +75,6 @@ type Config struct {
 
 	// Backend is the Git backend to use.
 	Backend backend.Backend
-
-	// Access is the access control backend to use.
-	Access backend.AccessMethod
 }
 
 // DefaultConfig returns a Config with the values populated with the defaults
@@ -90,13 +89,10 @@ func DefaultConfig() *Config {
 	if cfg.SSH.KeyPath == "" {
 		cfg.SSH.KeyPath = filepath.Join(cfg.DataPath, "ssh", "soft_serve")
 	}
-	fb, err := file.NewFileBackend(cfg.DataPath)
-	if err != nil {
-		log.Fatal(err)
+	if cfg.SSH.InternalKeyPath == "" {
+		cfg.SSH.InternalKeyPath = filepath.Join(cfg.DataPath, "ssh", "soft_serve_internal")
 	}
-	// Add the initial admin keys to the list of admins.
-	fb.AdditionalAdmins = cfg.InitialAdminKeys
-	return cfg.WithBackend(fb).WithAccessMethod(fb)
+	return cfg
 }
 
 // WithBackend sets the backend for the configuration.
@@ -104,9 +100,3 @@ func (c *Config) WithBackend(backend backend.Backend) *Config {
 	c.Backend = backend
 	return c
 }
-
-// WithAccessMethod sets the access control method for the configuration.
-func (c *Config) WithAccessMethod(access backend.AccessMethod) *Config {
-	c.Access = access
-	return c
-}

server/daemon.go 🔗

@@ -12,6 +12,7 @@ import (
 
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/server/config"
+	"github.com/charmbracelet/soft-serve/server/utils"
 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 )
 
@@ -201,19 +202,19 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 			return
 		}
 
-		name := sanitizeRepoName(string(opts[0]))
+		name := utils.SanitizeRepo(string(opts[0]))
 		logger.Debugf("git: connect %s %s %s", c.RemoteAddr(), cmd, name)
 		defer logger.Debugf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, name)
 		// git bare repositories should end in ".git"
 		// https://git-scm.com/docs/gitrepository-layout
 		repo := name + ".git"
-		reposDir := d.cfg.Backend.RepositoryStorePath()
+		reposDir := filepath.Join(d.cfg.DataPath, "repos")
 		if err := ensureWithin(reposDir, repo); err != nil {
 			fatal(c, err)
 			return
 		}
 
-		auth := d.cfg.Access.AccessLevel(name, nil)
+		auth := d.cfg.Backend.AccessLevel(name, nil)
 		if auth < backend.ReadOnlyAccess {
 			fatal(c, ErrNotAuthed)
 			return

server/daemon_test.go 🔗

@@ -8,10 +8,12 @@ import (
 	"log"
 	"net"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/charmbracelet/soft-serve/server/backend/file"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 )
@@ -29,7 +31,11 @@ func TestMain(m *testing.M) {
 	os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100")
 	os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "1")
 	os.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", randomPort()))
-	cfg := config.DefaultConfig()
+	fb, err := file.NewFileBackend(filepath.Join(tmp, "repos"))
+	if err != nil {
+		log.Fatal(err)
+	}
+	cfg := config.DefaultConfig().WithBackend(fb).WithAccessMethod(fb)
 	d, err := NewGitDaemon(cfg)
 	if err != nil {
 		log.Fatal(err)

server/http.go 🔗

@@ -13,6 +13,7 @@ import (
 
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/server/config"
+	"github.com/charmbracelet/soft-serve/server/utils"
 	"github.com/dustin/go-humanize"
 	"goji.io"
 	"goji.io/pat"
@@ -68,7 +69,7 @@ func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
 	mux := goji.NewMux()
 	s := &HTTPServer{
 		cfg:        cfg,
-		dirHandler: http.FileServer(http.Dir(cfg.Backend.RepositoryStorePath())),
+		dirHandler: http.FileServer(http.Dir(filepath.Join(cfg.DataPath, "repos"))),
 		server: &http.Server{
 			Addr:              cfg.HTTP.ListenAddr,
 			Handler:           mux,
@@ -114,7 +115,7 @@ Redirecting to docs at <a href="https://godoc.org/{{.ImportRoot}}/{{.Repo}}">god
 
 func (s *HTTPServer) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
 	repo := pat.Param(r, "repo")
-	repo = sanitizeRepoName(repo)
+	repo = utils.SanitizeRepo(repo)
 
 	// Only respond to go-get requests
 	if r.URL.Query().Get("go-get") != "1" {
@@ -122,7 +123,7 @@ func (s *HTTPServer) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	access := s.cfg.Access.AccessLevel(repo, nil)
+	access := s.cfg.Backend.AccessLevel(repo, nil)
 	if access < backend.ReadOnlyAccess {
 		http.NotFound(w, r)
 		return
@@ -149,16 +150,16 @@ func (s *HTTPServer) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
 
 func (s *HTTPServer) dumbGitHandler(w http.ResponseWriter, r *http.Request) {
 	repo := pat.Param(r, "repo")
-	repo = sanitizeRepoName(repo) + ".git"
+	repo = utils.SanitizeRepo(repo) + ".git"
 
-	access := s.cfg.Access.AccessLevel(repo, nil)
+	access := s.cfg.Backend.AccessLevel(repo, nil)
 	if access < backend.ReadOnlyAccess || !s.cfg.Backend.AllowKeyless() {
 		httpStatusError(w, http.StatusUnauthorized)
 		return
 	}
 
 	path := pattern.Path(r.Context())
-	stat, err := os.Stat(filepath.Join(s.cfg.Backend.RepositoryStorePath(), repo, path))
+	stat, err := os.Stat(filepath.Join(s.cfg.DataPath, "repos", repo, path))
 	// Restrict access to files
 	if err != nil || stat.IsDir() {
 		http.NotFound(w, r)

server/server.go 🔗

@@ -7,6 +7,7 @@ import (
 	"github.com/charmbracelet/log"
 
 	"github.com/charmbracelet/soft-serve/server/backend"
+	"github.com/charmbracelet/soft-serve/server/backend/file"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/charmbracelet/ssh"
 	"golang.org/x/sync/errgroup"
@@ -23,7 +24,6 @@ type Server struct {
 	HTTPServer *HTTPServer
 	Config     *config.Config
 	Backend    backend.Backend
-	Access     backend.AccessMethod
 }
 
 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
@@ -33,10 +33,19 @@ type Server struct {
 // publicly writable until configured otherwise by cloning the `config` repo.
 func NewServer(cfg *config.Config) (*Server, error) {
 	var err error
+	if cfg.Backend == nil {
+		fb, err := file.NewFileBackend(cfg.DataPath)
+		if err != nil {
+			logger.Fatal(err)
+		}
+		// Add the initial admin keys to the list of admins.
+		fb.AdditionalAdmins = cfg.InitialAdminKeys
+		cfg = cfg.WithBackend(fb)
+	}
+
 	srv := &Server{
 		Config:  cfg,
 		Backend: cfg.Backend,
-		Access:  cfg.Access,
 	}
 	srv.SSHServer, err = NewSSHServer(cfg)
 	if err != nil {

server/session.go 🔗

@@ -26,7 +26,7 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
 		initialRepo := ""
 		if len(cmd) == 1 {
 			initialRepo = cmd[0]
-			auth := cfg.Access.AccessLevel(initialRepo, s.PublicKey())
+			auth := cfg.Backend.AccessLevel(initialRepo, s.PublicKey())
 			if auth < backend.ReadOnlyAccess {
 				wish.Fatalln(s, cm.ErrUnauthorized)
 				return nil

server/session_test.go 🔗

@@ -3,10 +3,13 @@ package server
 import (
 	"errors"
 	"fmt"
+	"log"
 	"os"
+	"path/filepath"
 	"testing"
 	"time"
 
+	"github.com/charmbracelet/soft-serve/server/backend/file"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/charmbracelet/ssh"
 	bm "github.com/charmbracelet/wish/bubbletea"
@@ -49,7 +52,11 @@ func setup(tb testing.TB) *gossh.Session {
 		is.NoErr(os.Unsetenv("SOFT_SERVE_SSH_LISTEN_ADDR"))
 		is.NoErr(os.RemoveAll(dp))
 	})
-	cfg := config.DefaultConfig()
+	fb, err := file.NewFileBackend(filepath.Join(dp, "repos"))
+	if err != nil {
+		log.Fatal(err)
+	}
+	cfg := config.DefaultConfig().WithBackend(fb).WithAccessMethod(fb)
 	return testsession.New(tb, &ssh.Server{
 		Handler: bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256)(func(s ssh.Session) {
 			_, _, active := s.Pty()

server/ssh.go 🔗

@@ -12,6 +12,7 @@ import (
 	"github.com/charmbracelet/soft-serve/server/backend"
 	cm "github.com/charmbracelet/soft-serve/server/cmd"
 	"github.com/charmbracelet/soft-serve/server/config"
+	"github.com/charmbracelet/soft-serve/server/utils"
 	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	bm "github.com/charmbracelet/wish/bubbletea"
@@ -88,7 +89,7 @@ func (s *SSHServer) Shutdown(ctx context.Context) error {
 
 // PublicKeyAuthHandler handles public key authentication.
 func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
-	return s.cfg.Access.AccessLevel("", pk) > backend.NoAccess
+	return s.cfg.Backend.AccessLevel("", pk) > backend.NoAccess
 }
 
 // KeyboardInteractiveHandler handles keyboard interactive authentication.
@@ -109,14 +110,14 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 				if len(cmd) >= 2 && strings.HasPrefix(cmd[0], "git") {
 					gc := cmd[0]
 					// repo should be in the form of "repo.git"
-					name := sanitizeRepoName(cmd[1])
+					name := utils.SanitizeRepo(cmd[1])
 					pk := s.PublicKey()
-					access := cfg.Access.AccessLevel(name, pk)
+					access := cfg.Backend.AccessLevel(name, pk)
 					// git bare repositories should end in ".git"
 					// https://git-scm.com/docs/gitrepository-layout
 					repo := name + ".git"
 
-					reposDir := cfg.Backend.RepositoryStorePath()
+					reposDir := filepath.Join(cfg.DataPath, "repos")
 					if err := ensureWithin(reposDir, repo); err != nil {
 						sshFatal(s, err)
 						return
@@ -168,10 +169,3 @@ func sshFatal(s ssh.Session, v ...interface{}) {
 	WritePktline(s, v...)
 	s.Exit(1) // nolint: errcheck
 }
-
-func sanitizeRepoName(repo string) string {
-	repo = strings.TrimPrefix(repo, "/")
-	repo = filepath.Clean(repo)
-	repo = strings.TrimSuffix(repo, ".git")
-	return repo
-}

server/utils/utils.go 🔗

@@ -0,0 +1,14 @@
+package utils
+
+import (
+	"path/filepath"
+	"strings"
+)
+
+// SanitizeRepo returns a sanitized version of the given repository name.
+func SanitizeRepo(repo string) string {
+	repo = strings.TrimPrefix(repo, "/")
+	repo = filepath.Clean(repo)
+	repo = strings.TrimSuffix(repo, ".git")
+	return repo
+}