mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
245 lines
7.5 KiB
Go
245 lines
7.5 KiB
Go
// 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)
|
|
}
|