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}