From 54a9a960f4e6be617ec4e1e6679efb2d82e5e55e Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Mon, 20 Oct 2025 22:15:56 +0900 Subject: [PATCH] guard,knotserver/internal: move guard logic to internal server Change-Id: rpmokntslpkkznzusqynukvnvnwrumzl This will allow running guard without passing every single config options Signed-off-by: Seongmin Lee --- guard/guard.go | 103 ++++++++++++++--------------------------- knotserver/internal.go | 61 ++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 67 deletions(-) diff --git a/guard/guard.go b/guard/guard.go index fda0578d..92d1bda8 100644 --- a/guard/guard.go +++ b/guard/guard.go @@ -12,11 +12,8 @@ import ( "os/exec" "strings" - "github.com/bluesky-social/indigo/atproto/identity" securejoin "github.com/cyphar/filepath-securejoin" "github.com/urfave/cli/v3" - "tangled.org/core/idresolver" - "tangled.org/core/knotserver/config" "tangled.org/core/log" ) @@ -58,11 +55,6 @@ func Command() *cli.Command { func Run(ctx context.Context, cmd *cli.Command) error { l := log.FromContext(ctx) - c, err := config.Load(ctx) - if err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - incomingUser := cmd.String("user") gitDir := cmd.String("git-dir") logPath := cmd.String("log-path") @@ -99,6 +91,7 @@ func Run(ctx context.Context, cmd *cli.Command) error { "command", sshCommand, "client", clientIP) + // TODO: greet user with their resolved handle instead of did if sshCommand == "" { l.Info("access denied: no interactive shells", "user", incomingUser) fmt.Fprintf(os.Stderr, "Hi @%s! You've successfully authenticated.\n", incomingUser) @@ -113,25 +106,7 @@ func Run(ctx context.Context, cmd *cli.Command) error { } gitCommand := cmdParts[0] - - // did:foo/repo-name or - // handle/repo-name or - // any of the above with a leading slash (/) - - components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/") - l.Info("command components", "components", components) - - if len(components) != 2 { - l.Error("invalid repo format", "components", components) - fmt.Fprintln(os.Stderr, "invalid repo format, needs / or //") - os.Exit(-1) - } - - didOrHandle := components[0] - identity := resolveIdentity(ctx, c, l, didOrHandle) - did := identity.DID.String() - repoName := components[1] - qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName) + repoPath := cmdParts[1] validCommands := map[string]bool{ "git-receive-pack": true, @@ -144,22 +119,20 @@ func Run(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("access denied: invalid git command") } - if gitCommand != "git-upload-pack" { - if !isPushPermitted(l, incomingUser, qualifiedRepoName, endpoint) { - l.Error("access denied: user not allowed", - "did", incomingUser, - "reponame", qualifiedRepoName) - fmt.Fprintln(os.Stderr, "access denied: user not allowed") - os.Exit(-1) - } + // qualify repo path from internal server which holds the knot config + qualifiedRepoPath, err := guardAndQualifyRepo(l, endpoint, incomingUser, repoPath, gitCommand) + if err != nil { + l.Error("failed to run guard", "err", err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoName) + fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoPath) l.Info("processing command", "user", incomingUser, "command", gitCommand, - "repo", repoName, + "repo", repoPath, "fullPath", fullPath, "client", clientIP) @@ -183,7 +156,6 @@ func Run(ctx context.Context, cmd *cli.Command) error { gitCmd.Stdin = os.Stdin gitCmd.Env = append(os.Environ(), fmt.Sprintf("GIT_USER_DID=%s", incomingUser), - fmt.Sprintf("GIT_USER_PDS_ENDPOINT=%s", identity.PDSEndpoint()), ) if err := gitCmd.Run(); err != nil { @@ -195,45 +167,42 @@ func Run(ctx context.Context, cmd *cli.Command) error { l.Info("command completed", "user", incomingUser, "command", gitCommand, - "repo", repoName, + "repo", repoPath, "success", true) return nil } -func resolveIdentity(ctx context.Context, c *config.Config, l *slog.Logger, didOrHandle string) *identity.Identity { - resolver := idresolver.DefaultResolver(c.Server.PlcUrl) - ident, err := resolver.ResolveIdent(ctx, didOrHandle) - if err != nil { - l.Error("Error resolving handle", "error", err, "handle", didOrHandle) - fmt.Fprintf(os.Stderr, "error resolving handle: %v\n", err) - os.Exit(1) - } - if ident.Handle.IsInvalidHandle() { - l.Error("Error resolving handle", "invalid handle", didOrHandle) - fmt.Fprintf(os.Stderr, "error resolving handle: invalid handle\n") - os.Exit(1) - } - return ident -} - -func isPushPermitted(l *slog.Logger, user, qualifiedRepoName, endpoint string) bool { - u, _ := url.Parse(endpoint + "/push-allowed") +// runs guardAndQualifyRepo logic +func guardAndQualifyRepo(l *slog.Logger, endpoint, incomingUser, repo, gitCommand string) (string, error) { + u, _ := url.Parse(endpoint + "/guard") q := u.Query() - q.Add("user", user) - q.Add("repo", qualifiedRepoName) + q.Add("user", incomingUser) + q.Add("repo", repo) + q.Add("gitCmd", gitCommand) u.RawQuery = q.Encode() - req, err := http.Get(u.String()) + resp, err := http.Get(u.String()) if err != nil { - l.Error("Error verifying permissions", "error", err) - fmt.Fprintf(os.Stderr, "error verifying permissions: %v\n", err) - os.Exit(1) + return "", err } + defer resp.Body.Close() + + l.Info("Running guard", "url", u.String(), "status", resp.Status) - l.Info("Checking push permission", - "url", u.String(), - "status", req.Status) + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + text := string(body) - return req.StatusCode == http.StatusNoContent + switch resp.StatusCode { + case http.StatusOK: + return text, nil + case http.StatusForbidden: + l.Error("access denied: user not allowed", "did", incomingUser, "reponame", text) + return text, errors.New("access denied: user not allowed") + default: + return "", errors.New(text) + } } diff --git a/knotserver/internal.go b/knotserver/internal.go index 9c98de6f..4829848e 100644 --- a/knotserver/internal.go +++ b/knotserver/internal.go @@ -68,6 +68,66 @@ func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) { writeJSON(w, data) } +// response in text/plain format +// the body will be qualified repository path on success/push-denied +// or an error message when process failed +func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { + l := h.l.With("handler", "PostReceiveHook") + + var ( + incomingUser = r.URL.Query().Get("user") + repo = r.URL.Query().Get("repo") + gitCommand = r.URL.Query().Get("gitCmd") + ) + + if incomingUser == "" || repo == "" || gitCommand == "" { + w.WriteHeader(http.StatusBadRequest) + l.Error("invalid request", "incomingUser", incomingUser, "repo", repo, "gitCommand", gitCommand) + fmt.Fprintln(w, "invalid internal request") + return + } + + // did:foo/repo-name or + // handle/repo-name or + // any of the above with a leading slash (/) + components := strings.Split(strings.TrimPrefix(strings.Trim(repo, "'"), "/"), "/") + l.Info("command components", "components", components) + + if len(components) != 2 { + w.WriteHeader(http.StatusBadRequest) + l.Error("invalid repo format", "components", components) + fmt.Fprintln(w, "invalid repo format, needs / or //") + return + } + repoOwner := components[0] + repoName := components[1] + + resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) + + repoOwnerIdent, err := resolver.ResolveIdent(r.Context(), repoOwner) + if err != nil || repoOwnerIdent.Handle.IsInvalidHandle() { + l.Error("Error resolving handle", "handle", repoOwner, "err", err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "error resolving handle: invalid handle\n") + return + } + repoOwnerDid := repoOwnerIdent.DID.String() + + qualifiedRepo, _ := securejoin.SecureJoin(repoOwnerDid, repoName) + + if gitCommand == "git-receive-pack" { + ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, qualifiedRepo) + if err != nil || !ok { + w.WriteHeader(http.StatusForbidden) + fmt.Fprint(w, repo) + return + } + } + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, qualifiedRepo) +} + type PushOptions struct { skipCi bool verboseCi bool @@ -366,6 +426,7 @@ func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer r.Get("/push-allowed", h.PushAllowed) r.Get("/keys", h.InternalKeys) + r.Get("/guard", h.Guard) r.Post("/hooks/post-receive", h.PostReceiveHook) r.Mount("/debug", middleware.Profiler()) -- 2.43.0