cmd.go

  1package cmd
  2
  3import (
  4	"fmt"
  5	"net/url"
  6	"strings"
  7	"text/template"
  8	"unicode"
  9
 10	"github.com/charmbracelet/soft-serve/pkg/access"
 11	"github.com/charmbracelet/soft-serve/pkg/backend"
 12	"github.com/charmbracelet/soft-serve/pkg/config"
 13	"github.com/charmbracelet/soft-serve/pkg/proto"
 14	"github.com/charmbracelet/soft-serve/pkg/sshutils"
 15	"github.com/charmbracelet/soft-serve/pkg/utils"
 16	"github.com/charmbracelet/ssh"
 17	"github.com/spf13/cobra"
 18)
 19
 20var templateFuncs = template.FuncMap{
 21	"trim":                    strings.TrimSpace,
 22	"trimRightSpace":          trimRightSpace,
 23	"trimTrailingWhitespaces": trimRightSpace,
 24	"rpad":                    rpad,
 25	"gt":                      cobra.Gt,
 26	"eq":                      cobra.Eq,
 27}
 28
 29const (
 30	// UsageTemplate is the template used for the help output.
 31	UsageTemplate = `Usage:{{if .Runnable}}
 32  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
 33  {{.SSHCommand}}{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
 34
 35Aliases:
 36  {{.NameAndAliases}}{{end}}{{if .HasExample}}
 37
 38Examples:
 39{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
 40
 41Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
 42  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
 43
 44{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
 45  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
 46
 47Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
 48  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
 49
 50Flags:
 51{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
 52
 53Global Flags:
 54{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
 55
 56Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
 57  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
 58
 59Use "{{.SSHCommand}}{{.CommandPath}} [command] --help" for more information about a command.{{end}}
 60`
 61)
 62
 63// UsageFunc is a function that can be used as a cobra.Command's
 64// UsageFunc to render the help output.
 65func UsageFunc(c *cobra.Command) error {
 66	ctx := c.Context()
 67	cfg := config.FromContext(ctx)
 68	hostname := "localhost"
 69	port := "23231"
 70	url, err := url.Parse(cfg.SSH.PublicURL)
 71	if err == nil {
 72		hostname = url.Hostname()
 73		port = url.Port()
 74	}
 75
 76	sshCmd := "ssh"
 77	if port != "" && port != "22" {
 78		sshCmd += " -p " + port
 79	}
 80
 81	sshCmd += " " + hostname
 82	t := template.New("usage")
 83	t.Funcs(templateFuncs)
 84	template.Must(t.Parse(c.UsageTemplate()))
 85	return t.Execute(c.OutOrStderr(), struct { //nolint:wrapcheck
 86		*cobra.Command
 87		SSHCommand string
 88	}{
 89		Command:    c,
 90		SSHCommand: sshCmd,
 91	})
 92}
 93
 94func trimRightSpace(s string) string {
 95	return strings.TrimRightFunc(s, unicode.IsSpace)
 96}
 97
 98// rpad adds padding to the right of a string.
 99func rpad(s string, padding int) string {
100	template := fmt.Sprintf("%%-%ds", padding)
101	return fmt.Sprintf(template, s)
102}
103
104// CommandName returns the name of the command from the args.
105func CommandName(args []string) string {
106	if len(args) == 0 {
107		return ""
108	}
109	return args[0]
110}
111
112func checkIfReadable(cmd *cobra.Command, args []string) error {
113	var repo string
114	if len(args) > 0 {
115		repo = args[0]
116	}
117
118	ctx := cmd.Context()
119	be := backend.FromContext(ctx)
120	rn := utils.SanitizeRepo(repo)
121	user := proto.UserFromContext(ctx)
122	auth := be.AccessLevelForUser(cmd.Context(), rn, user)
123	if auth < access.ReadOnlyAccess {
124		return proto.ErrRepoNotFound
125	}
126	return nil
127}
128
129// IsPublicKeyAdmin returns true if the given public key is an admin key from
130// the initial_admin_keys config or environment field.
131func IsPublicKeyAdmin(cfg *config.Config, pk ssh.PublicKey) bool {
132	for _, k := range cfg.AdminKeys() {
133		if sshutils.KeysEqual(pk, k) {
134			return true
135		}
136	}
137	return false
138}
139
140func checkIfAdmin(cmd *cobra.Command, args []string) error {
141	var repo string
142	if len(args) > 0 {
143		repo = args[0]
144	}
145
146	ctx := cmd.Context()
147	cfg := config.FromContext(ctx)
148	be := backend.FromContext(ctx)
149	rn := utils.SanitizeRepo(repo)
150	pk := sshutils.PublicKeyFromContext(ctx)
151	if IsPublicKeyAdmin(cfg, pk) {
152		return nil
153	}
154
155	user := proto.UserFromContext(ctx)
156	if user == nil {
157		return proto.ErrUnauthorized
158	}
159
160	if user.IsAdmin() {
161		return nil
162	}
163
164	auth := be.AccessLevelForUser(cmd.Context(), rn, user)
165	if auth >= access.AdminAccess {
166		return nil
167	}
168
169	return proto.ErrUnauthorized
170}
171
172func checkIfCollab(cmd *cobra.Command, args []string) error {
173	var repo string
174	if len(args) > 0 {
175		repo = args[0]
176	}
177
178	ctx := cmd.Context()
179	be := backend.FromContext(ctx)
180	rn := utils.SanitizeRepo(repo)
181	user := proto.UserFromContext(ctx)
182	auth := be.AccessLevelForUser(cmd.Context(), rn, user)
183	if auth < access.ReadWriteAccess {
184		return proto.ErrUnauthorized
185	}
186	return nil
187}
188
189func checkIfReadableAndCollab(cmd *cobra.Command, args []string) error {
190	if err := checkIfReadable(cmd, args); err != nil {
191		return err
192	}
193	if err := checkIfCollab(cmd, args); err != nil {
194		return err
195	}
196	return nil
197}