forked from tangled.org/core
Monorepo for Tangled

guard,knotserver/internal: move guard logic to internal server

This will allow running guard without passing every single config
options

Signed-off-by: Seongmin Lee <git@boltless.me>

authored by boltless.me and committed by Tangled d9e6028d a99db81f

Changed files
+97 -67
guard
knotserver
+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
··· 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