Path: blob/main/components/gitpod-cli/cmd/credential-helper.go
2498 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cmd56import (7"bufio"8"context"9"fmt"10"io"11"os"12"os/exec"13"regexp"14"strings"15"time"1617"github.com/prometheus/procfs"18log "github.com/sirupsen/logrus"19"github.com/spf13/cobra"20"golang.org/x/xerrors"21"google.golang.org/grpc"22"google.golang.org/grpc/credentials/insecure"2324"github.com/gitpod-io/gitpod/common-go/util"25"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"26supervisor "github.com/gitpod-io/gitpod/supervisor/api"27)2829var credentialHelper = &cobra.Command{30Use: "credential-helper get",31Short: "Gitpod Credential Helper for Git",32Long: "Supports reading of credentials per host.",33Args: cobra.MinimumNArgs(1),34Hidden: true,35RunE: func(cmd *cobra.Command, args []string) error {36// ignore trace37utils.TrackCommandUsageEvent.Command = nil3839exitCode := 040action := args[0]41log.SetOutput(io.Discard)42f, err := os.OpenFile(os.TempDir()+"/gitpod-git-credential-helper.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)43if err == nil {44defer f.Close()45log.SetOutput(f)46}47if action != "get" {48return nil49}5051result, err := parseFromStdin()52host := result["host"]53if err != nil || host == "" {54log.WithError(err).Print("error parsing 'host' from stdin")55return GpError{OutCome: utils.Outcome_UserErr, Silence: true, ExitCode: &exitCode}56}5758var user, token string59defer func() {60// Server could return only the token and not the username, so we fallback to hardcoded `oauth2` username.61// See https://github.com/gitpod-io/gitpod/pull/7889#discussion_r80167095762if token != "" && user == "" {63user = "oauth2"64}65if token != "" {66result["username"] = user67result["password"] = token68}69for k, v := range result {70fmt.Printf("%s=%s\n", k, v)71}72}()7374ctx, cancel := context.WithTimeout(cmd.Context(), 1*time.Minute)75defer cancel()7677supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))78if err != nil {79log.WithError(err).Print("error connecting to supervisor")80return GpError{Err: xerrors.Errorf("error connecting to supervisor: %w", err), Silence: true, ExitCode: &exitCode}81}8283resp, err := supervisor.NewTokenServiceClient(supervisorConn).GetToken(ctx, &supervisor.GetTokenRequest{84Host: host,85Kind: "git",86})87if err != nil {88log.WithError(err).Print("error getting token from supervisor")89return GpError{Err: xerrors.Errorf("error getting token from supervisor: %w", err), Silence: true, ExitCode: &exitCode}90}9192user = resp.User93token = resp.Token9495gitCmdInfo := &gitCommandInfo{}96err = walkProcessTree(os.Getpid(), func(proc procfs.Proc) bool {97cmdLine, err := proc.CmdLine()98if err != nil {99log.WithError(err).Print("error reading proc cmdline")100return true101}102103cmdLineString := strings.Join(cmdLine, " ")104log.Printf("cmdLineString -> %v", cmdLineString)105gitCmdInfo.parseGitCommandAndRemote(cmdLineString)106107return gitCmdInfo.Ok()108})109if err != nil {110return GpError{Err: xerrors.Errorf("error walking process tree: %w", err), Silence: true, ExitCode: &exitCode}111}112if !gitCmdInfo.Ok() {113log.Warn(`Could not detect "RepoUrl" and or "GitCommand", token validation will not be performed`)114return nil115}116117// Starts another process which tracks the executed git event118gitCommandTracker := exec.Command("/proc/self/exe", "git-track-command", "--gitCommand", gitCmdInfo.GitCommand)119err = gitCommandTracker.Start()120if err != nil {121log.WithError(err).Print("error spawning tracker")122} else {123err = gitCommandTracker.Process.Release()124if err != nil {125log.WithError(err).Print("error releasing tracker")126}127}128129validator := exec.Command(130"/proc/self/exe",131"git-token-validator",132"--user", resp.User,133"--token", resp.Token,134"--scopes", strings.Join(resp.Scope, ","),135"--host", host,136"--repoURL", gitCmdInfo.RepoUrl,137"--gitCommand", gitCmdInfo.GitCommand,138)139err = validator.Start()140if err != nil {141return GpError{Err: xerrors.Errorf("error spawning validator: %w", err), Silence: true, ExitCode: &exitCode}142}143err = validator.Process.Release()144if err != nil {145log.WithError(err).Print("error releasing validator")146return GpError{Err: xerrors.Errorf("error releasing validator: %w", err), Silence: true, ExitCode: &exitCode}147}148return nil149},150}151152func parseFromStdin() (map[string]string, error) {153result := make(map[string]string)154scanner := bufio.NewScanner(os.Stdin)155for scanner.Scan() {156line := strings.TrimSpace(scanner.Text())157if len(line) > 0 {158tuple := strings.Split(line, "=")159if len(tuple) == 2 {160result[tuple[0]] = strings.TrimSpace(tuple[1])161}162}163}164if err := scanner.Err(); err != nil {165return nil, err166}167return result, nil168}169170type gitCommandInfo struct {171RepoUrl string172GitCommand string173}174175func (g *gitCommandInfo) Ok() bool {176return g.RepoUrl != "" && g.GitCommand != ""177}178179var gitCommandRegExp = regexp.MustCompile(`git(?:\s+(?:\S+\s+)*)(push|clone|fetch|pull|diff|ls-remote)(?:\s+(?:\S+\s+)*)?`)180var repoUrlRegExp = regexp.MustCompile(`remote-https?\s([^\s]+)\s+(https?:[^\s]+)`)181182// This method needs to be called multiple times to fill all the required info183// from different git commands184// For example from first command below the `RepoUrl` will be parsed and from185// the second command the `GitCommand` will be parsed186// `/usr/lib/git-core/git-remote-https origin https://github.com/jeanp413/test-gp-bug.git`187// `/usr/lib/git-core/git push`188func (g *gitCommandInfo) parseGitCommandAndRemote(cmdLineString string) {189matchCommand := gitCommandRegExp.FindStringSubmatch(cmdLineString)190if len(matchCommand) == 2 {191g.GitCommand = matchCommand[1]192}193194matchRepo := repoUrlRegExp.FindStringSubmatch(cmdLineString)195if len(matchRepo) == 3 {196g.RepoUrl = matchRepo[2]197if !strings.HasSuffix(g.RepoUrl, ".git") {198g.RepoUrl = g.RepoUrl + ".git"199}200}201}202203type pidCallbackFn func(procfs.Proc) bool204205func walkProcessTree(pid int, fn pidCallbackFn) error {206for {207proc, err := procfs.NewProc(pid)208if err != nil {209return err210}211212stop := fn(proc)213if stop {214return nil215}216217procStat, err := proc.Stat()218if err != nil {219return err220}221if procStat.PPID == pid || procStat.PPID == 1 /* supervisor pid*/ {222return nil223}224pid = procStat.PPID225}226}227228// How to smoke test:229// - 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 vscode230// - 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 vscode231// - Private npm package232// - Create a private git repository for an npm package e.g https://github.com/jeanp413/test-private-package233// - Start a workspace, then run `npm install github:jeanp413/test-private-package` with and without permissions in the dashboard234//235// - Private npm package no access236// - Open this workspace https://github.com/jeanp413/test-gp-bug and run `npm install`237// - Observe NO notification with this message appears `Unknown repository ” Please grant the necessary permissions.`238//239// - Clone private repo without permission240// - 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`241func init() {242rootCmd.AddCommand(credentialHelper)243}244245246