+26
-29
guard/guard.go
+26
-29
guard/guard.go
···
16
securejoin "github.com/cyphar/filepath-securejoin"
17
"github.com/urfave/cli/v3"
18
"tangled.org/core/idresolver"
19
-
"tangled.org/core/log"
20
)
21
22
func Command() *cli.Command {
···
55
}
56
57
func Run(ctx context.Context, cmd *cli.Command) error {
58
-
l := log.FromContext(ctx)
59
-
60
incomingUser := cmd.String("user")
61
gitDir := cmd.String("git-dir")
62
logPath := cmd.String("log-path")
63
endpoint := cmd.String("internal-api")
64
motdFile := cmd.String("motd-file")
65
66
logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
67
-
if err != nil {
68
-
l.Error("failed to open log file", "error", err)
69
-
return err
70
-
} else {
71
-
fileHandler := slog.NewJSONHandler(logFile, &slog.HandlerOptions{Level: slog.LevelInfo})
72
-
l = slog.New(fileHandler)
73
}
74
75
var clientIP string
76
if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" {
77
parts := strings.Fields(connInfo)
···
81
}
82
83
if incomingUser == "" {
84
-
l.Error("access denied: no user specified")
85
fmt.Fprintln(os.Stderr, "access denied: no user specified")
86
os.Exit(-1)
87
}
88
89
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
90
91
-
l.Info("connection attempt",
92
"user", incomingUser,
93
"command", sshCommand,
94
"client", clientIP)
95
96
if sshCommand == "" {
97
-
l.Info("access denied: no interactive shells", "user", incomingUser)
98
fmt.Fprintf(os.Stderr, "Hi @%s! You've successfully authenticated.\n", incomingUser)
99
os.Exit(-1)
100
}
101
102
cmdParts := strings.Fields(sshCommand)
103
if len(cmdParts) < 2 {
104
-
l.Error("invalid command format", "command", sshCommand)
105
fmt.Fprintln(os.Stderr, "invalid command format")
106
os.Exit(-1)
107
}
···
113
// any of the above with a leading slash (/)
114
115
components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/")
116
-
l.Info("command components", "components", components)
117
118
if len(components) != 2 {
119
-
l.Error("invalid repo format", "components", components)
120
fmt.Fprintln(os.Stderr, "invalid repo format, needs <user>/<repo> or /<user>/<repo>")
121
os.Exit(-1)
122
}
123
124
didOrHandle := components[0]
125
-
identity := resolveIdentity(ctx, l, didOrHandle)
126
did := identity.DID.String()
127
repoName := components[1]
128
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
···
133
"git-upload-archive": true,
134
}
135
if !validCommands[gitCommand] {
136
-
l.Error("access denied: invalid git command", "command", gitCommand)
137
fmt.Fprintln(os.Stderr, "access denied: invalid git command")
138
return fmt.Errorf("access denied: invalid git command")
139
}
140
141
if gitCommand != "git-upload-pack" {
142
-
if !isPushPermitted(l, incomingUser, qualifiedRepoName, endpoint) {
143
-
l.Error("access denied: user not allowed",
144
"did", incomingUser,
145
"reponame", qualifiedRepoName)
146
fmt.Fprintln(os.Stderr, "access denied: user not allowed")
···
150
151
fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoName)
152
153
-
l.Info("processing command",
154
"user", incomingUser,
155
"command", gitCommand,
156
"repo", repoName,
···
160
var motdReader io.Reader
161
if reader, err := os.Open(motdFile); err != nil {
162
if !errors.Is(err, os.ErrNotExist) {
163
-
l.Error("failed to read motd file", "error", err)
164
}
165
motdReader = strings.NewReader("Welcome to this knot!\n")
166
} else {
···
181
)
182
183
if err := gitCmd.Run(); err != nil {
184
-
l.Error("command failed", "error", err)
185
fmt.Fprintf(os.Stderr, "command failed: %v\n", err)
186
return fmt.Errorf("command failed: %v", err)
187
}
188
189
-
l.Info("command completed",
190
"user", incomingUser,
191
"command", gitCommand,
192
"repo", repoName,
···
195
return nil
196
}
197
198
-
func resolveIdentity(ctx context.Context, l *slog.Logger, didOrHandle string) *identity.Identity {
199
resolver := idresolver.DefaultResolver()
200
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
201
if err != nil {
202
-
l.Error("Error resolving handle", "error", err, "handle", didOrHandle)
203
fmt.Fprintf(os.Stderr, "error resolving handle: %v\n", err)
204
os.Exit(1)
205
}
206
if ident.Handle.IsInvalidHandle() {
207
-
l.Error("Error resolving handle", "invalid handle", didOrHandle)
208
fmt.Fprintf(os.Stderr, "error resolving handle: invalid handle\n")
209
os.Exit(1)
210
}
211
return ident
212
}
213
214
-
func isPushPermitted(l *slog.Logger, user, qualifiedRepoName, endpoint string) bool {
215
u, _ := url.Parse(endpoint + "/push-allowed")
216
q := u.Query()
217
q.Add("user", user)
···
220
221
req, err := http.Get(u.String())
222
if err != nil {
223
-
l.Error("Error verifying permissions", "error", err)
224
fmt.Fprintf(os.Stderr, "error verifying permissions: %v\n", err)
225
os.Exit(1)
226
}
227
228
-
l.Info("Checking push permission",
229
"url", u.String(),
230
"status", req.Status)
231
···
16
securejoin "github.com/cyphar/filepath-securejoin"
17
"github.com/urfave/cli/v3"
18
"tangled.org/core/idresolver"
19
)
20
21
func Command() *cli.Command {
···
54
}
55
56
func Run(ctx context.Context, cmd *cli.Command) error {
57
incomingUser := cmd.String("user")
58
gitDir := cmd.String("git-dir")
59
logPath := cmd.String("log-path")
60
endpoint := cmd.String("internal-api")
61
motdFile := cmd.String("motd-file")
62
63
+
stream := io.Discard
64
logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
65
+
if err == nil {
66
+
stream = logFile
67
}
68
69
+
fileHandler := slog.NewJSONHandler(stream, &slog.HandlerOptions{Level: slog.LevelInfo})
70
+
slog.SetDefault(slog.New(fileHandler))
71
+
72
var clientIP string
73
if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" {
74
parts := strings.Fields(connInfo)
···
78
}
79
80
if incomingUser == "" {
81
+
slog.Error("access denied: no user specified")
82
fmt.Fprintln(os.Stderr, "access denied: no user specified")
83
os.Exit(-1)
84
}
85
86
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
87
88
+
slog.Info("connection attempt",
89
"user", incomingUser,
90
"command", sshCommand,
91
"client", clientIP)
92
93
if sshCommand == "" {
94
+
slog.Info("access denied: no interactive shells", "user", incomingUser)
95
fmt.Fprintf(os.Stderr, "Hi @%s! You've successfully authenticated.\n", incomingUser)
96
os.Exit(-1)
97
}
98
99
cmdParts := strings.Fields(sshCommand)
100
if len(cmdParts) < 2 {
101
+
slog.Error("invalid command format", "command", sshCommand)
102
fmt.Fprintln(os.Stderr, "invalid command format")
103
os.Exit(-1)
104
}
···
110
// any of the above with a leading slash (/)
111
112
components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/")
113
+
slog.Info("command components", "components", components)
114
115
if len(components) != 2 {
116
+
slog.Error("invalid repo format", "components", components)
117
fmt.Fprintln(os.Stderr, "invalid repo format, needs <user>/<repo> or /<user>/<repo>")
118
os.Exit(-1)
119
}
120
121
didOrHandle := components[0]
122
+
identity := resolveIdentity(ctx, didOrHandle)
123
did := identity.DID.String()
124
repoName := components[1]
125
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
···
130
"git-upload-archive": true,
131
}
132
if !validCommands[gitCommand] {
133
+
slog.Error("access denied: invalid git command", "command", gitCommand)
134
fmt.Fprintln(os.Stderr, "access denied: invalid git command")
135
return fmt.Errorf("access denied: invalid git command")
136
}
137
138
if gitCommand != "git-upload-pack" {
139
+
if !isPushPermitted(incomingUser, qualifiedRepoName, endpoint) {
140
+
slog.Error("access denied: user not allowed",
141
"did", incomingUser,
142
"reponame", qualifiedRepoName)
143
fmt.Fprintln(os.Stderr, "access denied: user not allowed")
···
147
148
fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoName)
149
150
+
slog.Info("processing command",
151
"user", incomingUser,
152
"command", gitCommand,
153
"repo", repoName,
···
157
var motdReader io.Reader
158
if reader, err := os.Open(motdFile); err != nil {
159
if !errors.Is(err, os.ErrNotExist) {
160
+
slog.Error("failed to read motd file", "error", err)
161
}
162
motdReader = strings.NewReader("Welcome to this knot!\n")
163
} else {
···
178
)
179
180
if err := gitCmd.Run(); err != nil {
181
+
slog.Error("command failed", "error", err)
182
fmt.Fprintf(os.Stderr, "command failed: %v\n", err)
183
return fmt.Errorf("command failed: %v", err)
184
}
185
186
+
slog.Info("command completed",
187
"user", incomingUser,
188
"command", gitCommand,
189
"repo", repoName,
···
192
return nil
193
}
194
195
+
func resolveIdentity(ctx context.Context, didOrHandle string) *identity.Identity {
196
resolver := idresolver.DefaultResolver()
197
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
198
if err != nil {
199
+
slog.Error("Error resolving handle", "error", err, "handle", didOrHandle)
200
fmt.Fprintf(os.Stderr, "error resolving handle: %v\n", err)
201
os.Exit(1)
202
}
203
if ident.Handle.IsInvalidHandle() {
204
+
slog.Error("Error resolving handle", "invalid handle", didOrHandle)
205
fmt.Fprintf(os.Stderr, "error resolving handle: invalid handle\n")
206
os.Exit(1)
207
}
208
return ident
209
}
210
211
+
func isPushPermitted(user, qualifiedRepoName, endpoint string) bool {
212
u, _ := url.Parse(endpoint + "/push-allowed")
213
q := u.Query()
214
q.Add("user", user)
···
217
218
req, err := http.Get(u.String())
219
if err != nil {
220
+
slog.Error("Error verifying permissions", "error", err)
221
fmt.Fprintf(os.Stderr, "error verifying permissions: %v\n", err)
222
os.Exit(1)
223
}
224
225
+
slog.Info("checking push permission",
226
"url", u.String(),
227
"status", req.Status)
228