Allow repos to be private / admin only

Toby Padilla created

Change summary

config/config.go   | 16 +++++++++++++---
config/defaults.go |  3 ++-
config/git.go      |  8 ++++++--
tui/bubble.go      |  4 ++++
tui/commands.go    | 27 +++++++++++++++------------
tui/session.go     |  2 +-
6 files changed, 41 insertions(+), 19 deletions(-)

Detailed changes

config/config.go 🔗

@@ -31,9 +31,10 @@ type User struct {
 }
 
 type Repo struct {
-	Name string `yaml:"name"`
-	Repo string `yaml:"repo"`
-	Note string `yaml:"note"`
+	Name    string `yaml:"name"`
+	Repo    string `yaml:"repo"`
+	Note    string `yaml:"note"`
+	Private bool   `yaml:"private"`
 }
 
 func NewConfig(host string, port int, pk string, rs *git.RepoSource) (*Config, error) {
@@ -155,3 +156,12 @@ func (cfg *Config) createDefaultConfigRepo(yaml string) error {
 	}
 	return cfg.reload()
 }
+
+func (cfg *Config) isPrivate(repo string) bool {
+	for _, r := range cfg.Repos {
+		if r.Repo == repo {
+			return r.Private
+		}
+	}
+	return false
+}

config/defaults.go 🔗

@@ -1,6 +1,6 @@
 package config
 
-const defaultReadme = "# Soft Serve\n\n Welcome! You can configure your Soft Serve server by cloning this repo and pushing changes.\n\n## Repos\n\n{{ range .Repos }}* {{ .Name }}{{ if .Note }} - {{ .Note }} {{ end }}\n  - `git clone ssh://{{$.Host}}:{{$.Port}}/{{.Repo}}`\n{{ end }}"
+const defaultReadme = "# Soft Serve\n\n Welcome! You can configure your Soft Serve server by cloning this repo and pushing changes.\n\n```\ngit clone ssh://{{.Host}}:{{.Port}}/config\n```"
 
 const defaultConfig = `name: Soft Serve
 host: %s
@@ -16,6 +16,7 @@ allow-no-keys: false
 repos:
   - name: Home
     repo: config
+    private: true
     note: "Configuration and content repo for this server"`
 
 const hasKeyUserConfig = `

config/git.go 🔗

@@ -35,6 +35,10 @@ func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
 }
 
 func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel {
+	private := cfg.isPrivate(repo)
+	if repo == "config" {
+		private = true
+	}
 	for _, u := range cfg.Users {
 		apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(u.PublicKey))
 		if err != nil {
@@ -50,12 +54,12 @@ func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel {
 					return gm.ReadWriteAccess
 				}
 			}
-			if repo != "config" {
+			if !private {
 				return gm.ReadOnlyAccess
 			}
 		}
 	}
-	if repo == "config" && (cfg.AnonAccess != "read-write") {
+	if private && (cfg.AnonAccess != "read-write") {
 		return gm.NoAccess
 	}
 	switch cfg.AnonAccess {

tui/bubble.go 🔗

@@ -11,6 +11,7 @@ import (
 	"github.com/charmbracelet/soft/tui/bubbles/repo"
 	"github.com/charmbracelet/soft/tui/bubbles/selection"
 	"github.com/charmbracelet/soft/tui/style"
+	"github.com/gliderlabs/ssh"
 )
 
 type sessionState int
@@ -27,6 +28,7 @@ type SessionConfig struct {
 	Width       int
 	Height      int
 	InitialRepo string
+	Session     ssh.Session
 }
 
 type MenuEntry struct {
@@ -49,6 +51,7 @@ type Bubble struct {
 	boxes       []tea.Model
 	activeBox   int
 	repoSelect  *selection.Bubble
+	session     ssh.Session
 }
 
 func NewBubble(cfg *config.Config, sCfg *SessionConfig) *Bubble {
@@ -60,6 +63,7 @@ func NewBubble(cfg *config.Config, sCfg *SessionConfig) *Bubble {
 		repoMenu:    make([]MenuEntry, 0),
 		boxes:       make([]tea.Model, 2),
 		initialRepo: sCfg.InitialRepo,
+		session:     sCfg.Session,
 	}
 	b.state = startState
 	return b

tui/commands.go 🔗

@@ -8,6 +8,7 @@ import (
 	"github.com/charmbracelet/soft/config"
 	br "github.com/charmbracelet/soft/tui/bubbles/repo"
 	"github.com/charmbracelet/soft/tui/bubbles/selection"
+	gm "github.com/charmbracelet/wish/git"
 	"github.com/muesli/termenv"
 )
 
@@ -61,18 +62,20 @@ func (b *Bubble) setupCmd() tea.Msg {
 
 func (b *Bubble) menuEntriesFromSource() ([]MenuEntry, error) {
 	mes := make([]MenuEntry, 0)
-	for _, r := range b.config.Repos {
-		me, err := b.newMenuEntry(r.Name, r.Repo)
-		if err != nil {
-			return nil, err
-		}
-		mes = append(mes, me)
-	}
 	rs := b.config.Source.AllRepos()
 OUTER:
 	for _, r := range rs {
-		for _, me := range mes {
-			if r.Name == me.Repo {
+		acc := b.config.AuthRepo(r.Name, b.session.PublicKey())
+		if acc == gm.NoAccess && r.Name != "config" {
+			continue
+		}
+		for _, cr := range b.config.Repos {
+			if r.Name == cr.Repo {
+				me, err := b.newMenuEntry(cr.Name, cr.Repo)
+				if err != nil {
+					return nil, err
+				}
+				mes = append(mes, me)
 				continue OUTER
 			}
 		}
@@ -87,6 +90,9 @@ OUTER:
 
 func (b *Bubble) newMenuEntry(name string, repo string) (MenuEntry, error) {
 	var tmplConfig *config.Config
+	if repo == "config" {
+		tmplConfig = b.config
+	}
 	me := MenuEntry{Name: name, Repo: repo}
 	width := b.width
 	boxLeftWidth := b.styles.Menu.GetWidth() + b.styles.Menu.GetHorizontalFrameSize()
@@ -95,9 +101,6 @@ func (b *Bubble) newMenuEntry(name string, repo string) (MenuEntry, error) {
 		lipgloss.Height(b.footerView()) +
 		b.styles.RepoBody.GetVerticalFrameSize() +
 		b.styles.App.GetVerticalMargins()
-	if repo == "config" {
-		tmplConfig = b.config
-	}
 	rb := br.NewBubble(
 		b.config.Source,
 		me.Repo,

tui/session.go 🔗

@@ -11,7 +11,7 @@ import (
 func SessionHandler(cfg *config.Config) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
 	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
 		cmd := s.Command()
-		scfg := &SessionConfig{}
+		scfg := &SessionConfig{Session: s}
 		switch len(cmd) {
 		case 0:
 			scfg.InitialRepo = ""