11package git
22
33import (
4+ "context"
5+ "crypto/sha256"
46 "fmt"
57 "io"
68 "io/ioutil"
79 "os"
10+ "strconv"
811 "strings"
12+ "time"
13+
14+ gocache "github.com/patrickmn/go-cache"
915
1016 argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
17+ "github.com/bradleyfalzon/ghinstallation"
1118 log "github.com/sirupsen/logrus"
1219
20+ "github.com/argoproj/argo-cd/v2/common"
21+
1322 certutil "github.com/argoproj/argo-cd/v2/util/cert"
1423)
1524
25+ // In memory cache for storing github APP api token credentials
26+ var (
27+ githubAppTokenCache * gocache.Cache
28+ )
29+
30+ func init () {
31+ githubAppCredsExp := common .GithubAppCredsExpirationDuration
32+ if exp := os .Getenv (common .EnvGithubAppCredsExpirationDuration ); exp != "" {
33+ if qps , err := strconv .Atoi (exp ); err != nil {
34+ githubAppCredsExp = time .Duration (qps ) * time .Minute
35+ }
36+ }
37+
38+ githubAppTokenCache = gocache .New (githubAppCredsExp , 1 * time .Minute )
39+ }
40+
1641type Creds interface {
1742 Environ () (io.Closer , []string , error )
1843}
@@ -25,13 +50,26 @@ func (c NopCloser) Close() error {
2550 return nil
2651}
2752
53+ var _ Creds = NopCreds {}
54+
2855type NopCreds struct {
2956}
3057
3158func (c NopCreds ) Environ () (io.Closer , []string , error ) {
3259 return NopCloser {}, nil , nil
3360}
3461
62+ var _ io.Closer = NopCloser {}
63+
64+ type GenericHTTPSCreds interface {
65+ HasClientCert () bool
66+ GetClientCertData () string
67+ GetClientCertKey () string
68+ Environ () (io.Closer , []string , error )
69+ }
70+
71+ var _ GenericHTTPSCreds = HTTPSCreds {}
72+
3573// HTTPS creds implementation
3674type HTTPSCreds struct {
3775 // Username for authentication
@@ -44,15 +82,18 @@ type HTTPSCreds struct {
4482 clientCertData string
4583 // Client certificate key to use
4684 clientCertKey string
85+ // HTTP/HTTPS proxy used to access repository
86+ proxy string
4787}
4888
49- func NewHTTPSCreds (username string , password string , clientCertData string , clientCertKey string , insecure bool ) HTTPSCreds {
89+ func NewHTTPSCreds (username string , password string , clientCertData string , clientCertKey string , insecure bool , proxy string ) GenericHTTPSCreds {
5090 return HTTPSCreds {
5191 username ,
5292 password ,
5393 insecure ,
5494 clientCertData ,
5595 clientCertKey ,
96+ proxy ,
5697 }
5798}
5899
@@ -71,7 +112,7 @@ func (c HTTPSCreds) Environ() (io.Closer, []string, error) {
71112 // In case the repo is configured for using a TLS client cert, we need to make
72113 // sure git client will use it. The certificate's key must not be password
73114 // protected.
74- if c .clientCertData != "" && c . clientCertKey != "" {
115+ if c .HasClientCert () {
75116 var certFile , keyFile * os.File
76117
77118 // We need to actually create two temp files, one for storing cert data and
@@ -116,6 +157,18 @@ func (c HTTPSCreds) Environ() (io.Closer, []string, error) {
116157 return httpCloser , env , nil
117158}
118159
160+ func (g HTTPSCreds ) HasClientCert () bool {
161+ return g .clientCertData != "" && g .clientCertKey != ""
162+ }
163+
164+ func (c HTTPSCreds ) GetClientCertData () string {
165+ return c .clientCertData
166+ }
167+
168+ func (c HTTPSCreds ) GetClientCertKey () string {
169+ return c .clientCertKey
170+ }
171+
119172// SSH implementation
120173type SSHCreds struct {
121174 sshPrivateKey string
@@ -179,3 +232,144 @@ func (c SSHCreds) Environ() (io.Closer, []string, error) {
179232 env = append (env , []string {fmt .Sprintf ("GIT_SSH_COMMAND=%s" , strings .Join (args , " " ))}... )
180233 return sshPrivateKeyFile (file .Name ()), env , nil
181234}
235+
236+ // GitHubAppCreds to authenticate as GitHub application
237+ type GitHubAppCreds struct {
238+ appID int64
239+ appInstallId int64
240+ privateKey string
241+ baseURL string
242+ repoURL string
243+ clientCertData string
244+ clientCertKey string
245+ insecure bool
246+ proxy string
247+ }
248+
249+ // NewGitHubAppCreds provide github app credentials
250+ func NewGitHubAppCreds (appID int64 , appInstallId int64 , privateKey string , baseURL string , repoURL string , clientCertData string , clientCertKey string , insecure bool ) GenericHTTPSCreds {
251+ return GitHubAppCreds {appID : appID , appInstallId : appInstallId , privateKey : privateKey , baseURL : baseURL , repoURL : repoURL , clientCertData : clientCertData , clientCertKey : clientCertKey , insecure : insecure }
252+ }
253+
254+ func (g GitHubAppCreds ) Environ () (io.Closer , []string , error ) {
255+ token , err := g .getAccessToken ()
256+ if err != nil {
257+ return NopCloser {}, nil , err
258+ }
259+
260+ env := []string {fmt .Sprintf ("GIT_ASKPASS=%s" , "git-ask-pass.sh" ), "GIT_USERNAME=x-access-token" , fmt .Sprintf ("GIT_PASSWORD=%s" , token )}
261+ httpCloser := authFilePaths (make ([]string , 0 ))
262+
263+ // GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at
264+ // all.
265+ if g .insecure {
266+ env = append (env , "GIT_SSL_NO_VERIFY=true" )
267+ }
268+
269+ // In case the repo is configured for using a TLS client cert, we need to make
270+ // sure git client will use it. The certificate's key must not be password
271+ // protected.
272+ if g .HasClientCert () {
273+ var certFile , keyFile * os.File
274+
275+ // We need to actually create two temp files, one for storing cert data and
276+ // another for storing the key. If we fail to create second fail, the first
277+ // must be removed.
278+ certFile , err := ioutil .TempFile (argoio .TempDir , "" )
279+ if err == nil {
280+ defer certFile .Close ()
281+ keyFile , err = ioutil .TempFile (argoio .TempDir , "" )
282+ if err != nil {
283+ removeErr := os .Remove (certFile .Name ())
284+ if removeErr != nil {
285+ log .Errorf ("Could not remove previously created tempfile %s: %v" , certFile .Name (), removeErr )
286+ }
287+ return NopCloser {}, nil , err
288+ }
289+ defer keyFile .Close ()
290+ } else {
291+ return NopCloser {}, nil , err
292+ }
293+
294+ // We should have both temp files by now
295+ httpCloser = authFilePaths ([]string {certFile .Name (), keyFile .Name ()})
296+
297+ _ , err = certFile .WriteString (g .clientCertData )
298+ if err != nil {
299+ httpCloser .Close ()
300+ return NopCloser {}, nil , err
301+ }
302+ // GIT_SSL_CERT is the full path to a client certificate to be used
303+ env = append (env , fmt .Sprintf ("GIT_SSL_CERT=%s" , certFile .Name ()))
304+
305+ _ , err = keyFile .WriteString (g .clientCertKey )
306+ if err != nil {
307+ httpCloser .Close ()
308+ return NopCloser {}, nil , err
309+ }
310+ // GIT_SSL_KEY is the full path to a client certificate's key to be used
311+ env = append (env , fmt .Sprintf ("GIT_SSL_KEY=%s" , keyFile .Name ()))
312+
313+ }
314+ return httpCloser , env , nil
315+ }
316+
317+ // getAccessToken fetches GitHub token using the app id, install id, and private key.
318+ // the token is then cached for re-use.
319+ func (g GitHubAppCreds ) getAccessToken () (string , error ) {
320+ // Timeout
321+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
322+ defer cancel ()
323+
324+ // Compute hash of creds for lookup in cache
325+ h := sha256 .New ()
326+ _ , err := h .Write ([]byte (fmt .Sprintf ("%s %d %d %s" , g .privateKey , g .appID , g .appInstallId , g .baseURL )))
327+ if err != nil {
328+ return "" , err
329+ }
330+ key := fmt .Sprintf ("%x" , h .Sum (nil ))
331+
332+ // Check cache for GitHub transport which helps fetch an API token
333+ t , found := githubAppTokenCache .Get (key )
334+ if found {
335+ itr := t .(* ghinstallation.Transport )
336+ // This method caches the token and if it's expired retrieves a new one
337+ return itr .Token (ctx )
338+ }
339+
340+ // GitHub API url
341+ baseUrl := "https://api.github.com"
342+ if g .baseURL != "" {
343+ baseUrl = strings .TrimSuffix (g .baseURL , "/" )
344+ }
345+
346+ // Create a new GitHub transport
347+ c := GetRepoHTTPClient (baseUrl , g .insecure , g , g .proxy )
348+ itr , err := ghinstallation .New (c .Transport ,
349+ g .appID ,
350+ g .appInstallId ,
351+ []byte (g .privateKey ),
352+ )
353+ if err != nil {
354+ return "" , err
355+ }
356+
357+ itr .BaseURL = baseUrl
358+
359+ // Add transport to cache
360+ githubAppTokenCache .Set (key , itr , time .Minute * 60 )
361+
362+ return itr .Token (ctx )
363+ }
364+
365+ func (g GitHubAppCreds ) HasClientCert () bool {
366+ return g .clientCertData != "" && g .clientCertKey != ""
367+ }
368+
369+ func (g GitHubAppCreds ) GetClientCertData () string {
370+ return g .clientCertData
371+ }
372+
373+ func (g GitHubAppCreds ) GetClientCertKey () string {
374+ return g .clientCertKey
375+ }
0 commit comments