From 074eada2c8630bfd321216a676c1702f6919a103 Mon Sep 17 00:00:00 2001 From: Toby Padilla Date: Sat, 2 Oct 2021 17:05:55 -0500 Subject: [PATCH] Use new Wish auth WIP --- 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(-) create mode 100644 config/auth.go create mode 100644 config/config.go create mode 100644 config/defaults.go diff --git a/config/auth.go b/config/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..999262c5a07821e8e08a0b3417814c007977ab41 --- /dev/null +++ b/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 +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..b64974d8be1017ab8c78129d1c27dd9fa0275dfe --- /dev/null +++ b/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 +} diff --git a/config/defaults.go b/config/defaults.go new file mode 100644 index 0000000000000000000000000000000000000000..1a7cbb3866df8db37deef9ac6e2bff02f0bc45e0 --- /dev/null +++ b/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` diff --git a/go.mod b/go.mod index 0afb48b770c62697c2bc0f388c6418aca7031f46..c7408bd7120adf343dfb9e4084c5edbbe8d21f2e 100644 --- a/go.mod +++ b/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 ( diff --git a/main.go b/main.go index 71288764ed2e2fd5da0ff9e8b50e20499848de3f..92c1eaaccce8147781f2eca97b717ca0eac3cbe7 100644 --- a/main.go +++ b/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) diff --git a/tui/bubble.go b/tui/bubble.go index e0ebb9ea62b40f0f5ea0d1f8503b6f4e83304e49..400a249ee7c813f3b3b9119c978e5586442e2ecf 100644 --- a/tui/bubble.go +++ b/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, diff --git a/tui/bubbles/repo/bubble.go b/tui/bubbles/repo/bubble.go index aad6a734ea943ac014f7bf8005a9e30b98f1adae..bf2ac0c591c724b58b391fbb32975980163ce3b1 100644 --- a/tui/bubbles/repo/bubble.go +++ b/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 { diff --git a/tui/commands.go b/tui/commands.go index 4f7c6d45259e13c77632a2f4fb6f43b1c4ccb99d..7f777cc2e40db1418837fd27abfeb1fd4be2a8ee 100644 --- a/tui/commands.go +++ b/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() diff --git a/tui/session.go b/tui/session.go index 78ed852cddbea2cceee98d2015aac07b18c58e52..51eedc361eb876aef7534ae193d0b049cf73767a 100644 --- a/tui/session.go +++ b/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 }