Use new Wish auth WIP

Toby Padilla created

Change summary

config/auth.go             |  20 ++++
config/config.go           | 164 ++++++++++++++++++++++++++++++++++++++++
config/defaults.go         |  42 ++++++++++
go.mod                     |   1 
main.go                    |  47 +++++++---
tui/bubble.go              |  28 +-----
tui/bubbles/repo/bubble.go |   2 
tui/commands.go            |  25 ++---
tui/session.go             |  64 +-------------
9 files changed, 287 insertions(+), 106 deletions(-)

Detailed changes

config/auth.go 🔗

@@ -0,0 +1,20 @@
+package config
+
+import (
+	gm "github.com/charmbracelet/wish/git"
+	"github.com/gliderlabs/ssh"
+)
+
+func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel {
+	// TODO: check yaml for access rules
+	return gm.ReadWriteAccess
+}
+
+func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool {
+	return cfg.AnonReadOnly && cfg.AllowNoKeys
+}
+
+func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
+	// TODO: check yaml for access rules
+	return true
+}

config/config.go 🔗

@@ -0,0 +1,164 @@
+package config
+
+import (
+	"log"
+
+	"gopkg.in/yaml.v2"
+
+	"fmt"
+	"os"
+	"path/filepath"
+	"soft-serve/git"
+
+	"github.com/gliderlabs/ssh"
+	gg "github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
+)
+
+type Config struct {
+	Name         string `yaml:"name"`
+	Host         string `yaml:"host"`
+	Port         int    `yaml:"port"`
+	AnonReadOnly bool   `yaml:"anon-access"`
+	AllowNoKeys  bool   `yaml:"allow-no-keys"`
+	Users        []User `yaml:"users"`
+	Repos        []Repo `yaml:"repos"`
+	Source       *git.RepoSource
+}
+
+type User struct {
+	Name        string   `yaml:"name"`
+	Admin       bool     `yaml:"admin"`
+	PublicKey   string   `yaml:"pk"`
+	CollabRepos []string `yaml:"collab_repos"`
+}
+
+type Repo struct {
+	Name string `yaml:"name"`
+	Repo string `yaml:"repo"`
+	Note string `yaml:"note"`
+}
+
+func NewConfig(host string, port int, anon bool, pk string, rs *git.RepoSource) (*Config, error) {
+	cfg := &Config{}
+	cfg.Host = host
+	cfg.Port = port
+	cfg.AnonReadOnly = anon
+	cfg.Source = rs
+
+	var yamlUsers string
+	var h string
+	if host == "" {
+		h = "localhost"
+	} else {
+		h = host
+	}
+	yamlConfig := fmt.Sprintf(defaultConfig, h, port, anon)
+	if pk != "" {
+		yamlUsers = fmt.Sprintf(hasKeyUserConfig, pk)
+	} else {
+		yamlUsers = defaultUserConfig
+	}
+	yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig)
+	err := cfg.createDefaultConfigRepo(yaml)
+	if err != nil {
+		return nil, err
+	}
+	return cfg, nil
+}
+
+func (cfg *Config) Pushed(repo string, pk ssh.PublicKey) {
+	err := cfg.Reload()
+	if err != nil {
+		log.Printf("error reloading after push: %s", err)
+	}
+}
+
+func (cfg *Config) Reload() error {
+	err := cfg.Source.LoadRepos()
+	if err != nil {
+		return err
+	}
+	cr, err := cfg.Source.GetRepo("config")
+	if err != nil {
+		return err
+	}
+	cs, err := cr.LatestFile("config.yaml")
+	if err != nil {
+		return err
+	}
+	err = yaml.Unmarshal([]byte(cs), cfg)
+	if err != nil {
+		return fmt.Errorf("bad yaml in config.yaml: %s", err)
+	}
+	return nil
+}
+
+func createFile(path string, content string) error {
+	f, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	_, err = f.WriteString(content)
+	if err != nil {
+		return err
+	}
+	return f.Sync()
+}
+
+func (cfg *Config) createDefaultConfigRepo(yaml string) error {
+	cn := "config"
+	rs := cfg.Source
+	err := rs.LoadRepos()
+	if err != nil {
+		return err
+	}
+	_, err = rs.GetRepo(cn)
+	if err == git.ErrMissingRepo {
+		cr, err := rs.InitRepo(cn, false)
+		if err != nil {
+			return err
+		}
+
+		rp := filepath.Join(rs.Path, cn, "README.md")
+		err = createFile(rp, defaultReadme)
+		if err != nil {
+			return err
+		}
+		cp := filepath.Join(rs.Path, cn, "config.yaml")
+		err = createFile(cp, yaml)
+		if err != nil {
+			return err
+		}
+		wt, err := cr.Repository.Worktree()
+		if err != nil {
+			return err
+		}
+		_, err = wt.Add("README.md")
+		if err != nil {
+			return err
+		}
+		_, err = wt.Add("config.yaml")
+		if err != nil {
+			return err
+		}
+		_, err = wt.Commit("Default init", &gg.CommitOptions{
+			All: true,
+			Author: &object.Signature{
+				Name:  "Soft Serve Server",
+				Email: "vt100@charm.sh",
+			},
+		})
+		if err != nil {
+			return err
+		}
+		err = rs.LoadRepos()
+		if err != nil {
+			return err
+		}
+	} else if err != nil {
+		return err
+	}
+	return nil
+}

config/defaults.go 🔗

@@ -0,0 +1,42 @@
+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 defaultConfig = `
+name: Soft Serve
+host: %s
+port: %d
+
+# Set the access level for anonymous users. Options are: read-write, read-only and no-access
+anon-access: %v
+
+# Allow read only even if they don't have private keys, any password will work
+allow-no-keys: false
+
+# Customize repo display in menu
+repos:
+  - name: Home
+	  repo: config
+		note: "Configuration and content repo for this server"`
+
+const hasKeyUserConfig = `
+# Users can read all repos, and push to collab-repos, admin can push to all repos
+users:
+  - name: admin
+	  admin: true
+		public-key: |
+		  %s`
+
+const defaultUserConfig = `
+# users:
+#   - name: admin
+# 	  admin: true
+# 		public-key: |
+# 		  KEY TEXT`
+
+const exampleUserConfig = `
+#  - name: little-buddy
+#	   collab-repos:
+#		   - soft-serve
+#		 public-key: |
+#		   KEY TEXT`

go.mod 🔗

@@ -19,6 +19,7 @@ require (
 	github.com/muesli/reflow v0.3.0
 	github.com/muesli/termenv v0.9.0
 	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
+	gopkg.in/yaml.v2 v2.3.0
 )
 
 require (

main.go 🔗

@@ -3,45 +3,62 @@ package main
 import (
 	"fmt"
 	"log"
+	"soft-serve/config"
+	"soft-serve/git"
 	"soft-serve/tui"
-	"time"
 
 	"github.com/charmbracelet/wish"
 	bm "github.com/charmbracelet/wish/bubbletea"
 	gm "github.com/charmbracelet/wish/git"
 	lm "github.com/charmbracelet/wish/logging"
+	"github.com/gliderlabs/ssh"
 
 	"github.com/meowgorithm/babyenv"
 )
 
-type Config struct {
-	Port         int    `env:"SOFT_SERVE_PORT" default:"23231"`
-	Host         string `env:"SOFT_SERVE_HOST" default:""`
-	KeyPath      string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"`
-	RepoAuth     string `env:"SOFT_SERVE_REPO_KEYS" default:""`
-	RepoAuthFile string `env:"SOFT_SERVE_REPO_KEYS_PATH" default:".ssh/soft_serve_git_authorized_keys"`
-	RepoPath     string `env:"SOFT_SERVE_REPO_PATH" default:".repos"`
+type serverConfig struct {
+	Port     int    `env:"SOFT_SERVE_PORT" default:"23231"`
+	Host     string `env:"SOFT_SERVE_HOST" default:""`
+	InitKey  string `env:"SOFT_SERVE_REPO_KEY" default:""`
+	KeyPath  string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"`
+	RepoPath string `env:"SOFT_SERVE_REPO_PATH" default:".repos"`
 }
 
 func main() {
-	var cfg Config
-	err := babyenv.Parse(&cfg)
+	var scfg serverConfig
+	var cfg *config.Config
+	var err error
+	err = babyenv.Parse(&scfg)
 	if err != nil {
 		log.Fatalln(err)
 	}
+	rs := git.NewRepoSource(scfg.RepoPath)
+	if scfg.InitKey == "" {
+		cfg, err = config.NewConfig(scfg.Host, scfg.Port, true, "", rs)
+		if err != nil {
+			log.Fatalln(err)
+		}
+	} else {
+		cfg, err = config.NewConfig(scfg.Host, scfg.Port, false, scfg.InitKey, rs)
+		if err != nil {
+			log.Fatalln(err)
+		}
+	}
 	s, err := wish.NewServer(
-		wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)),
-		wish.WithHostKeyPath(cfg.KeyPath),
+		ssh.PublicKeyAuth(cfg.PublicKeyHandler),
+		ssh.PasswordAuth(cfg.PasswordHandler),
+		wish.WithAddress(fmt.Sprintf("%s:%d", scfg.Host, scfg.Port)),
+		wish.WithHostKeyPath(scfg.KeyPath),
 		wish.WithMiddlewares(
-			bm.Middleware(tui.SessionHandler(cfg.RepoPath, time.Second*5)),
-			gm.Middleware(cfg.RepoPath, cfg.RepoAuth, cfg.RepoAuthFile),
+			bm.Middleware(tui.SessionHandler(cfg)),
+			gm.MiddlewareWithPushCallback(scfg.RepoPath, cfg, cfg.Pushed),
 			lm.Middleware(),
 		),
 	)
 	if err != nil {
 		log.Fatalln(err)
 	}
-	log.Printf("Starting SSH server on %s:%d\n", cfg.Host, cfg.Port)
+	log.Printf("Starting SSH server on %s:%d\n", scfg.Host, scfg.Port)
 	err = s.ListenAndServe()
 	if err != nil {
 		log.Fatalln(err)

tui/bubble.go 🔗

@@ -2,6 +2,7 @@ package tui
 
 import (
 	"fmt"
+	"soft-serve/config"
 	"soft-serve/git"
 	"soft-serve/tui/bubbles/repo"
 	"soft-serve/tui/bubbles/selection"
@@ -22,13 +23,10 @@ const (
 	quitState
 )
 
-type Config struct {
-	Name         string      `json:"name"`
-	Host         string      `json:"host"`
-	Port         int64       `json:"port"`
-	ShowAllRepos bool        `json:"show_all_repos"`
-	Menu         []MenuEntry `json:"menu"`
-	RepoSource   *git.RepoSource
+type SessionConfig struct {
+	Width       int
+	Height      int
+	InitialRepo string
 }
 
 type MenuEntry struct {
@@ -38,20 +36,13 @@ type MenuEntry struct {
 	bubble *repo.Bubble
 }
 
-type SessionConfig struct {
-	Width       int
-	Height      int
-	InitialRepo string
-}
-
 type Bubble struct {
-	config      *Config
+	config      *config.Config
 	styles      *style.Styles
 	state       sessionState
 	error       string
 	width       int
 	height      int
-	repoSource  *git.RepoSource
 	initialRepo string
 	repoMenu    []MenuEntry
 	repos       []*git.Repo
@@ -60,17 +51,12 @@ type Bubble struct {
 	repoSelect  *selection.Bubble
 }
 
-func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
-	var repoSource *git.RepoSource = nil
-	if cfg != nil {
-		repoSource = cfg.RepoSource
-	}
+func NewBubble(cfg *config.Config, sCfg *SessionConfig) *Bubble {
 	b := &Bubble{
 		config:      cfg,
 		styles:      style.DefaultStyles(),
 		width:       sCfg.Width,
 		height:      sCfg.Height,
-		repoSource:  repoSource,
 		repoMenu:    make([]MenuEntry, 0),
 		boxes:       make([]tea.Model, 2),
 		initialRepo: sCfg.InitialRepo,

tui/bubbles/repo/bubble.go 🔗

@@ -46,7 +46,7 @@ type Bubble struct {
 	// solution would be to (rename and) move this Bubble into the parent
 	// package.
 	Host string
-	Port int64
+	Port int
 }
 
 func NewBubble(rs *git.RepoSource, name string, styles *style.Styles, width, wm, height, hm int, tmp interface{}) *Bubble {

tui/commands.go 🔗

@@ -3,6 +3,7 @@ package tui
 import (
 	"fmt"
 	"log"
+	"soft-serve/config"
 	"soft-serve/tui/bubbles/repo"
 	"soft-serve/tui/bubbles/selection"
 	"time"
@@ -19,29 +20,27 @@ func (e errMsg) Error() string {
 }
 
 func (b *Bubble) setupCmd() tea.Msg {
-	if b.config == nil || b.config.RepoSource == nil {
+	if b.config == nil || b.config.Source == nil {
 		return errMsg{err: fmt.Errorf("config not set")}
 	}
 	ct := time.Now()
 	lipgloss.SetColorProfile(termenv.ANSI256)
-	b.repos = b.repoSource.AllRepos()
-	mes := append([]MenuEntry{}, b.config.Menu...)
+	b.repos = b.config.Source.AllRepos()
+	mes := append([]MenuEntry{}, b.repoMenu...)
 	rs := make([]string, 0)
-	if b.config.ShowAllRepos {
-	OUTER:
-		for _, r := range b.repos {
-			for _, me := range mes {
-				if r.Name == me.Repo {
-					continue OUTER
-				}
+OUTER:
+	for _, r := range b.repos {
+		for _, me := range mes {
+			if r.Name == me.Repo {
+				continue OUTER
 			}
-			mes = append(mes, MenuEntry{Name: r.Name, Repo: r.Name})
 		}
+		mes = append(mes, MenuEntry{Name: r.Name, Repo: r.Name})
 	}
 	if len(mes) == 0 {
 		return errMsg{fmt.Errorf("no repos found")}
 	}
-	var tmplConfig *Config
+	var tmplConfig *config.Config
 	for _, me := range mes {
 		if me.Repo == "config" {
 			tmplConfig = b.config
@@ -53,7 +52,7 @@ func (b *Bubble) setupCmd() tea.Msg {
 			lipgloss.Height(b.footerView()) +
 			b.styles.RepoBody.GetVerticalFrameSize() +
 			b.styles.App.GetVerticalMargins()
-		rb := repo.NewBubble(b.repoSource, me.Repo, b.styles, width, boxLeftWidth, b.height, heightMargin, tmplConfig)
+		rb := repo.NewBubble(b.config.Source, me.Repo, b.styles, width, boxLeftWidth, b.height, heightMargin, tmplConfig)
 		rb.Host = b.config.Host
 		rb.Port = b.config.Port
 		initCmd := rb.Init()

tui/session.go 🔗

@@ -1,52 +1,22 @@
 package tui
 
 import (
-	"encoding/json"
 	"fmt"
-	"log"
-	"soft-serve/git"
-	"time"
+	"soft-serve/config"
 
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/gliderlabs/ssh"
 )
 
-func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
-	rs := git.NewRepoSource(reposPath)
-	// createDefaultConfigRepo runs rs.LoadRepos()
-	err := createDefaultConfigRepo(rs)
-	if err != nil {
-		if err != nil {
-			log.Fatalf("cannot create config repo: %s", err)
-		}
-	}
-	appCfg, err := loadConfig(rs)
-	if err != nil {
-		log.Printf("cannot load config: %s", err)
-	}
-
+func SessionHandler(cfg *config.Config) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
 	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
 		cmd := s.Command()
-		// reload repos and config on git push
-		if len(cmd) > 0 && cmd[0] == "git-receive-pack" {
-			ct := time.Now()
-			err := rs.LoadRepos()
-			if err != nil {
-				log.Printf("cannot load repos: %s", err)
-			}
-			cfg, err := loadConfig(rs)
-			if err != nil {
-				log.Printf("cannot load config: %s", err)
-			}
-			appCfg = cfg
-			log.Printf("Repo bubble loaded in %s", time.Since(ct))
-		}
-		cfg := &SessionConfig{}
+		scfg := &SessionConfig{}
 		switch len(cmd) {
 		case 0:
-			cfg.InitialRepo = ""
+			scfg.InitialRepo = ""
 		case 1:
-			cfg.InitialRepo = cmd[0]
+			scfg.InitialRepo = cmd[0]
 		default:
 			return nil, nil
 		}
@@ -55,26 +25,8 @@ func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session)
 			fmt.Println("not active")
 			return nil, nil
 		}
-		cfg.Width = pty.Window.Width
-		cfg.Height = pty.Window.Height
-		return NewBubble(appCfg, cfg), []tea.ProgramOption{tea.WithAltScreen()}
-	}
-}
-
-func loadConfig(rs *git.RepoSource) (*Config, error) {
-	cfg := &Config{}
-	cfg.RepoSource = rs
-	cr, err := rs.GetRepo("config")
-	if err != nil {
-		return nil, err
-	}
-	cs, err := cr.LatestFile("config.json")
-	if err != nil {
-		return nil, err
-	}
-	err = json.Unmarshal([]byte(cs), cfg)
-	if err != nil {
-		return nil, fmt.Errorf("bad json in config.json: %s", err)
+		scfg.Width = pty.Window.Width
+		scfg.Height = pty.Window.Height
+		return NewBubble(cfg, scfg), []tea.ProgramOption{tea.WithAltScreen()}
 	}
-	return cfg, nil
 }