// Copyright (c) 2022 Gitpod GmbH. All rights reserved. // Licensed under the GNU Affero General Public License (AGPL). // See License.AGPL.txt in the project root for license information. package cmd import ( "bufio" "context" "fmt" "io" "os" "os/exec" "regexp" "strings" "time" "github.com/prometheus/procfs" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/xerrors" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/gitpod-io/gitpod/common-go/util" "github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils" supervisor "github.com/gitpod-io/gitpod/supervisor/api" ) var credentialHelper = &cobra.Command{ Use: "credential-helper get", Short: "Gitpod Credential Helper for Git", Long: "Supports reading of credentials per host.", Args: cobra.MinimumNArgs(1), Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { // ignore trace utils.TrackCommandUsageEvent.Command = nil exitCode := 0 action := args[0] log.SetOutput(io.Discard) f, err := os.OpenFile(os.TempDir()+"/gitpod-git-credential-helper.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err == nil { defer f.Close() log.SetOutput(f) } if action != "get" { return nil } result, err := parseFromStdin() host := result["host"] if err != nil || host == "" { log.WithError(err).Print("error parsing 'host' from stdin") return GpError{OutCome: utils.Outcome_UserErr, Silence: true, ExitCode: &exitCode} } var user, token string defer func() { // Server could return only the token and not the username, so we fallback to hardcoded `oauth2` username. // See https://github.com/gitpod-io/gitpod/pull/7889#discussion_r801670957 if token != "" && user == "" { user = "oauth2" } if token != "" { result["username"] = user result["password"] = token } for k, v := range result { fmt.Printf("%s=%s\n", k, v) } }() ctx, cancel := context.WithTimeout(cmd.Context(), 1*time.Minute) defer cancel() supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.WithError(err).Print("error connecting to supervisor") return GpError{Err: xerrors.Errorf("error connecting to supervisor: %w", err), Silence: true, ExitCode: &exitCode} } resp, err := supervisor.NewTokenServiceClient(supervisorConn).GetToken(ctx, &supervisor.GetTokenRequest{ Host: host, Kind: "git", }) if err != nil { log.WithError(err).Print("error getting token from supervisor") return GpError{Err: xerrors.Errorf("error getting token from supervisor: %w", err), Silence: true, ExitCode: &exitCode} } user = resp.User token = resp.Token gitCmdInfo := &gitCommandInfo{} err = walkProcessTree(os.Getpid(), func(proc procfs.Proc) bool { cmdLine, err := proc.CmdLine() if err != nil { log.WithError(err).Print("error reading proc cmdline") return true } cmdLineString := strings.Join(cmdLine, " ") log.Printf("cmdLineString -> %v", cmdLineString) gitCmdInfo.parseGitCommandAndRemote(cmdLineString) return gitCmdInfo.Ok() }) if err != nil { return GpError{Err: xerrors.Errorf("error walking process tree: %w", err), Silence: true, ExitCode: &exitCode} } if !gitCmdInfo.Ok() { log.Warn(`Could not detect "RepoUrl" and or "GitCommand", token validation will not be performed`) return nil } // Starts another process which tracks the executed git event gitCommandTracker := exec.Command("/proc/self/exe", "git-track-command", "--gitCommand", gitCmdInfo.GitCommand) err = gitCommandTracker.Start() if err != nil { log.WithError(err).Print("error spawning tracker") } else { err = gitCommandTracker.Process.Release() if err != nil { log.WithError(err).Print("error releasing tracker") } } validator := exec.Command( "/proc/self/exe", "git-token-validator", "--user", resp.User, "--token", resp.Token, "--scopes", strings.Join(resp.Scope, ","), "--host", host, "--repoURL", gitCmdInfo.RepoUrl, "--gitCommand", gitCmdInfo.GitCommand, ) err = validator.Start() if err != nil { return GpError{Err: xerrors.Errorf("error spawning validator: %w", err), Silence: true, ExitCode: &exitCode} } err = validator.Process.Release() if err != nil { log.WithError(err).Print("error releasing validator") return GpError{Err: xerrors.Errorf("error releasing validator: %w", err), Silence: true, ExitCode: &exitCode} } return nil }, } func parseFromStdin() (map[string]string, error) { result := make(map[string]string) scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if len(line) > 0 { tuple := strings.Split(line, "=") if len(tuple) == 2 { result[tuple[0]] = strings.TrimSpace(tuple[1]) } } } if err := scanner.Err(); err != nil { return nil, err } return result, nil } type gitCommandInfo struct { RepoUrl string GitCommand string } func (g *gitCommandInfo) Ok() bool { return g.RepoUrl != "" && g.GitCommand != "" } var gitCommandRegExp = regexp.MustCompile(`git(?:\s+(?:\S+\s+)*)(push|clone|fetch|pull|diff|ls-remote)(?:\s+(?:\S+\s+)*)?`) var repoUrlRegExp = regexp.MustCompile(`remote-https?\s([^\s]+)\s+(https?:[^\s]+)`) // This method needs to be called multiple times to fill all the required info // from different git commands // For example from first command below the `RepoUrl` will be parsed and from // the second command the `GitCommand` will be parsed // `/usr/lib/git-core/git-remote-https origin https://github.com/jeanp413/test-gp-bug.git` // `/usr/lib/git-core/git push` func (g *gitCommandInfo) parseGitCommandAndRemote(cmdLineString string) { matchCommand := gitCommandRegExp.FindStringSubmatch(cmdLineString) if len(matchCommand) == 2 { g.GitCommand = matchCommand[1] } matchRepo := repoUrlRegExp.FindStringSubmatch(cmdLineString) if len(matchRepo) == 3 { g.RepoUrl = matchRepo[2] if !strings.HasSuffix(g.RepoUrl, ".git") { g.RepoUrl = g.RepoUrl + ".git" } } } type pidCallbackFn func(procfs.Proc) bool func walkProcessTree(pid int, fn pidCallbackFn) error { for { proc, err := procfs.NewProc(pid) if err != nil { return err } stop := fn(proc) if stop { return nil } procStat, err := proc.Stat() if err != nil { return err } if procStat.PPID == pid || procStat.PPID == 1 /* supervisor pid*/ { return nil } pid = procStat.PPID } } // How to smoke test: // - Open a public git repository and try pushing some commit with and without permissions in the dashboard, if no permissions a popup should appear in vscode // - Open a private git repository and try pushing some commit with and without permissions in the dashboard, if no permissions a popup should appear in vscode // - Private npm package // - Create a private git repository for an npm package e.g https://github.com/jeanp413/test-private-package // - Start a workspace, then run `npm install github:jeanp413/test-private-package` with and without permissions in the dashboard // // - Private npm package no access // - Open this workspace https://github.com/jeanp413/test-gp-bug and run `npm install` // - Observe NO notification with this message appears `Unknown repository ” Please grant the necessary permissions.` // // - Clone private repo without permission // - Start a workspace, then run `git clone 'https://gitlab.ebizmarts.com/ebizmarts/magento2-pos-api-request.git`, you should see a prompt ask your username and password, instead of `'gp credential-helper' told us to quit` func init() { rootCmd.AddCommand(credentialHelper) }