Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

Seongmin Lee and committed by
Tangled
d9e6028d a99db81f

+97 -67
+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 ··· 55 58 func Run(ctx context.Context, cmd *cli.Command) error { 56 59 l := log.FromContext(ctx) 57 60 58 - c, err := config.Load(ctx) 59 - if err != nil { 60 - return fmt.Errorf("failed to load config: %w", err) 61 - } 62 - 63 61 incomingUser := cmd.String("user") 64 62 gitDir := cmd.String("git-dir") 65 63 logPath := cmd.String("log-path") ··· 91 99 "command", sshCommand, 92 100 "client", clientIP) 93 101 102 + // TODO: greet user with their resolved handle instead of did 94 103 if sshCommand == "" { 95 104 l.Info("access denied: no interactive shells", "user", incomingUser) 96 105 fmt.Fprintf(os.Stderr, "Hi @%s! You've successfully authenticated.\n", incomingUser) ··· 106 113 } 107 114 108 115 gitCommand := cmdParts[0] 109 - 110 - // did:foo/repo-name or 111 - // handle/repo-name or 112 - // any of the above with a leading slash (/) 113 - 114 - components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/") 115 - l.Info("command components", "components", components) 116 - 117 - if len(components) != 2 { 118 - l.Error("invalid repo format", "components", components) 119 - fmt.Fprintln(os.Stderr, "invalid repo format, needs <user>/<repo> or /<user>/<repo>") 120 - os.Exit(-1) 121 - } 122 - 123 - didOrHandle := components[0] 124 - identity := resolveIdentity(ctx, c, l, didOrHandle) 125 - did := identity.DID.String() 126 - repoName := components[1] 127 - qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName) 116 + repoPath := cmdParts[1] 128 117 129 118 validCommands := map[string]bool{ 130 119 "git-receive-pack": true, ··· 119 144 return fmt.Errorf("access denied: invalid git command") 120 145 } 121 146 122 - if gitCommand != "git-upload-pack" { 123 - if !isPushPermitted(l, incomingUser, qualifiedRepoName, endpoint) { 124 - l.Error("access denied: user not allowed", 125 - "did", incomingUser, 126 - "reponame", qualifiedRepoName) 127 - fmt.Fprintln(os.Stderr, "access denied: user not allowed") 128 - os.Exit(-1) 129 - } 147 + // qualify repo path from internal server which holds the knot config 148 + qualifiedRepoPath, err := guardAndQualifyRepo(l, endpoint, incomingUser, repoPath, gitCommand) 149 + if err != nil { 150 + l.Error("failed to run guard", "err", err) 151 + fmt.Fprintln(os.Stderr, err) 152 + os.Exit(1) 130 153 } 131 154 132 - fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoName) 155 + fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoPath) 133 156 134 157 l.Info("processing command", 135 158 "user", incomingUser, 136 159 "command", gitCommand, 137 - "repo", repoName, 160 + "repo", repoPath, 138 161 "fullPath", fullPath, 139 162 "client", clientIP) 140 163 ··· 156 183 gitCmd.Stdin = os.Stdin 157 184 gitCmd.Env = append(os.Environ(), 158 185 fmt.Sprintf("GIT_USER_DID=%s", incomingUser), 159 - fmt.Sprintf("GIT_USER_PDS_ENDPOINT=%s", identity.PDSEndpoint()), 160 186 ) 161 187 162 188 if err := gitCmd.Run(); err != nil { ··· 167 195 l.Info("command completed", 168 196 "user", incomingUser, 169 197 "command", gitCommand, 170 - "repo", repoName, 198 + "repo", repoPath, 171 199 "success", true) 172 200 173 201 return nil 174 202 } 175 203 176 - func resolveIdentity(ctx context.Context, c *config.Config, l *slog.Logger, didOrHandle string) *identity.Identity { 177 - resolver := idresolver.DefaultResolver(c.Server.PlcUrl) 178 - ident, err := resolver.ResolveIdent(ctx, didOrHandle) 179 - if err != nil { 180 - l.Error("Error resolving handle", "error", err, "handle", didOrHandle) 181 - fmt.Fprintf(os.Stderr, "error resolving handle: %v\n", err) 182 - os.Exit(1) 183 - } 184 - if ident.Handle.IsInvalidHandle() { 185 - l.Error("Error resolving handle", "invalid handle", didOrHandle) 186 - fmt.Fprintf(os.Stderr, "error resolving handle: invalid handle\n") 187 - os.Exit(1) 188 - } 189 - return ident 190 - } 191 - 192 - func isPushPermitted(l *slog.Logger, user, qualifiedRepoName, endpoint string) bool { 193 - u, _ := url.Parse(endpoint + "/push-allowed") 204 + // runs guardAndQualifyRepo logic 205 + func guardAndQualifyRepo(l *slog.Logger, endpoint, incomingUser, repo, gitCommand string) (string, error) { 206 + u, _ := url.Parse(endpoint + "/guard") 194 207 q := u.Query() 195 - q.Add("user", user) 196 - q.Add("repo", qualifiedRepoName) 208 + q.Add("user", incomingUser) 209 + q.Add("repo", repo) 210 + q.Add("gitCmd", gitCommand) 197 211 u.RawQuery = q.Encode() 198 212 199 - req, err := http.Get(u.String()) 213 + resp, err := http.Get(u.String()) 200 214 if err != nil { 201 - l.Error("Error verifying permissions", "error", err) 202 - fmt.Fprintf(os.Stderr, "error verifying permissions: %v\n", err) 203 - os.Exit(1) 215 + return "", err 204 216 } 217 + defer resp.Body.Close() 205 218 206 - l.Info("Checking push permission", 207 - "url", u.String(), 208 - "status", req.Status) 219 + l.Info("Running guard", "url", u.String(), "status", resp.Status) 209 220 210 - return req.StatusCode == http.StatusNoContent 221 + body, err := io.ReadAll(resp.Body) 222 + if err != nil { 223 + return "", err 224 + } 225 + text := string(body) 226 + 227 + switch resp.StatusCode { 228 + case http.StatusOK: 229 + return text, nil 230 + case http.StatusForbidden: 231 + l.Error("access denied: user not allowed", "did", incomingUser, "reponame", text) 232 + return text, errors.New("access denied: user not allowed") 233 + default: 234 + return "", errors.New(text) 235 + } 211 236 }
+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 ··· 426 366 427 367 r.Get("/push-allowed", h.PushAllowed) 428 368 r.Get("/keys", h.InternalKeys) 369 + r.Get("/guard", h.Guard) 429 370 r.Post("/hooks/post-receive", h.PostReceiveHook) 430 371 r.Mount("/debug", middleware.Profiler()) 431 372