Something went wrong. Try again.
forked from charmbracelet/soft-serve
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257package cmd
import ( "context" "fmt" "net/url" "strings" "text/template" "unicode"
"charm.land/ssh" "github.com/charmbracelet/soft-serve/pkg/access" "github.com/charmbracelet/soft-serve/pkg/backend" "github.com/charmbracelet/soft-serve/pkg/config" "github.com/charmbracelet/soft-serve/pkg/proto" "github.com/charmbracelet/soft-serve/pkg/sshutils" "github.com/charmbracelet/soft-serve/pkg/utils" "github.com/spf13/cobra")
var templateFuncs = template.FuncMap{ "trim": strings.TrimSpace, "trimRightSpace": trimRightSpace, "trimTrailingWhitespaces": trimRightSpace, "rpad": rpad, "gt": cobra.Gt, "eq": cobra.Eq,}
const ( // UsageTemplate is the template used for the help output. UsageTemplate = `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.SSHCommand}}{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.SSHCommand}}{{.CommandPath}} [command] --help" for more information about a command.{{end}}`)
// UsageFunc is a function that can be used as a cobra.Command's// UsageFunc to render the help output.func UsageFunc(c *cobra.Command) error { ctx := c.Context() cfg := config.FromContext(ctx) hostname := "localhost" port := "23231" url, err := url.Parse(cfg.SSH.PublicURL) if err == nil { hostname = url.Hostname() port = url.Port() }
sshCmd := "ssh" if port != "" && port != "22" { sshCmd += " -p " + port }
sshCmd += " " + hostname t := template.New("usage") t.Funcs(templateFuncs) template.Must(t.Parse(c.UsageTemplate())) return t.Execute(c.OutOrStderr(), struct { *cobra.Command SSHCommand string }{ Command: c, SSHCommand: sshCmd, })}
func trimRightSpace(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace)}
// rpad adds padding to the right of a string.func rpad(s string, padding int) string { template := fmt.Sprintf("%%-%ds", padding) return fmt.Sprintf(template, s)}
// CommandName returns the name of the command from the args.func CommandName(args []string) string { if len(args) == 0 { return "" } return args[0]}
// denyReadAs is the one answer for a caller who may not read a repository,// whether it is missing or private: anon for anonymous callers (HTTP 401),// registered for everyone else (HTTP 404).func denyReadAs(anonymous bool, anon, registered error) error { if anonymous { return anon } return registered}
// denyRead is denyReadAs for repo commands. Server admins always get// ErrRepoNotFound since nothing is hidden from them.func denyRead(ctx context.Context) error { anonymous := proto.UserFromContext(ctx) == nil && !isServerAdmin(ctx) return denyReadAs(anonymous, proto.ErrUnauthorized, proto.ErrRepoNotFound)}
// repoContextKey holds the repoStash a gate stored for the command.type repoContextKey struct{}
// repoStash is the repository a gate resolved for a command and the// caller's access level on it. repo is nil only for server admins acting// on a name with no repository behind it.type repoStash struct { name string repo proto.Repository level access.AccessLevel}
// requireRepo gates a repo-scoped command: the caller must be able to read// the repository named by the first argument and hold at least need on it.// Readability is decided before existence, so a caller who cannot read gets// denyRead's answer whether or not the repository exists. Server admins// pass with a nil repository so RunE can act on orphaned rows.func requireRepo(need access.AccessLevel) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() name := repoArg(args) repo, level := readableRepo(ctx, name) if repo == nil && !isServerAdmin(ctx) { return denyRead(ctx) }
cmd.SetContext(context.WithValue(ctx, repoContextKey{}, repoStash{name: name, repo: repo, level: level})) return requireLevel(cmd.Context(), need) }}
// requireLevel checks the stashed access level against need; use it in// setter arms of commands gated with requireRepo.func requireLevel(ctx context.Context, need access.AccessLevel) error { if _, level, _ := repoFromContext(ctx); level < need { return proto.ErrUnauthorized } return nil}
// readableRepo is backend.Readable with the server admin override.func readableRepo(ctx context.Context, name string) (proto.Repository, access.AccessLevel) { be := backend.FromContext(ctx) if isServerAdmin(ctx) { repo, _ := be.Repository(ctx, name) return repo, access.AdminAccess } return be.Readable(ctx, name, proto.UserFromContext(ctx))}
// repoFromContext returns the repository and access level stored by// requireRepo. The error is denyRead's answer when no repository exists.func repoFromContext(ctx context.Context) (proto.Repository, access.AccessLevel, error) { stash, _ := ctx.Value(repoContextKey{}).(repoStash) if stash.repo == nil { return nil, stash.level, denyRead(ctx) } return stash.repo, stash.level, nil}
// IsPublicKeyAdmin returns true if the given public key is an admin key from// the initial_admin_keys config or environment field.func IsPublicKeyAdmin(cfg *config.Config, pk ssh.PublicKey) bool { for _, k := range cfg.AdminKeys() { if sshutils.KeysEqual(pk, k) { return true } } return false}
// isServerAdmin reports whether the caller is a server administrator: either// their public key is one of the configured admin keys, or their account has// the admin flag set.//// This is the single source of truth for "is this caller a server admin".// Every authorization gate defers to it so the definition cannot drift.func isServerAdmin(ctx context.Context) bool { cfg := config.FromContext(ctx) pk := sshutils.PublicKeyFromContext(ctx) if IsPublicKeyAdmin(cfg, pk) { return true }
user := proto.UserFromContext(ctx) return user != nil && user.IsAdmin()}
// repoArg returns the repository name from a repo-scoped command's arguments.// Repo-scoped commands always take the repository as their first argument.func repoArg(args []string) string { if len(args) == 0 { return "" } return utils.SanitizeRepo(args[0])}
// repoAccessLevel returns the caller's access level for the sanitized name.func repoAccessLevel(ctx context.Context, repo string) access.AccessLevel { _, level := readableRepo(ctx, repo) return level}
// checkIfServerAdmin is the authorization gate for global (non-repo-scoped)// commands such as `user` and `settings`. It allows server admins only.//// Unlike checkIfRepoAdmin, it never consults repository access levels, so it// cannot be bypassed by creating a repository whose name matches the command// argument. Attach it to the parent command so that every subcommand,// including ones added later, is gated by default.func checkIfServerAdmin(cmd *cobra.Command, _ []string) error { if !isServerAdmin(cmd.Context()) { return proto.ErrUnauthorized } return nil}
// checkIfRepoCollab gates create and import, which name a repository that// should not exist yet, on write access to that name. It cannot hide// whether the name is taken: with one flat namespace, "already exists" is// the answer either way.func checkIfRepoCollab(cmd *cobra.Command, args []string) error { if repoAccessLevel(cmd.Context(), repoArg(args)) < access.ReadWriteAccess { return proto.ErrUnauthorized } return nil}