Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-cli/cmd/credential-helper.go
2498 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package cmd
6
7
import (
8
"bufio"
9
"context"
10
"fmt"
11
"io"
12
"os"
13
"os/exec"
14
"regexp"
15
"strings"
16
"time"
17
18
"github.com/prometheus/procfs"
19
log "github.com/sirupsen/logrus"
20
"github.com/spf13/cobra"
21
"golang.org/x/xerrors"
22
"google.golang.org/grpc"
23
"google.golang.org/grpc/credentials/insecure"
24
25
"github.com/gitpod-io/gitpod/common-go/util"
26
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"
27
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
28
)
29
30
var credentialHelper = &cobra.Command{
31
Use: "credential-helper get",
32
Short: "Gitpod Credential Helper for Git",
33
Long: "Supports reading of credentials per host.",
34
Args: cobra.MinimumNArgs(1),
35
Hidden: true,
36
RunE: func(cmd *cobra.Command, args []string) error {
37
// ignore trace
38
utils.TrackCommandUsageEvent.Command = nil
39
40
exitCode := 0
41
action := args[0]
42
log.SetOutput(io.Discard)
43
f, err := os.OpenFile(os.TempDir()+"/gitpod-git-credential-helper.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
44
if err == nil {
45
defer f.Close()
46
log.SetOutput(f)
47
}
48
if action != "get" {
49
return nil
50
}
51
52
result, err := parseFromStdin()
53
host := result["host"]
54
if err != nil || host == "" {
55
log.WithError(err).Print("error parsing 'host' from stdin")
56
return GpError{OutCome: utils.Outcome_UserErr, Silence: true, ExitCode: &exitCode}
57
}
58
59
var user, token string
60
defer func() {
61
// Server could return only the token and not the username, so we fallback to hardcoded `oauth2` username.
62
// See https://github.com/gitpod-io/gitpod/pull/7889#discussion_r801670957
63
if token != "" && user == "" {
64
user = "oauth2"
65
}
66
if token != "" {
67
result["username"] = user
68
result["password"] = token
69
}
70
for k, v := range result {
71
fmt.Printf("%s=%s\n", k, v)
72
}
73
}()
74
75
ctx, cancel := context.WithTimeout(cmd.Context(), 1*time.Minute)
76
defer cancel()
77
78
supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))
79
if err != nil {
80
log.WithError(err).Print("error connecting to supervisor")
81
return GpError{Err: xerrors.Errorf("error connecting to supervisor: %w", err), Silence: true, ExitCode: &exitCode}
82
}
83
84
resp, err := supervisor.NewTokenServiceClient(supervisorConn).GetToken(ctx, &supervisor.GetTokenRequest{
85
Host: host,
86
Kind: "git",
87
})
88
if err != nil {
89
log.WithError(err).Print("error getting token from supervisor")
90
return GpError{Err: xerrors.Errorf("error getting token from supervisor: %w", err), Silence: true, ExitCode: &exitCode}
91
}
92
93
user = resp.User
94
token = resp.Token
95
96
gitCmdInfo := &gitCommandInfo{}
97
err = walkProcessTree(os.Getpid(), func(proc procfs.Proc) bool {
98
cmdLine, err := proc.CmdLine()
99
if err != nil {
100
log.WithError(err).Print("error reading proc cmdline")
101
return true
102
}
103
104
cmdLineString := strings.Join(cmdLine, " ")
105
log.Printf("cmdLineString -> %v", cmdLineString)
106
gitCmdInfo.parseGitCommandAndRemote(cmdLineString)
107
108
return gitCmdInfo.Ok()
109
})
110
if err != nil {
111
return GpError{Err: xerrors.Errorf("error walking process tree: %w", err), Silence: true, ExitCode: &exitCode}
112
}
113
if !gitCmdInfo.Ok() {
114
log.Warn(`Could not detect "RepoUrl" and or "GitCommand", token validation will not be performed`)
115
return nil
116
}
117
118
// Starts another process which tracks the executed git event
119
gitCommandTracker := exec.Command("/proc/self/exe", "git-track-command", "--gitCommand", gitCmdInfo.GitCommand)
120
err = gitCommandTracker.Start()
121
if err != nil {
122
log.WithError(err).Print("error spawning tracker")
123
} else {
124
err = gitCommandTracker.Process.Release()
125
if err != nil {
126
log.WithError(err).Print("error releasing tracker")
127
}
128
}
129
130
validator := exec.Command(
131
"/proc/self/exe",
132
"git-token-validator",
133
"--user", resp.User,
134
"--token", resp.Token,
135
"--scopes", strings.Join(resp.Scope, ","),
136
"--host", host,
137
"--repoURL", gitCmdInfo.RepoUrl,
138
"--gitCommand", gitCmdInfo.GitCommand,
139
)
140
err = validator.Start()
141
if err != nil {
142
return GpError{Err: xerrors.Errorf("error spawning validator: %w", err), Silence: true, ExitCode: &exitCode}
143
}
144
err = validator.Process.Release()
145
if err != nil {
146
log.WithError(err).Print("error releasing validator")
147
return GpError{Err: xerrors.Errorf("error releasing validator: %w", err), Silence: true, ExitCode: &exitCode}
148
}
149
return nil
150
},
151
}
152
153
func parseFromStdin() (map[string]string, error) {
154
result := make(map[string]string)
155
scanner := bufio.NewScanner(os.Stdin)
156
for scanner.Scan() {
157
line := strings.TrimSpace(scanner.Text())
158
if len(line) > 0 {
159
tuple := strings.Split(line, "=")
160
if len(tuple) == 2 {
161
result[tuple[0]] = strings.TrimSpace(tuple[1])
162
}
163
}
164
}
165
if err := scanner.Err(); err != nil {
166
return nil, err
167
}
168
return result, nil
169
}
170
171
type gitCommandInfo struct {
172
RepoUrl string
173
GitCommand string
174
}
175
176
func (g *gitCommandInfo) Ok() bool {
177
return g.RepoUrl != "" && g.GitCommand != ""
178
}
179
180
var gitCommandRegExp = regexp.MustCompile(`git(?:\s+(?:\S+\s+)*)(push|clone|fetch|pull|diff|ls-remote)(?:\s+(?:\S+\s+)*)?`)
181
var repoUrlRegExp = regexp.MustCompile(`remote-https?\s([^\s]+)\s+(https?:[^\s]+)`)
182
183
// This method needs to be called multiple times to fill all the required info
184
// from different git commands
185
// For example from first command below the `RepoUrl` will be parsed and from
186
// the second command the `GitCommand` will be parsed
187
// `/usr/lib/git-core/git-remote-https origin https://github.com/jeanp413/test-gp-bug.git`
188
// `/usr/lib/git-core/git push`
189
func (g *gitCommandInfo) parseGitCommandAndRemote(cmdLineString string) {
190
matchCommand := gitCommandRegExp.FindStringSubmatch(cmdLineString)
191
if len(matchCommand) == 2 {
192
g.GitCommand = matchCommand[1]
193
}
194
195
matchRepo := repoUrlRegExp.FindStringSubmatch(cmdLineString)
196
if len(matchRepo) == 3 {
197
g.RepoUrl = matchRepo[2]
198
if !strings.HasSuffix(g.RepoUrl, ".git") {
199
g.RepoUrl = g.RepoUrl + ".git"
200
}
201
}
202
}
203
204
type pidCallbackFn func(procfs.Proc) bool
205
206
func walkProcessTree(pid int, fn pidCallbackFn) error {
207
for {
208
proc, err := procfs.NewProc(pid)
209
if err != nil {
210
return err
211
}
212
213
stop := fn(proc)
214
if stop {
215
return nil
216
}
217
218
procStat, err := proc.Stat()
219
if err != nil {
220
return err
221
}
222
if procStat.PPID == pid || procStat.PPID == 1 /* supervisor pid*/ {
223
return nil
224
}
225
pid = procStat.PPID
226
}
227
}
228
229
// How to smoke test:
230
// - 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
231
// - 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
232
// - Private npm package
233
// - Create a private git repository for an npm package e.g https://github.com/jeanp413/test-private-package
234
// - Start a workspace, then run `npm install github:jeanp413/test-private-package` with and without permissions in the dashboard
235
//
236
// - Private npm package no access
237
// - Open this workspace https://github.com/jeanp413/test-gp-bug and run `npm install`
238
// - Observe NO notification with this message appears `Unknown repository ” Please grant the necessary permissions.`
239
//
240
// - Clone private repo without permission
241
// - 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`
242
func init() {
243
rootCmd.AddCommand(credentialHelper)
244
}
245
246