cmd.go

  1package cmd
  2
  3import (
  4	"context"
  5	"fmt"
  6	"strings"
  7
  8	"github.com/charmbracelet/soft-serve/server/backend"
  9	"github.com/charmbracelet/soft-serve/server/config"
 10	"github.com/charmbracelet/ssh"
 11	"github.com/charmbracelet/wish"
 12	"github.com/spf13/cobra"
 13)
 14
 15// ContextKey is a type that can be used as a key in a context.
 16type ContextKey string
 17
 18// String returns the string representation of the ContextKey.
 19func (c ContextKey) String() string {
 20	return string(c) + "ContextKey"
 21}
 22
 23var (
 24	// ConfigCtxKey is the key for the config in the context.
 25	ConfigCtxKey = ContextKey("config")
 26	// SessionCtxKey is the key for the session in the context.
 27	SessionCtxKey = ContextKey("session")
 28)
 29
 30var (
 31	// ErrUnauthorized is returned when the user is not authorized to perform action.
 32	ErrUnauthorized = fmt.Errorf("Unauthorized")
 33	// ErrRepoNotFound is returned when the repo is not found.
 34	ErrRepoNotFound = fmt.Errorf("Repository not found")
 35	// ErrFileNotFound is returned when the file is not found.
 36	ErrFileNotFound = fmt.Errorf("File not found")
 37)
 38
 39// rootCommand is the root command for the server.
 40func rootCommand() *cobra.Command {
 41	rootCmd := &cobra.Command{
 42		Use:          "soft",
 43		Short:        "Soft Serve is a self-hostable Git server for the command line.",
 44		SilenceUsage: true,
 45	}
 46	// TODO: use command usage template to include hostname and port
 47	rootCmd.CompletionOptions.DisableDefaultCmd = true
 48	rootCmd.AddCommand(
 49		branchCommand(),
 50		createCommand(),
 51		deleteCommand(),
 52		descriptionCommand(),
 53		listCommand(),
 54		privateCommand(),
 55		renameCommand(),
 56		showCommand(),
 57		tagCommand(),
 58	)
 59
 60	return rootCmd
 61}
 62
 63func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
 64	ctx := cmd.Context()
 65	cfg := ctx.Value(ConfigCtxKey).(*config.Config)
 66	s := ctx.Value(SessionCtxKey).(ssh.Session)
 67	return cfg, s
 68}
 69
 70func checkIfReadable(cmd *cobra.Command, args []string) error {
 71	var repo string
 72	if len(args) > 0 {
 73		repo = args[0]
 74	}
 75	cfg, s := fromContext(cmd)
 76	rn := strings.TrimSuffix(repo, ".git")
 77	auth := cfg.Access.AccessLevel(rn, s.PublicKey())
 78	if auth < backend.ReadOnlyAccess {
 79		return ErrUnauthorized
 80	}
 81	return nil
 82}
 83
 84func checkIfAdmin(cmd *cobra.Command, args []string) error {
 85	cfg, s := fromContext(cmd)
 86	if !cfg.Backend.IsAdmin(s.PublicKey()) {
 87		return ErrUnauthorized
 88	}
 89	return nil
 90}
 91
 92func checkIfCollab(cmd *cobra.Command, args []string) error {
 93	var repo string
 94	if len(args) > 0 {
 95		repo = args[0]
 96	}
 97	cfg, s := fromContext(cmd)
 98	rn := strings.TrimSuffix(repo, ".git")
 99	auth := cfg.Access.AccessLevel(rn, s.PublicKey())
100	if auth < backend.ReadWriteAccess {
101		return ErrUnauthorized
102	}
103	return nil
104}
105
106// Middleware is the Soft Serve middleware that handles SSH commands.
107func Middleware(cfg *config.Config) wish.Middleware {
108	return func(sh ssh.Handler) ssh.Handler {
109		return func(s ssh.Session) {
110			func() {
111				_, _, active := s.Pty()
112				if active {
113					return
114				}
115				ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
116				ctx = context.WithValue(ctx, SessionCtxKey, s)
117
118				rootCmd := rootCommand()
119				rootCmd.SetArgs(s.Command())
120				if len(s.Command()) == 0 {
121					// otherwise it'll default to os.Args, which is not what we want.
122					rootCmd.SetArgs([]string{"--help"})
123				}
124				rootCmd.SetIn(s)
125				rootCmd.SetOut(s)
126				rootCmd.CompletionOptions.DisableDefaultCmd = true
127				rootCmd.SetErr(s.Stderr())
128				if err := rootCmd.ExecuteContext(ctx); err != nil {
129					_ = s.Exit(1)
130				}
131			}()
132			sh(s)
133		}
134	}
135}