1package cmd23import (4 "fmt"5 "net/url"6 "strings"7 "text/template"8 "unicode"910 "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)1920var 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}2829const (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}}3435Aliases:36 {{.NameAndAliases}}{{end}}{{if .HasExample}}3738Examples:39{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}4041Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}42 {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}4344{{.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}}4647Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}48 {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}4950Flags:51{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}5253Global Flags:54{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}5556Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}57 {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}5859Use "{{.SSHCommand}}{{.CommandPath}} [command] --help" for more information about a command.{{end}}60`61)6263// UsageFunc is a function that can be used as a cobra.Command's64// 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 }7576 sshCmd := "ssh"77 if port != "" && port != "22" {78 sshCmd += " -p " + port79 }8081 sshCmd += " " + hostname82 t := template.New("usage")83 t.Funcs(templateFuncs)84 template.Must(t.Parse(c.UsageTemplate()))85 return t.Execute(c.OutOrStderr(), struct {86 *cobra.Command87 SSHCommand string88 }{89 Command: c,90 SSHCommand: sshCmd,91 })92}9394func trimRightSpace(s string) string {95 return strings.TrimRightFunc(s, unicode.IsSpace)96}9798// 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}103104// 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}111112func checkIfReadable(cmd *cobra.Command, args []string) error {113 var repo string114 if len(args) > 0 {115 repo = args[0]116 }117118 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.ErrRepoNotFound125 }126 return nil127}128129// IsPublicKeyAdmin returns true if the given public key is an admin key from130// 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 true135 }136 }137 return false138}139140func checkIfAdmin(cmd *cobra.Command, args []string) error {141 var repo string142 if len(args) > 0 {143 repo = args[0]144 }145146 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 nil153 }154155 user := proto.UserFromContext(ctx)156 if user == nil {157 return proto.ErrUnauthorized158 }159160 if user.IsAdmin() {161 return nil162 }163164 auth := be.AccessLevelForUser(cmd.Context(), rn, user)165 if auth >= access.AdminAccess {166 return nil167 }168169 return proto.ErrUnauthorized170}171172func checkIfCollab(cmd *cobra.Command, args []string) error {173 var repo string174 if len(args) > 0 {175 repo = args[0]176 }177178 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.ErrUnauthorized185 }186 return nil187}188189func checkIfReadableAndCollab(cmd *cobra.Command, args []string) error {190 if err := checkIfReadable(cmd, args); err != nil {191 return err192 }193 if err := checkIfCollab(cmd, args); err != nil {194 return err195 }196 return nil197}