+36
-67
guard/guard.go
+36
-67
guard/guard.go
···
12
12
"os/exec"
13
13
"strings"
14
14
15
-
"github.com/bluesky-social/indigo/atproto/identity"
16
15
securejoin "github.com/cyphar/filepath-securejoin"
17
16
"github.com/urfave/cli/v3"
18
-
"tangled.org/core/idresolver"
19
-
"tangled.org/core/knotserver/config"
20
17
"tangled.org/core/log"
21
18
)
22
19
···
58
55
func Run(ctx context.Context, cmd *cli.Command) error {
59
56
l := log.FromContext(ctx)
60
57
61
-
c, err := config.Load(ctx)
62
-
if err != nil {
63
-
return fmt.Errorf("failed to load config: %w", err)
64
-
}
65
-
66
58
incomingUser := cmd.String("user")
67
59
gitDir := cmd.String("git-dir")
68
60
logPath := cmd.String("log-path")
···
99
91
"command", sshCommand,
100
92
"client", clientIP)
101
93
94
+
// TODO: greet user with their resolved handle instead of did
102
95
if sshCommand == "" {
103
96
l.Info("access denied: no interactive shells", "user", incomingUser)
104
97
fmt.Fprintf(os.Stderr, "Hi @%s! You've successfully authenticated.\n", incomingUser)
···
113
106
}
114
107
115
108
gitCommand := cmdParts[0]
116
-
117
-
// did:foo/repo-name or
118
-
// handle/repo-name or
119
-
// any of the above with a leading slash (/)
120
-
121
-
components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/")
122
-
l.Info("command components", "components", components)
123
-
124
-
if len(components) != 2 {
125
-
l.Error("invalid repo format", "components", components)
126
-
fmt.Fprintln(os.Stderr, "invalid repo format, needs <user>/<repo> or /<user>/<repo>")
127
-
os.Exit(-1)
128
-
}
129
-
130
-
didOrHandle := components[0]
131
-
identity := resolveIdentity(ctx, c, l, didOrHandle)
132
-
did := identity.DID.String()
133
-
repoName := components[1]
134
-
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
109
+
repoPath := cmdParts[1]
135
110
136
111
validCommands := map[string]bool{
137
112
"git-receive-pack": true,
···
144
119
return fmt.Errorf("access denied: invalid git command")
145
120
}
146
121
147
-
if gitCommand != "git-upload-pack" {
148
-
if !isPushPermitted(l, incomingUser, qualifiedRepoName, endpoint) {
149
-
l.Error("access denied: user not allowed",
150
-
"did", incomingUser,
151
-
"reponame", qualifiedRepoName)
152
-
fmt.Fprintln(os.Stderr, "access denied: user not allowed")
153
-
os.Exit(-1)
154
-
}
122
+
// qualify repo path from internal server which holds the knot config
123
+
qualifiedRepoPath, err := guardAndQualifyRepo(l, endpoint, incomingUser, repoPath, gitCommand)
124
+
if err != nil {
125
+
l.Error("failed to run guard", "err", err)
126
+
fmt.Fprintln(os.Stderr, err)
127
+
os.Exit(1)
155
128
}
156
129
157
-
fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoName)
130
+
fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoPath)
158
131
159
132
l.Info("processing command",
160
133
"user", incomingUser,
161
134
"command", gitCommand,
162
-
"repo", repoName,
135
+
"repo", repoPath,
163
136
"fullPath", fullPath,
164
137
"client", clientIP)
165
138
···
183
156
gitCmd.Stdin = os.Stdin
184
157
gitCmd.Env = append(os.Environ(),
185
158
fmt.Sprintf("GIT_USER_DID=%s", incomingUser),
186
-
fmt.Sprintf("GIT_USER_PDS_ENDPOINT=%s", identity.PDSEndpoint()),
187
159
)
188
160
189
161
if err := gitCmd.Run(); err != nil {
···
195
167
l.Info("command completed",
196
168
"user", incomingUser,
197
169
"command", gitCommand,
198
-
"repo", repoName,
170
+
"repo", repoPath,
199
171
"success", true)
200
172
201
173
return nil
202
174
}
203
175
204
-
func resolveIdentity(ctx context.Context, c *config.Config, l *slog.Logger, didOrHandle string) *identity.Identity {
205
-
resolver := idresolver.DefaultResolver(c.Server.PlcUrl)
206
-
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
176
+
// runs guardAndQualifyRepo logic
177
+
func guardAndQualifyRepo(l *slog.Logger, endpoint, incomingUser, repo, gitCommand string) (string, error) {
178
+
u, _ := url.Parse(endpoint + "/guard")
179
+
q := u.Query()
180
+
q.Add("user", incomingUser)
181
+
q.Add("repo", repo)
182
+
q.Add("gitCmd", gitCommand)
183
+
u.RawQuery = q.Encode()
184
+
185
+
resp, err := http.Get(u.String())
207
186
if err != nil {
208
-
l.Error("Error resolving handle", "error", err, "handle", didOrHandle)
209
-
fmt.Fprintf(os.Stderr, "error resolving handle: %v\n", err)
210
-
os.Exit(1)
187
+
return "", err
211
188
}
212
-
if ident.Handle.IsInvalidHandle() {
213
-
l.Error("Error resolving handle", "invalid handle", didOrHandle)
214
-
fmt.Fprintf(os.Stderr, "error resolving handle: invalid handle\n")
215
-
os.Exit(1)
216
-
}
217
-
return ident
218
-
}
189
+
defer resp.Body.Close()
219
190
220
-
func isPushPermitted(l *slog.Logger, user, qualifiedRepoName, endpoint string) bool {
221
-
u, _ := url.Parse(endpoint + "/push-allowed")
222
-
q := u.Query()
223
-
q.Add("user", user)
224
-
q.Add("repo", qualifiedRepoName)
225
-
u.RawQuery = q.Encode()
191
+
l.Info("Running guard", "url", u.String(), "status", resp.Status)
226
192
227
-
req, err := http.Get(u.String())
193
+
body, err := io.ReadAll(resp.Body)
228
194
if err != nil {
229
-
l.Error("Error verifying permissions", "error", err)
230
-
fmt.Fprintf(os.Stderr, "error verifying permissions: %v\n", err)
231
-
os.Exit(1)
195
+
return "", err
232
196
}
197
+
text := string(body)
233
198
234
-
l.Info("Checking push permission",
235
-
"url", u.String(),
236
-
"status", req.Status)
237
-
238
-
return req.StatusCode == http.StatusNoContent
199
+
switch resp.StatusCode {
200
+
case http.StatusOK:
201
+
return text, nil
202
+
case http.StatusForbidden:
203
+
l.Error("access denied: user not allowed", "did", incomingUser, "reponame", text)
204
+
return text, errors.New("access denied: user not allowed")
205
+
default:
206
+
return "", errors.New(text)
207
+
}
239
208
}
+61
knotserver/internal.go
+61
knotserver/internal.go
···
68
68
writeJSON(w, data)
69
69
}
70
70
71
+
// response in text/plain format
72
+
// the body will be qualified repository path on success/push-denied
73
+
// or an error message when process failed
74
+
func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) {
75
+
l := h.l.With("handler", "PostReceiveHook")
76
+
77
+
var (
78
+
incomingUser = r.URL.Query().Get("user")
79
+
repo = r.URL.Query().Get("repo")
80
+
gitCommand = r.URL.Query().Get("gitCmd")
81
+
)
82
+
83
+
if incomingUser == "" || repo == "" || gitCommand == "" {
84
+
w.WriteHeader(http.StatusBadRequest)
85
+
l.Error("invalid request", "incomingUser", incomingUser, "repo", repo, "gitCommand", gitCommand)
86
+
fmt.Fprintln(w, "invalid internal request")
87
+
return
88
+
}
89
+
90
+
// did:foo/repo-name or
91
+
// handle/repo-name or
92
+
// any of the above with a leading slash (/)
93
+
components := strings.Split(strings.TrimPrefix(strings.Trim(repo, "'"), "/"), "/")
94
+
l.Info("command components", "components", components)
95
+
96
+
if len(components) != 2 {
97
+
w.WriteHeader(http.StatusBadRequest)
98
+
l.Error("invalid repo format", "components", components)
99
+
fmt.Fprintln(w, "invalid repo format, needs <user>/<repo> or /<user>/<repo>")
100
+
return
101
+
}
102
+
repoOwner := components[0]
103
+
repoName := components[1]
104
+
105
+
resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl)
106
+
107
+
repoOwnerIdent, err := resolver.ResolveIdent(r.Context(), repoOwner)
108
+
if err != nil || repoOwnerIdent.Handle.IsInvalidHandle() {
109
+
l.Error("Error resolving handle", "handle", repoOwner, "err", err)
110
+
w.WriteHeader(http.StatusInternalServerError)
111
+
fmt.Fprintf(w, "error resolving handle: invalid handle\n")
112
+
return
113
+
}
114
+
repoOwnerDid := repoOwnerIdent.DID.String()
115
+
116
+
qualifiedRepo, _ := securejoin.SecureJoin(repoOwnerDid, repoName)
117
+
118
+
if gitCommand == "git-receive-pack" {
119
+
ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, qualifiedRepo)
120
+
if err != nil || !ok {
121
+
w.WriteHeader(http.StatusForbidden)
122
+
fmt.Fprint(w, repo)
123
+
return
124
+
}
125
+
}
126
+
127
+
w.WriteHeader(http.StatusOK)
128
+
fmt.Fprint(w, qualifiedRepo)
129
+
}
130
+
71
131
type PushOptions struct {
72
132
skipCi bool
73
133
verboseCi bool
···
366
426
367
427
r.Get("/push-allowed", h.PushAllowed)
368
428
r.Get("/keys", h.InternalKeys)
429
+
r.Get("/guard", h.Guard)
369
430
r.Post("/hooks/post-receive", h.PostReceiveHook)
370
431
r.Mount("/debug", middleware.Profiler())
371
432