1f673f30802b88a426a83683f950eab8376f0ccf

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

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

Diff

This diff is truncated to protect this page.

  1diff --git a/server/backend/access.go b/server/backend/access.go
  2index 52cc16eff8357c46fa84d18bed37336ba81ded82..e13e20fcc5e175ca68f633339a36b293ce39adb9 100644
  3--- a/server/backend/access.go
  4+++ b/server/backend/access.go
  5@@ -1,7 +1,5 @@
  6 package backend
  7 
  8-import "golang.org/x/crypto/ssh"
  9-
 10 // AccessLevel is the level of access allowed to a repo.
 11 type AccessLevel int
 12 
 13@@ -34,9 +32,3 @@ func (a AccessLevel) String() string {
 14 		return "unknown"
 15 	}
 16 }
 17-
 18-// AccessMethod is an interface that handles repository authorization.
 19-type AccessMethod interface {
 20-	// AccessLevel returns the access level for the given repository and key.
 21-	AccessLevel(repo string, pk ssh.PublicKey) AccessLevel
 22-}
 23diff --git a/server/backend/file/file.go b/server/backend/file/file.go
 24index 66d19ac4fb08559eaeb7cd0095c6dd55b1db9f01..68e5d92ff701dcadc50ba3939b66e26f560cda51 100644
 25--- a/server/backend/file/file.go
 26+++ b/server/backend/file/file.go
 27@@ -31,6 +31,7 @@ import (
 28 	"github.com/charmbracelet/log"
 29 	"github.com/charmbracelet/soft-serve/git"
 30 	"github.com/charmbracelet/soft-serve/server/backend"
 31+	"github.com/charmbracelet/soft-serve/server/utils"
 32 	"github.com/charmbracelet/ssh"
 33 	gossh "golang.org/x/crypto/ssh"
 34 )
 35@@ -59,8 +60,6 @@ var (
 36 
 37 var _ backend.Backend = &FileBackend{}
 38 
 39-var _ backend.AccessMethod = &FileBackend{}
 40-
 41 // FileBackend is a backend that uses the filesystem.
 42 type FileBackend struct { // nolint:revive
 43 	// path is the path to the directory containing the repositories and config
 44@@ -78,11 +77,6 @@ func (fb *FileBackend) reposPath() string {
 45 	return filepath.Join(fb.path, repos)
 46 }
 47 
 48-// RepositoryStorePath returns the path to the repository store.
 49-func (fb *FileBackend) RepositoryStorePath() string {
 50-	return fb.reposPath()
 51-}
 52-
 53 func (fb *FileBackend) settingsPath() string {
 54 	return filepath.Join(fb.path, settings)
 55 }
 56@@ -95,10 +89,6 @@ func (fb *FileBackend) collabsPath(repo string) string {
 57 	return filepath.Join(fb.path, collabs, repo, collabs)
 58 }
 59 
 60-func sanatizeRepo(repo string) string {
 61-	return strings.TrimSuffix(repo, ".git")
 62-}
 63-
 64 func readOneLine(path string) (string, error) {
 65 	f, err := os.Open(path)
 66 	if err != nil {
 67@@ -221,7 +211,7 @@ func (fb *FileBackend) AddAdmin(pk gossh.PublicKey, memo string) error {
 68 //
 69 // It implements backend.Backend.
 70 func (fb *FileBackend) AddCollaborator(pk gossh.PublicKey, memo string, repo string) error {
 71-	name := sanatizeRepo(repo)
 72+	name := utils.SanitizeRepo(repo)
 73 	repo = name + ".git"
 74 	// Check if repo exists
 75 	if !exists(filepath.Join(fb.reposPath(), repo)) {
 76@@ -278,7 +268,7 @@ func (fb *FileBackend) Admins() ([]string, error) {
 77 //
 78 // It implements backend.Backend.
 79 func (fb *FileBackend) Collaborators(repo string) ([]string, error) {
 80-	name := sanatizeRepo(repo)
 81+	name := utils.SanitizeRepo(repo)
 82 	repo = name + ".git"
 83 	// Check if repo exists
 84 	if !exists(filepath.Join(fb.reposPath(), repo)) {
 85@@ -359,7 +349,7 @@ func (fb *FileBackend) RemoveAdmin(pk gossh.PublicKey) error {
 86 //
 87 // It implements backend.Backend.
 88 func (fb *FileBackend) RemoveCollaborator(pk gossh.PublicKey, repo string) error {
 89-	name := sanatizeRepo(repo)
 90+	name := utils.SanitizeRepo(repo)
 91 	repo = name + ".git"
 92 	// Check if repo exists
 93 	if !exists(filepath.Join(fb.reposPath(), repo)) {
 94@@ -458,7 +448,7 @@ func (fb *FileBackend) AnonAccess() backend.AccessLevel {
 95 //
 96 // It implements backend.Backend.
 97 func (fb *FileBackend) Description(repo string) string {
 98-	repo = sanatizeRepo(repo) + ".git"
 99+	repo = utils.SanitizeRepo(repo) + ".git"
100 	r := &Repo{path: filepath.Join(fb.reposPath(), repo), root: fb.reposPath()}
101 	return r.Description()
102 }
103@@ -501,7 +491,7 @@ func (fb *FileBackend) IsAdmin(pk gossh.PublicKey) bool {
104 //
105 // It implements backend.Backend.
106 func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, repo string) bool {
107-	repo = sanatizeRepo(repo) + ".git"
108+	repo = utils.SanitizeRepo(repo) + ".git"
109 	_, err := os.Stat(fb.collabsPath(repo))
110 	if err != nil {
111 		return false
112@@ -532,7 +522,7 @@ func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, repo string) bool {
113 //
114 // It implements backend.Backend.
115 func (fb *FileBackend) IsPrivate(repo string) bool {
116-	repo = sanatizeRepo(repo) + ".git"
117+	repo = utils.SanitizeRepo(repo) + ".git"
118 	r := &Repo{path: filepath.Join(fb.reposPath(), repo), root: fb.reposPath()}
119 	return r.IsPrivate()
120 }
121@@ -569,7 +559,7 @@ func (fb *FileBackend) SetAnonAccess(level backend.AccessLevel) error {
122 //
123 // It implements backend.Backend.
124 func (fb *FileBackend) SetDescription(repo string, desc string) error {
125-	repo = sanatizeRepo(repo) + ".git"
126+	repo = utils.SanitizeRepo(repo) + ".git"
127diff --git a/server/backend/noop/noop.go b/server/backend/noop/noop.go
128index b91ffbee395e9daca5a465a61175d33a90c119e4..db7324bc0b60fe7fa006b70d1d530e94358446af 100644
129--- a/server/backend/noop/noop.go
130+++ b/server/backend/noop/noop.go
131@@ -14,18 +14,11 @@ var ErrNotImpl = fmt.Errorf("not implemented")
132 
133 var _ backend.Backend = (*Noop)(nil)
134 
135-var _ backend.AccessMethod = (*Noop)(nil)
136-
137 // Noop is a backend that does nothing. It's used for testing.
138 type Noop struct {
139 	Port string
140 }
141 
142-// RepositoryStorePath implements backend.Backend
143-func (*Noop) RepositoryStorePath() string {
144-	return ""
145-}
146-
147 // Admins implements backend.Backend
148 func (*Noop) Admins() ([]string, error) {
149 	return nil, nil
150diff --git a/server/backend/repo.go b/server/backend/repo.go
151index 6cf46c807e7ca42bb0a0deeb32e1a786715a0e8a..05d2de240656cca7749dcec0afb5bdd5d4bfd8e8 100644
152--- a/server/backend/repo.go
153+++ b/server/backend/repo.go
154@@ -11,8 +11,6 @@ type RepositoryStore interface {
155 	Repository(repo string) (Repository, error)
156 	// Repositories returns a list of all repositories.
157 	Repositories() ([]Repository, error)
158-	// RepositoryStorePath returns the path to the repository store.
159-	RepositoryStorePath() string
160 	// CreateRepository creates a new repository.
161 	CreateRepository(name string, private bool) (Repository, error)
162 	// DeleteRepository deletes a repository.
163@@ -35,6 +33,8 @@ type RepositoryMetadata interface {
164 
165 // RepositoryAccess is an interface for managing repository access.
166 type RepositoryAccess interface {
167+	// AccessLevel returns the access level for the given repository and key.
168+	AccessLevel(repo string, pk ssh.PublicKey) AccessLevel
169 	// IsCollaborator returns true if the authorized key is a collaborator on the repository.
170 	IsCollaborator(pk ssh.PublicKey, repo string) bool
171 	// AddCollaborator adds the authorized key as a collaborator on the repository.
172diff --git a/server/cmd/cmd.go b/server/cmd/cmd.go
173index 44dd4b67153b085ff990dcf6eba4c9f9ec36329f..e32836abb31be39b40829fddcb9f8082aaa8b60e 100644
174--- a/server/cmd/cmd.go
175+++ b/server/cmd/cmd.go
176@@ -3,10 +3,10 @@ package cmd
177 import (
178 	"context"
179 	"fmt"
180-	"strings"
181 
182 	"github.com/charmbracelet/soft-serve/server/backend"
183 	"github.com/charmbracelet/soft-serve/server/config"
184+	"github.com/charmbracelet/soft-serve/server/utils"
185 	"github.com/charmbracelet/ssh"
186 	"github.com/charmbracelet/wish"
187 	"github.com/spf13/cobra"
188@@ -75,8 +75,8 @@ func checkIfReadable(cmd *cobra.Command, args []string) error {
189 		repo = args[0]
190 	}
191 	cfg, s := fromContext(cmd)
192-	rn := strings.TrimSuffix(repo, ".git")
193-	auth := cfg.Access.AccessLevel(rn, s.PublicKey())
194+	rn := utils.SanitizeRepo(repo)
195+	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
196 	if auth < backend.ReadOnlyAccess {
197 		return ErrUnauthorized
198 	}
199@@ -97,8 +97,8 @@ func checkIfCollab(cmd *cobra.Command, args []string) error {
200 		repo = args[0]
201 	}
202 	cfg, s := fromContext(cmd)
203-	rn := strings.TrimSuffix(repo, ".git")
204-	auth := cfg.Access.AccessLevel(rn, s.PublicKey())
205+	rn := utils.SanitizeRepo(repo)
206+	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
207 	if auth < backend.ReadWriteAccess {
208 		return ErrUnauthorized
209 	}
210diff --git a/server/cmd/list.go b/server/cmd/list.go
211index 0bec7fd198aeca000b5ed3a02c82d7e31b134f1e..86f95d7770087a404814c5c23313a5ec03ac61bc 100644
212--- a/server/cmd/list.go
213+++ b/server/cmd/list.go
214@@ -13,21 +13,21 @@ import (
215 // listCommand returns a command that list file or directory at path.
216 func listCommand() *cobra.Command {
217 	listCmd := &cobra.Command{
218-		Use:               "list PATH",
219-		Aliases:           []string{"ls"},
220-		Short:             "List files at repository.",
221-		Args:              cobra.RangeArgs(0, 1),
222-		PersistentPreRunE: checkIfReadable,
223+		Use:     "list PATH",
224+		Aliases: []string{"ls"},
225+		Short:   "List files at repository.",
226+		Args:    cobra.RangeArgs(0, 1),
227 		RunE: func(cmd *cobra.Command, args []string) error {
228 			cfg, s := fromContext(cmd)
229 			rn := ""
230 			path := ""
231 			ps := []string{}
232 			if len(args) > 0 {
233+				// FIXME: nested repos are not supported.
234 				path = filepath.Clean(args[0])
235 				ps = strings.Split(path, "/")
236 				rn = strings.TrimSuffix(ps[0], ".git")
237-				auth := cfg.Access.AccessLevel(rn, s.PublicKey())
238+				auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
239 				if auth < backend.ReadOnlyAccess {
240 					return ErrUnauthorized
241 				}
242@@ -38,7 +38,7 @@ func listCommand() *cobra.Command {
243 					return err
244 				}
245 				for _, r := range repos {
246-					if cfg.Access.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
247+					if cfg.Backend.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
248 						cmd.Println(r.Name())
249 					}
250 				}
251diff --git a/server/cmd/show.go b/server/cmd/show.go
252index cab515e4f78b2d2c7a8a0a922a2da2ae10302293..14981dc27d95c1ecaa97ec189c3b615e57dbf5b8 100644
253--- a/server/cmd/show.go
254+++ b/server/cmd/show.go
255@@ -33,14 +33,11 @@ func showCommand() *cobra.Command {
256 		Args:              cobra.ExactArgs(1),
257 		PersistentPreRunE: checkIfReadable,
258 		RunE: func(cmd *cobra.Command, args []string) error {
259-			cfg, s := fromContext(cmd)
260+			cfg, _ := fromContext(cmd)
261+			// FIXME: nested repos are not supported.
262 			ps := strings.Split(args[0], "/")
263 			rn := strings.TrimSuffix(ps[0], ".git")
264 			fp := strings.Join(ps[1:], "/")
265-			auth := cfg.Access.AccessLevel(rn, s.PublicKey())
266-			if auth < backend.ReadOnlyAccess {
267-				return ErrUnauthorized
268-			}
269 			var repo backend.Repository
270 			repoExists := false
271 			repos, err := cfg.Backend.Repositories()
272diff --git a/server/config/config.go b/server/config/config.go
273index e005cdea7a879bbe6711a0f98d785f48465dad8a..8595fe48a599007d4425bfcfcd5ea1e75519309a 100644
274--- a/server/config/config.go
275+++ b/server/config/config.go
276@@ -6,7 +6,6 @@ import (
277 	"github.com/caarlos0/env/v6"
278 	"github.com/charmbracelet/log"
279 	"github.com/charmbracelet/soft-serve/server/backend"
280-	"github.com/charmbracelet/soft-serve/server/backend/file"
281 )
282 
283 // SSHConfig is the configuration for the SSH server.
284@@ -20,6 +19,9 @@ type SSHConfig struct {
285 	// KeyPath is the path to the SSH server's private key.
286 	KeyPath string `env:"KEY_PATH"`
287 
288+	// InternalKeyPath is the path to the SSH server's internal private key.
289+	InternalKeyPath string `env:"INTERNAL_KEY_PATH"`
290+
291 	// MaxTimeout is the maximum number of seconds a connection can take.
292 	MaxTimeout int `env:"MAX_TIMEOUT" envDefault:"0"`
293 
294@@ -73,9 +75,6 @@ type Config struct {
295 
296 	// Backend is the Git backend to use.
297 	Backend backend.Backend
298-
299-	// Access is the access control backend to use.
300-	Access backend.AccessMethod
301 }
302 
303 // DefaultConfig returns a Config with the values populated with the defaults
304@@ -90,13 +89,10 @@ func DefaultConfig() *Config {
305 	if cfg.SSH.KeyPath == "" {
306 		cfg.SSH.KeyPath = filepath.Join(cfg.DataPath, "ssh", "soft_serve")
307 	}
308-	fb, err := file.NewFileBackend(cfg.DataPath)
309-	if err != nil {
310-		log.Fatal(err)
311+	if cfg.SSH.InternalKeyPath == "" {
312+		cfg.SSH.InternalKeyPath = filepath.Join(cfg.DataPath, "ssh", "soft_serve_internal")
313 	}
314-	// Add the initial admin keys to the list of admins.
315-	fb.AdditionalAdmins = cfg.InitialAdminKeys
316-	return cfg.WithBackend(fb).WithAccessMethod(fb)
317+	return cfg
318 }
319 
320 // WithBackend sets the backend for the configuration.
321@@ -104,9 +100,3 @@ func (c *Config) WithBackend(backend backend.Backend) *Config {
322 	c.Backend = backend
323 	return c
324 }
325-
326-// WithAccessMethod sets the access control method for the configuration.
327-func (c *Config) WithAccessMethod(access backend.AccessMethod) *Config {
328-	c.Access = access
329-	return c
330-}
331diff --git a/server/daemon.go b/server/daemon.go
332index 789f1e9bd39b2adc7fd4ce7152d3b1844e2f4195..86ad93e4167f908198a177fd176b9c79079db47d 100644
333--- a/server/daemon.go
334+++ b/server/daemon.go
335@@ -12,6 +12,7 @@ import (
336 
337 	"github.com/charmbracelet/soft-serve/server/backend"
338 	"github.com/charmbracelet/soft-serve/server/config"
339+	"github.com/charmbracelet/soft-serve/server/utils"
340 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
341 )
342 
343@@ -201,19 +202,19 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
344 			return
345 		}
346 
347-		name := sanitizeRepoName(string(opts[0]))
348+		name := utils.SanitizeRepo(string(opts[0]))
349 		logger.Debugf("git: connect %s %s %s", c.RemoteAddr(), cmd, name)
350 		defer logger.Debugf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, name)
351 		// git bare repositories should end in ".git"
352 		// https://git-scm.com/docs/gitrepository-layout
353 		repo := name + ".git"
354-		reposDir := d.cfg.Backend.RepositoryStorePath()
355+		reposDir := filepath.Join(d.cfg.DataPath, "repos")
356 		if err := ensureWithin(reposDir, repo); err != nil {
357 			fatal(c, err)
358 			return
359 		}
360 
361-		auth := d.cfg.Access.AccessLevel(name, nil)
362+		auth := d.cfg.Backend.AccessLevel(name, nil)
363 		if auth < backend.ReadOnlyAccess {
364 			fatal(c, ErrNotAuthed)
365 			return
366diff --git a/server/daemon_test.go b/server/daemon_test.go
367index e2e059e42cd6553cf5b707a386061204a1852b98..220624000d8cd733a1686f0bcb38214376059db3 100644
368--- a/server/daemon_test.go
369+++ b/server/daemon_test.go
370@@ -8,10 +8,12 @@ import (
371 	"log"
372 	"net"
373 	"os"
374+	"path/filepath"
375 	"strings"
376 	"testing"
377 	"time"
378 
379+	"github.com/charmbracelet/soft-serve/server/backend/file"
380 	"github.com/charmbracelet/soft-serve/server/config"
381 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
382 )
383@@ -29,7 +31,11 @@ func TestMain(m *testing.M) {
384 	os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100")
385 	os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "1")
386 	os.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", randomPort()))
387-	cfg := config.DefaultConfig()
388+	fb, err := file.NewFileBackend(filepath.Join(tmp, "repos"))
389+	if err != nil {
390+		log.Fatal(err)
391+	}
392+	cfg := config.DefaultConfig().WithBackend(fb).WithAccessMethod(fb)
393 	d, err := NewGitDaemon(cfg)
394 	if err != nil {
395 		log.Fatal(err)
396diff --git a/server/http.go b/server/http.go
397index fdc58c551f394e920852a1fe47cb337501b956ef..5c7d168840031de3122b15441728c77e4fd11562 100644
398--- a/server/http.go
399+++ b/server/http.go
400@@ -13,6 +13,7 @@ import (
401 
402 	"github.com/charmbracelet/soft-serve/server/backend"
403 	"github.com/charmbracelet/soft-serve/server/config"
404+	"github.com/charmbracelet/soft-serve/server/utils"
405 	"github.com/dustin/go-humanize"
406 	"goji.io"
407 	"goji.io/pat"
408@@ -68,7 +69,7 @@ func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
409 	mux := goji.NewMux()
410 	s := &HTTPServer{
411 		cfg:        cfg,
412-		dirHandler: http.FileServer(http.Dir(cfg.Backend.RepositoryStorePath())),
413+		dirHandler: http.FileServer(http.Dir(filepath.Join(cfg.DataPath, "repos"))),
414 		server: &http.Server{
415 			Addr:              cfg.HTTP.ListenAddr,
416 			Handler:           mux,
417@@ -114,7 +115,7 @@ Redirecting to docs at <a href="https://godoc.org/{{.ImportRoot}}/{{.Repo}}">god
418 
419 func (s *HTTPServer) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
420 	repo := pat.Param(r, "repo")
421-	repo = sanitizeRepoName(repo)
422+	repo = utils.SanitizeRepo(repo)
423 
424 	// Only respond to go-get requests
425 	if r.URL.Query().Get("go-get") != "1" {
426@@ -122,7 +123,7 @@ func (s *HTTPServer) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
427 		return
428 	}
429 
430-	access := s.cfg.Access.AccessLevel(repo, nil)
431+	access := s.cfg.Backend.AccessLevel(repo, nil)
432 	if access < backend.ReadOnlyAccess {
433 		http.NotFound(w, r)
434 		return
435@@ -149,16 +150,16 @@ func (s *HTTPServer) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
436 
437 func (s *HTTPServer) dumbGitHandler(w http.ResponseWriter, r *http.Request) {
438 	repo := pat.Param(r, "repo")
439-	repo = sanitizeRepoName(repo) + ".git"
440+	repo = utils.SanitizeRepo(repo) + ".git"
441 
442-	access := s.cfg.Access.AccessLevel(repo, nil)
443+	access := s.cfg.Backend.AccessLevel(repo, nil)
444 	if access < backend.ReadOnlyAccess || !s.cfg.Backend.AllowKeyless() {
445 		httpStatusError(w, http.StatusUnauthorized)
446 		return
447 	}
448 
449 	path := pattern.Path(r.Context())
450-	stat, err := os.Stat(filepath.Join(s.cfg.Backend.RepositoryStorePath(), repo, path))
451+	stat, err := os.Stat(filepath.Join(s.cfg.DataPath, "repos", repo, path))
452 	// Restrict access to files
453 	if err != nil || stat.IsDir() {
454 		http.NotFound(w, r)
455diff --git a/server/server.go b/server/server.go
456index f1c703af2a1e195c530a13a87db7d369e052b22a..5cfa6192a967f08123e8334ce80a4b98743df35e 100644
457--- a/server/server.go
458+++ b/server/server.go
459@@ -7,6 +7,7 @@ import (
460 	"github.com/charmbracelet/log"
461 
462 	"github.com/charmbracelet/soft-serve/server/backend"
463+	"github.com/charmbracelet/soft-serve/server/backend/file"
464 	"github.com/charmbracelet/soft-serve/server/config"
465 	"github.com/charmbracelet/ssh"
466 	"golang.org/x/sync/errgroup"
467@@ -23,7 +24,6 @@ type Server struct {
468 	HTTPServer *HTTPServer
469 	Config     *config.Config
470 	Backend    backend.Backend
471-	Access     backend.AccessMethod
472 }
473 
474 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
475@@ -33,10 +33,19 @@ type Server struct {
476 // publicly writable until configured otherwise by cloning the `config` repo.
477 func NewServer(cfg *config.Config) (*Server, error) {
478 	var err error
479+	if cfg.Backend == nil {
480+		fb, err := file.NewFileBackend(cfg.DataPath)
481+		if err != nil {
482+			logger.Fatal(err)
483+		}
484+		// Add the initial admin keys to the list of admins.
485+		fb.AdditionalAdmins = cfg.InitialAdminKeys
486+		cfg = cfg.WithBackend(fb)
487+	}
488+
489 	srv := &Server{
490 		Config:  cfg,
491 		Backend: cfg.Backend,
492-		Access:  cfg.Access,
493 	}
494 	srv.SSHServer, err = NewSSHServer(cfg)
495 	if err != nil {
496diff --git a/server/session.go b/server/session.go
497index 56bc02ed88ef87d9aa1474dcc99435627e0f4b20..03df433ddb530e753e083af6c55f4c4f96d9c044 100644
498--- a/server/session.go
499+++ b/server/session.go
500@@ -26,7 +26,7 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
501 		initialRepo := ""
502 		if len(cmd) == 1 {
503 			initialRepo = cmd[0]
504-			auth := cfg.Access.AccessLevel(initialRepo, s.PublicKey())
505+			auth := cfg.Backend.AccessLevel(initialRepo, s.PublicKey())
506 			if auth < backend.ReadOnlyAccess {
507 				wish.Fatalln(s, cm.ErrUnauthorized)
508 				return nil
509diff --git a/server/session_test.go b/server/session_test.go
510index bd81a2320ba8fa14c2f5712b4154a8e6aa98ed76..a120321a266005fdf6d98f6504dde1523e5ffc4b 100644
511--- a/server/session_test.go
512+++ b/server/session_test.go
513@@ -3,10 +3,13 @@ package server
514 import (
515 	"errors"
516 	"fmt"
517+	"log"
518 	"os"
519+	"path/filepath"
520 	"testing"
521 	"time"
522 
523+	"github.com/charmbracelet/soft-serve/server/backend/file"
524 	"github.com/charmbracelet/soft-serve/server/config"
525 	"github.com/charmbracelet/ssh"
526 	bm "github.com/charmbracelet/wish/bubbletea"
527@@ -49,7 +52,11 @@ func setup(tb testing.TB) *gossh.Session {
528 		is.NoErr(os.Unsetenv("SOFT_SERVE_SSH_LISTEN_ADDR"))
529 		is.NoErr(os.RemoveAll(dp))
530 	})
531-	cfg := config.DefaultConfig()
532+	fb, err := file.NewFileBackend(filepath.Join(dp, "repos"))
533+	if err != nil {
534+		log.Fatal(err)
535+	}
536+	cfg := config.DefaultConfig().WithBackend(fb).WithAccessMethod(fb)
537 	return testsession.New(tb, &ssh.Server{
538 		Handler: bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256)(func(s ssh.Session) {
539 			_, _, active := s.Pty()
540diff --git a/server/ssh.go b/server/ssh.go
541index 25dcdaed86d01b6e9c3d18263f27bab7037aeafd..61e8f7f54c7072866f7927b583375b6cfb11802b 100644
542--- a/server/ssh.go
543+++ b/server/ssh.go
544@@ -12,6 +12,7 @@ import (
545 	"github.com/charmbracelet/soft-serve/server/backend"
546 	cm "github.com/charmbracelet/soft-serve/server/cmd"
547 	"github.com/charmbracelet/soft-serve/server/config"
548+	"github.com/charmbracelet/soft-serve/server/utils"
549 	"github.com/charmbracelet/ssh"
550 	"github.com/charmbracelet/wish"
551 	bm "github.com/charmbracelet/wish/bubbletea"
552@@ -88,7 +89,7 @@ func (s *SSHServer) Shutdown(ctx context.Context) error {
553 
554 // PublicKeyAuthHandler handles public key authentication.
555 func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
556-	return s.cfg.Access.AccessLevel("", pk) > backend.NoAccess
557+	return s.cfg.Backend.AccessLevel("", pk) > backend.NoAccess
558 }
559 
560 // KeyboardInteractiveHandler handles keyboard interactive authentication.
561@@ -109,14 +110,14 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
562 				if len(cmd) >= 2 && strings.HasPrefix(cmd[0], "git") {
563 					gc := cmd[0]
564 					// repo should be in the form of "repo.git"
565-					name := sanitizeRepoName(cmd[1])
566+					name := utils.SanitizeRepo(cmd[1])
567 					pk := s.PublicKey()
568-					access := cfg.Access.AccessLevel(name, pk)
569+					access := cfg.Backend.AccessLevel(name, pk)
570 					// git bare repositories should end in ".git"
571 					// https://git-scm.com/docs/gitrepository-layout
572 					repo := name + ".git"
573 
574-					reposDir := cfg.Backend.RepositoryStorePath()
575+					reposDir := filepath.Join(cfg.DataPath, "repos")
576 					if err := ensureWithin(reposDir, repo); err != nil {
577 						sshFatal(s, err)
578 						return
579@@ -168,10 +169,3 @@ func sshFatal(s ssh.Session, v ...interface{}) {
580 	WritePktline(s, v...)
581 	s.Exit(1) // nolint: errcheck
582 }
583-
584-func sanitizeRepoName(repo string) string {
585-	repo = strings.TrimPrefix(repo, "/")
586-	repo = filepath.Clean(repo)
587-	repo = strings.TrimSuffix(repo, ".git")
588-	return repo
589-}
590diff --git a/server/utils/utils.go b/server/utils/utils.go
591new file mode 100644
592index 0000000000000000000000000000000000000000..3f2fe5dabe16b0daa646a56d1bf3e01c0b5572cb
593--- /dev/null
594+++ b/server/utils/utils.go
595@@ -0,0 +1,14 @@
596+package utils
597+
598+import (
599+	"path/filepath"
600+	"strings"
601+)
602+
603+// SanitizeRepo returns a sanitized version of the given repository name.
604+func SanitizeRepo(repo string) string {
605+	repo = strings.TrimPrefix(repo, "/")
606+	repo = filepath.Clean(repo)
607+	repo = strings.TrimSuffix(repo, ".git")
608+	return repo
609+}