From 76cb6b70bf110ced5d043ed16ba9f2465b9b18e1 Mon Sep 17 00:00:00 2001 From: Dipti Pai Date: Fri, 11 Oct 2024 09:22:38 -0700 Subject: [PATCH] [RFC-007] Implement GitHub app authentication for git repositories. - Add github app based authentication method to fetch installation token in auth package. - Add unit tests to test the github app authentication - Add github provider options in git package. - Use the github provider to clone from go-git package. - Add unit tests to fetch git credentials and cloning the repository using github app authentication. - Add e2e tests to test pull/push to git repositories using github app authentication. - Update the github workflow to run e2etests from CI. Signed-off-by: Dipti Pai --- .github/workflows/e2e.yaml | 3 + auth/github/client.go | 176 ++++++++++++++++++++++++ auth/github/client_test.go | 228 ++++++++++++++++++++++++++++++++ auth/go.mod | 7 +- auth/go.sum | 14 ++ git/credentials.go | 27 +++- git/credentials_test.go | 78 +++++++++++ git/go.mod | 5 + git/go.sum | 14 ++ git/gogit/client.go | 37 +++++- git/gogit/client_test.go | 153 ++++++++++++++++++--- git/gogit/go.mod | 4 + git/gogit/go.sum | 10 ++ git/internal/e2e/README.md | 34 ++++- git/internal/e2e/github_test.go | 110 ++++++++++++--- git/internal/e2e/go.mod | 7 +- git/internal/e2e/go.sum | 6 + git/options.go | 6 +- oci/tests/integration/go.mod | 4 + oci/tests/integration/go.sum | 9 ++ 20 files changed, 877 insertions(+), 55 deletions(-) create mode 100644 auth/github/client.go create mode 100644 auth/github/client_test.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 08f6fd3c..ec78500e 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -61,6 +61,9 @@ jobs: export GITHUB_USER="fluxcd-gitprovider-bot" export GITHUB_ORG="fluxcd-testing" export GITHUB_TOKEN="${{ secrets.GITPROVIDER_BOT_TOKEN }}" + export GHAPP_ID="${{ secrets.GHAPP_ID }}" + export GHAPP_INSTALL_ID="${{ secrets.GHAPP_INSTALL_ID }}" + export GHAPP_PRIVATE_KEY="${{ secrets.GHAPP_PRIVATE_KEY }}" elif [[ ${{ matrix.provider }} = "gitlab" ]]; then export GO_TEST_PREFIX="TestGitLabE2E" export GITLAB_USER="fluxcd-gitprovider-bot" diff --git a/auth/github/client.go b/auth/github/client.go new file mode 100644 index 00000000..00b95b45 --- /dev/null +++ b/auth/github/client.go @@ -0,0 +1,176 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/bradleyfalzon/ghinstallation/v2" + "golang.org/x/net/http/httpproxy" +) + +const ( + AppIDKey = "githubAppID" + AppInstallationIDKey = "githubAppInstallationID" + AppPrivateKey = "githubAppPrivateKey" + AppBaseUrl = "githubAppBaseURL" +) + +// Client is an authentication provider for GitHub Apps. +type Client struct { + appID string + installationID string + privateKey []byte + apiURL string + proxyURL *url.URL + ghTransport *ghinstallation.Transport +} + +// OptFunc enables specifying options for the provider. +type OptFunc func(*Client) + +// New returns a new authentication provider for GitHub Apps. +func New(opts ...OptFunc) (*Client, error) { + var err error + + p := &Client{} + for _, opt := range opts { + opt(p) + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + if p.proxyURL != nil { + proxyStr := p.proxyURL.String() + proxyConfig := &httpproxy.Config{ + HTTPProxy: proxyStr, + HTTPSProxy: proxyStr, + } + proxyFunc := func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } + transport.Proxy = proxyFunc + } + + appID, err := strconv.Atoi(p.appID) + if err != nil { + return nil, fmt.Errorf("invalid app id, err: %v", err) + } + installationID, err := strconv.Atoi(p.installationID) + if err != nil { + return nil, fmt.Errorf("invalid app installation id, err: %v", err) + } + + p.ghTransport, err = ghinstallation.New(transport, int64(appID), int64(installationID), p.privateKey) + if err != nil { + return nil, err + } + + if p.apiURL != "" { + p.ghTransport.BaseURL = p.apiURL + } + + return p, nil +} + +// WithInstallationID configures the installation ID of the GitHub App. +func WithInstllationID(installationID string) OptFunc { + return func(p *Client) { + p.installationID = installationID + } +} + +// WithAppID configures the app ID of the GitHub App. +func WithAppID(appID string) OptFunc { + return func(p *Client) { + p.appID = appID + } +} + +// WithPrivateKey configures the private key of the GitHub App. +func WithPrivateKey(pk []byte) OptFunc { + return func(p *Client) { + p.privateKey = pk + } +} + +// WithAppBaseURL configures the GitHub API endpoint to use to fetch GitHub App +// installation token. +func WithAppBaseURL(appBaseURL string) OptFunc { + return func(p *Client) { + p.apiURL = appBaseURL + } +} + +// WithAppData configures the client using data from a map +func WithAppData(appData map[string][]byte) OptFunc { + return func(p *Client) { + val, ok := appData[AppIDKey] + if ok { + p.appID = string(val) + } + val, ok = appData[AppInstallationIDKey] + if ok { + p.installationID = string(val) + } + val, ok = appData[AppPrivateKey] + if ok { + p.privateKey = val + } + val, ok = appData[AppBaseUrl] + if ok { + p.apiURL = string(val) + } + } +} + +// WithProxyURL sets the proxy URL to use with the transport. +func WithProxyURL(proxyURL *url.URL) OptFunc { + return func(p *Client) { + p.proxyURL = proxyURL + } +} + +// AppToken contains a GitHub App installation token and its expiry. +type AppToken struct { + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` +} + +// GetToken returns the token that can be used to authenticate +// as a GitHub App installation. +// Ref: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation +func (p *Client) GetToken(ctx context.Context) (*AppToken, error) { + token, err := p.ghTransport.Token(ctx) + if err != nil { + return nil, err + } + + expiresAt, _, err := p.ghTransport.Expiry() + if err != nil { + return nil, err + } + + return &AppToken{ + Token: token, + ExpiresAt: expiresAt, + }, nil +} diff --git a/auth/github/client_test.go b/auth/github/client_test.go new file mode 100644 index 00000000..65967ffb --- /dev/null +++ b/auth/github/client_test.go @@ -0,0 +1,228 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/fluxcd/pkg/ssh" + . "github.com/onsi/gomega" +) + +func TestClient_Options(t *testing.T) { + appID := "123" + installationID := "456" + kp, _ := ssh.GenerateKeyPair(ssh.RSA_4096) + gitHubDefaultURL := "https://api.github.com" + gitHubEnterpriseURL := "https://github.example.com/api/v3" + proxy, _ := url.Parse("http://localhost:8080") + + tests := []struct { + name string + opts []OptFunc + wantErr error + }{ + { + name: "Create new client", + opts: []OptFunc{WithInstllationID(installationID), WithAppID(appID), WithPrivateKey(kp.PrivateKey)}, + }, + { + name: "Create new client with proxy", + opts: []OptFunc{WithInstllationID(installationID), WithAppID(appID), WithPrivateKey(kp.PrivateKey), WithProxyURL((proxy))}, + }, + { + name: "Create new client with custom api url", + opts: []OptFunc{WithAppBaseURL(gitHubEnterpriseURL), WithInstllationID(installationID), WithAppID(appID), WithPrivateKey(kp.PrivateKey)}, + }, + { + name: "Create new client with app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(appID), + AppInstallationIDKey: []byte(installationID), + AppPrivateKey: kp.PrivateKey, + }, + )}, + }, + { + name: "Create new client with empty data", + opts: []OptFunc{WithAppData(map[string][]byte{})}, + wantErr: errors.New("invalid app id, err: strconv.Atoi: parsing \"\": invalid syntax"), + }, + { + name: "Create new client with app data with missing AppID Key", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppInstallationIDKey: []byte(installationID), + AppPrivateKey: kp.PrivateKey, + }, + )}, + wantErr: errors.New("invalid app id, err: strconv.Atoi: parsing \"\": invalid syntax"), + }, + { + name: "Create new client with app data with missing AppInstallationIDKey Key", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte("123"), + AppPrivateKey: kp.PrivateKey, + }, + )}, + wantErr: errors.New("invalid app installation id, err: strconv.Atoi: parsing \"\": invalid syntax"), + }, + { + name: "Create new client with app data with missing private Key", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(appID), + AppInstallationIDKey: []byte(installationID), + }, + )}, + wantErr: errors.New("could not parse private key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key"), + }, + { + name: "Create new client with invalid appID in app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte("abc"), + AppInstallationIDKey: []byte(installationID), + AppPrivateKey: kp.PrivateKey, + }, + )}, + wantErr: errors.New("invalid app id, err: strconv.Atoi: parsing \"abc\": invalid syntax"), + }, + { + name: "Create new client with invalid installationID in app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(appID), + AppInstallationIDKey: []byte("abc"), + AppPrivateKey: kp.PrivateKey, + }, + )}, + wantErr: errors.New("invalid app installation id, err: strconv.Atoi: parsing \"abc\": invalid syntax"), + }, + { + name: "Create new client with invalid private key in app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(appID), + AppInstallationIDKey: []byte(installationID), + AppPrivateKey: []byte(""), + }, + )}, + wantErr: errors.New("could not parse private key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key"), + }, + { + name: "Create new client with no private key option", + opts: []OptFunc{WithInstllationID(installationID), WithAppID(appID)}, + wantErr: errors.New("could not parse private key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + opts := tt.opts + + client, err := New(opts...) + if tt.wantErr != nil { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(Equal(tt.wantErr)) + } else { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(client.appID).To(Equal(appID)) + g.Expect(client.installationID).To(Equal(installationID)) + g.Expect(client.privateKey).To(Equal(kp.PrivateKey)) + + if client.apiURL != "" { + g.Expect(client.apiURL).To(Equal(gitHubEnterpriseURL)) + g.Expect(client.ghTransport.BaseURL).To(Equal(gitHubEnterpriseURL)) + } else { + g.Expect(client.ghTransport.BaseURL).To(Equal(gitHubDefaultURL)) + } + } + }) + } +} + +func TestClient_GetToken(t *testing.T) { + expiresAt := time.Now().UTC().Add(time.Hour) + tests := []struct { + name string + accessToken *AppToken + statusCode int + wantErr bool + wantAppToken *AppToken + }{ + { + name: "Get valid token", + accessToken: &AppToken{ + Token: "access-token", + ExpiresAt: expiresAt, + }, + statusCode: http.StatusOK, + wantAppToken: &AppToken{ + Token: "access-token", + ExpiresAt: expiresAt, + }, + }, + { + name: "Failure in getting token", + statusCode: http.StatusInternalServerError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + var response []byte + var err error + if tt.accessToken != nil { + response, err = json.Marshal(tt.accessToken) + g.Expect(err).ToNot(HaveOccurred()) + } + w.Write(response) + } + srv := httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) + + kp, err := ssh.GenerateKeyPair(ssh.RSA_4096) + g.Expect(err).ToNot(HaveOccurred()) + opts := []OptFunc{ + WithAppBaseURL(srv.URL), WithInstllationID("123"), WithAppID("456"), WithPrivateKey(kp.PrivateKey), + } + + provider, err := New(opts...) + g.Expect(err).ToNot(HaveOccurred()) + + appToken, err := provider.GetToken(context.TODO()) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(appToken.Token).To(Equal(tt.wantAppToken.Token)) + g.Expect(appToken.ExpiresAt).To(Equal(tt.wantAppToken.ExpiresAt)) + } + }) + } +} diff --git a/auth/go.mod b/auth/go.mod index 5d1a1817..1ead063c 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -5,21 +5,26 @@ go 1.22.4 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 + github.com/fluxcd/pkg/ssh v0.14.1 github.com/onsi/gomega v1.34.2 + golang.org/x/net v0.29.0 ) require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/auth/go.sum b/auth/go.sum index ed666c8f..eba27e34 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -6,16 +6,27 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fluxcd/pkg/ssh v0.14.1 h1:C/RBDch6cxAqQtaOohcasSAeGfZznNEeZtvpfI+hXQY= +github.com/fluxcd/pkg/ssh v0.14.1/go.mod h1:HsVzHyF7CkfTnjtLEI6XK+8tfyWqwI1TPxJ34HcMg2o= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -45,10 +56,13 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/git/credentials.go b/git/credentials.go index f1eae0e0..1429b43d 100644 --- a/git/credentials.go +++ b/git/credentials.go @@ -22,16 +22,22 @@ import ( "time" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/github" ) const ( - ProviderAzure = "azure" + ProviderAzure = "azure" + ProviderGitHub = "github" + + GitHubAccessTokenUsername = "x-access-token" ) // Credentials contains authentication data needed in order to access a Git // repository. type Credentials struct { BearerToken string + Username string + Password string } // GetCredentials returns authentication credentials for accessing the provided @@ -67,6 +73,25 @@ func GetCredentials(ctx context.Context, providerOpts *ProviderOptions) (*Creden BearerToken: accessToken.Token, } return &creds, accessToken.ExpiresOn, nil + case ProviderGitHub: + opts := providerOpts.GitHubOpts + if providerOpts.GitHubOpts == nil { + return nil, expiresOn, fmt.Errorf("provider options are not specified for GitHub") + } + client, err := github.New(opts...) + if err != nil { + return nil, expiresOn, err + } + appToken, err := client.GetToken(ctx) + if err != nil { + return nil, expiresOn, err + } + + creds = Credentials{ + Username: GitHubAccessTokenUsername, + Password: appToken.Token, + } + return &creds, appToken.ExpiresAt, nil default: return nil, expiresOn, fmt.Errorf("invalid provider") } diff --git a/git/credentials_test.go b/git/credentials_test.go index e7831b8e..db529bdc 100644 --- a/git/credentials_test.go +++ b/git/credentials_test.go @@ -18,11 +18,16 @@ package git import ( "context" + "encoding/json" "errors" + "net/http" + "net/http/httptest" "testing" "time" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/github" + "github.com/fluxcd/pkg/ssh" . "github.com/onsi/gomega" ) @@ -98,3 +103,76 @@ func TestGetCredentials(t *testing.T) { }) } } + +func TestGetCredentials_GitHub(t *testing.T) { + expiresAt := time.Now().UTC().Add(time.Hour) + tests := []struct { + name string + accessToken *github.AppToken + statusCode int + wantCredentials *Credentials + wantErr bool + }{ + { + name: "get credentials from github success", + statusCode: http.StatusOK, + accessToken: &github.AppToken{ + Token: "access-token", + ExpiresAt: expiresAt, + }, + wantCredentials: &Credentials{ + Username: GitHubAccessTokenUsername, + Password: "access-token", + }, + }, + { + name: "get credentials from github failure", + statusCode: http.StatusInternalServerError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + var response []byte + var err error + if tt.accessToken != nil { + response, err = json.Marshal(tt.accessToken) + g.Expect(err).ToNot(HaveOccurred()) + } + w.Write(response) + } + srv := httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) + + kp, err := ssh.GenerateKeyPair(ssh.RSA_4096) + g.Expect(err).ToNot(HaveOccurred()) + + providerOpts := &ProviderOptions{ + Name: ProviderGitHub, + GitHubOpts: []github.OptFunc{github.WithAppBaseURL(srv.URL), github.WithAppID("123"), + github.WithInstllationID("456"), github.WithPrivateKey(kp.PrivateKey)}, + } + + creds, expiry, err := GetCredentials(context.TODO(), providerOpts) + if tt.wantCredentials != nil { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(*creds).To(Equal(*tt.wantCredentials)) + + g.Expect(creds.Username).To(Equal(tt.wantCredentials.Username)) + g.Expect(creds.Password).To(Equal(tt.wantCredentials.Password)) + g.Expect(expiry).To(Equal(expiresAt)) + } else { + g.Expect(creds).To(BeNil()) + g.Expect(err).To(HaveOccurred()) + } + }) + } + +} diff --git a/git/go.mod b/git/go.mod index 935f41b0..2935ac9c 100644 --- a/git/go.mod +++ b/git/go.mod @@ -8,6 +8,7 @@ require ( github.com/ProtonMail/go-crypto v1.0.0 github.com/cyphar/filepath-securejoin v0.3.2 github.com/fluxcd/pkg/auth v0.0.1 + github.com/fluxcd/pkg/ssh v0.14.1 github.com/onsi/gomega v1.34.2 ) @@ -16,9 +17,13 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/git/go.sum b/git/go.sum index b6a90147..ba1de76a 100644 --- a/git/go.sum +++ b/git/go.sum @@ -8,6 +8,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= @@ -16,14 +18,23 @@ github.com/cyphar/filepath-securejoin v0.3.2 h1:QhZu5AxQ+o1XZH0Ye05YzvJ0kAdK6VQc github.com/cyphar/filepath-securejoin v0.3.2/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fluxcd/pkg/ssh v0.14.1 h1:C/RBDch6cxAqQtaOohcasSAeGfZznNEeZtvpfI+hXQY= +github.com/fluxcd/pkg/ssh v0.14.1/go.mod h1:HsVzHyF7CkfTnjtLEI6XK+8tfyWqwI1TPxJ34HcMg2o= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -83,6 +94,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -98,6 +111,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/git/gogit/client.go b/git/gogit/client.go index b10a66fa..c280101a 100644 --- a/git/gogit/client.go +++ b/git/gogit/client.go @@ -40,6 +40,7 @@ import ( "github.com/go-git/go-git/v5/storage/memory" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/github" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/repository" ) @@ -207,7 +208,7 @@ func WithProxy(opts transport.ProxyOptions) ClientOption { } func (g *Client) Init(ctx context.Context, url, branch string) error { - if err := g.validateUrl(url); err != nil { + if err := g.validateUrlAndAuthOptions(url); err != nil { return err } @@ -248,11 +249,11 @@ func (g *Client) Init(ctx context.Context, url, branch string) error { } func (g *Client) Clone(ctx context.Context, url string, cfg repository.CloneConfig) (*git.Commit, error) { - if err := g.providerAuth(ctx); err != nil { + if err := g.validateUrlAndAuthOptions(url); err != nil { return nil, err } - if err := g.validateUrl(url); err != nil { + if err := g.providerAuth(ctx); err != nil { return nil, err } @@ -279,12 +280,17 @@ func (g *Client) clone(ctx context.Context, url string, cfg repository.CloneConf } } -func (g *Client) validateUrl(u string) error { +// validateUrlAndAuthOptions performs validations on the input url and auth options. +// Validations are performed taking into consideration that ProviderAzure sets +// the bearer token in authOpts and ProviderGitHub sets username/password in +// authOpts. +func (g *Client) validateUrlAndAuthOptions(u string) error { ru, err := url.Parse(u) if err != nil { return fmt.Errorf("cannot parse url: %w", err) } + hasAzureProvider := g.hasProvider(git.ProviderAzure) if g.authOpts != nil { httpOrHttps := g.authOpts.Transport == git.HTTP || g.authOpts.Transport == git.HTTPS hasUsernameOrPassword := g.authOpts.Username != "" || g.authOpts.Password != "" @@ -293,6 +299,10 @@ func (g *Client) validateUrl(u string) error { if httpOrHttps && hasBearerToken && hasUsernameOrPassword { return errors.New("basic auth and bearer token cannot be set at the same time") } + + if httpOrHttps && hasUsernameOrPassword && hasAzureProvider { + return errors.New("basic auth and provider cannot be set at the same time") + } } if g.credentialsOverHTTP { @@ -309,14 +319,23 @@ func (g *Client) validateUrl(u string) error { return errors.New("basic auth cannot be sent over HTTP") } else if g.authOpts.BearerToken != "" { return errors.New("bearer token cannot be sent over HTTP") + } else if g.hasProvider(git.ProviderGitHub) { + return errors.New("github provider cannot be used with HTTP") + } else if hasAzureProvider { + return errors.New("azure provider cannot be used with HTTP") } } return nil } +func (g *Client) hasProvider(name string) bool { + return g.authOpts != nil && g.authOpts.ProviderOpts != nil && g.authOpts.ProviderOpts.Name == name +} + func (g *Client) providerAuth(ctx context.Context) error { - if g.authOpts != nil && g.authOpts.ProviderOpts != nil && g.authOpts.BearerToken == "" { + if g.authOpts != nil && g.authOpts.ProviderOpts != nil && g.authOpts.BearerToken == "" && + g.authOpts.Username == "" && g.authOpts.Password == "" { if g.proxy.URL != "" { proxyURL, err := g.proxy.FullURL() if err != nil { @@ -325,6 +344,8 @@ func (g *Client) providerAuth(ctx context.Context) error { switch g.authOpts.ProviderOpts.Name { case git.ProviderAzure: g.authOpts.ProviderOpts.AzureOpts = append(g.authOpts.ProviderOpts.AzureOpts, azure.WithProxyURL(proxyURL)) + case git.ProviderGitHub: + g.authOpts.ProviderOpts.GitHubOpts = append(g.authOpts.ProviderOpts.GitHubOpts, github.WithProxyURL(proxyURL)) default: return fmt.Errorf("invalid provider") } @@ -335,6 +356,8 @@ func (g *Client) providerAuth(ctx context.Context) error { return err } g.authOpts.BearerToken = providerCreds.BearerToken + g.authOpts.Username = providerCreds.Username + g.authOpts.Password = providerCreds.Password } return nil @@ -419,6 +442,10 @@ func (g *Client) Push(ctx context.Context, cfg repository.PushConfig) error { return git.ErrNoGitRepository } + if err := g.providerAuth(ctx); err != nil { + return err + } + authMethod, err := transportAuth(g.authOpts, g.useDefaultKnownHosts) if err != nil { return fmt.Errorf("failed to construct auth method with options: %w", err) diff --git a/git/gogit/client_test.go b/git/gogit/client_test.go index aafb4f70..8a887fa1 100644 --- a/git/gogit/client_test.go +++ b/git/gogit/client_test.go @@ -18,8 +18,11 @@ package gogit import ( "context" + "encoding/json" "errors" "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -32,9 +35,11 @@ import ( . "github.com/onsi/gomega" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/github" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/repository" "github.com/fluxcd/pkg/gittestserver" + "github.com/fluxcd/pkg/ssh" ) func TestNewClient(t *testing.T) { @@ -729,7 +734,7 @@ func TestValidateUrl(t *testing.T) { }, opts...) g.Expect(err).ToNot(HaveOccurred()) - err = ggc.validateUrl(tt.url) + err = ggc.validateUrlAndAuthOptions(tt.url) if tt.expectedError == "" { g.Expect(err).To(BeNil()) @@ -741,7 +746,7 @@ func TestValidateUrl(t *testing.T) { } } -func TestProviderAuth(t *testing.T) { +func TestProviderAuthValidations(t *testing.T) { expiresAt := time.Now().UTC().Add(time.Hour) tests := []struct { name string @@ -751,6 +756,8 @@ func TestProviderAuth(t *testing.T) { wantAuthErr error wantValidationErr error wantBearerToken string + wantUsername string + wantPassword string }{ { name: "nil authopts", @@ -821,6 +828,7 @@ func TestProviderAuth(t *testing.T) { }, { name: "authopts with provider and error", + url: "https://url", authOpts: &git.AuthOptions{ ProviderOpts: &git.ProviderOptions{ Name: git.ProviderAzure, @@ -843,7 +851,7 @@ func TestProviderAuth(t *testing.T) { wantAuthErr: errors.New("invalid provider"), }, { - name: "authopts with provider and username/password/https", + name: "authopts with azure provider and username/password/https", url: "https://url", authOpts: &git.AuthOptions{ ProviderOpts: &git.ProviderOptions{ @@ -860,11 +868,10 @@ func TestProviderAuth(t *testing.T) { Password: "password", Transport: git.HTTPS, }, - wantBearerToken: "ado-token", - wantValidationErr: errors.New("basic auth and bearer token cannot be set at the same time"), + wantValidationErr: errors.New("basic auth and provider cannot be set at the same time"), }, { - name: "authopts with provider and username/password/http", + name: "authopts with azure provider and username/password/http", url: "http://url", authOpts: &git.AuthOptions{ ProviderOpts: &git.ProviderOptions{ @@ -881,11 +888,10 @@ func TestProviderAuth(t *testing.T) { Password: "password", Transport: git.HTTP, }, - wantBearerToken: "ado-token", - wantValidationErr: errors.New("basic auth and bearer token cannot be set at the same time"), + wantValidationErr: errors.New("basic auth and provider cannot be set at the same time"), }, { - name: "authopts with provider and http", + name: "authopts with azure provider and http", url: "http://url", authOpts: &git.AuthOptions{ ProviderOpts: &git.ProviderOptions{ @@ -900,8 +906,34 @@ func TestProviderAuth(t *testing.T) { }, Transport: git.HTTP, }, - wantBearerToken: "ado-token", - wantValidationErr: errors.New("bearer token cannot be sent over HTTP"), + wantValidationErr: errors.New("azure provider cannot be used with HTTP"), + }, + { + name: "authopts with github provider and username/password/https", + url: "https://url", + authOpts: &git.AuthOptions{ + ProviderOpts: &git.ProviderOptions{ + Name: git.ProviderGitHub, + GitHubOpts: []github.OptFunc{}, + }, + Username: "user", + Password: "password", + Transport: git.HTTPS, + }, + wantUsername: "user", + wantPassword: "password", + }, + { + name: "authopts with github provider and http", + url: "http://url", + authOpts: &git.AuthOptions{ + ProviderOpts: &git.ProviderOptions{ + Name: git.ProviderGitHub, + GitHubOpts: []github.OptFunc{}, + }, + Transport: git.HTTP, + }, + wantValidationErr: errors.New("github provider cannot be used with HTTP"), }, } @@ -914,27 +946,108 @@ func TestProviderAuth(t *testing.T) { ggc, err := NewClient(t.TempDir(), tt.authOpts, opts...) g.Expect(err).ToNot(HaveOccurred()) - err = ggc.providerAuth(context.TODO()) - if tt.wantAuthErr != nil { + err = ggc.validateUrlAndAuthOptions(tt.url) + if tt.wantValidationErr != nil { g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(Equal(tt.wantAuthErr)) + g.Expect(err).To(Equal(tt.wantValidationErr)) } else { g.Expect(err).ToNot(HaveOccurred()) - if tt.authOpts != nil { - g.Expect(tt.authOpts.BearerToken).To(Equal(tt.wantBearerToken)) - } - err = ggc.validateUrl(tt.url) - if tt.wantValidationErr != nil { + err = ggc.providerAuth(context.TODO()) + if tt.wantAuthErr != nil { g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(Equal(tt.wantValidationErr)) + g.Expect(err).To(Equal(tt.wantAuthErr)) } else { g.Expect(err).ToNot(HaveOccurred()) + if tt.wantBearerToken != "" { + g.Expect(tt.authOpts.BearerToken).To(Equal(tt.wantBearerToken)) + } + if tt.wantUsername != "" { + g.Expect(tt.authOpts.Username).To(Equal(tt.wantUsername)) + } + if tt.wantPassword != "" { + g.Expect(tt.authOpts.Password).To(Equal(tt.authOpts.Password)) + } } } }) } } +func TestProviderAuth_GitHub(t *testing.T) { + expiresAt := time.Now().UTC().Add(time.Hour) + tests := []struct { + name string + statusCode int + accessToken *github.AppToken + wantUsername string + wantPassword string + wantErr bool + }{ + { + name: "test git provider auth success", + statusCode: http.StatusOK, + accessToken: &github.AppToken{ + Token: "access-token", + ExpiresAt: expiresAt, + }, + wantUsername: git.GitHubAccessTokenUsername, + wantPassword: "access-token", + }, + { + name: "test git provider auth failure", + statusCode: http.StatusInternalServerError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + var response []byte + var err error + if tt.accessToken != nil { + response, err = json.Marshal(tt.accessToken) + g.Expect(err).ToNot(HaveOccurred()) + } + w.Write(response) + } + srv := httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) + + kp, err := ssh.GenerateKeyPair(ssh.RSA_4096) + g.Expect(err).ToNot(HaveOccurred()) + authOpts := &git.AuthOptions{ + ProviderOpts: &git.ProviderOptions{ + Name: git.ProviderGitHub, + GitHubOpts: []github.OptFunc{github.WithAppBaseURL(srv.URL), github.WithAppID("123"), + github.WithInstllationID("456"), github.WithPrivateKey(kp.PrivateKey)}, + }, + Transport: git.HTTP, + } + + opts := []ClientOption{WithDiskStorage()} + ggc, err := NewClient(t.TempDir(), authOpts, opts...) + g.Expect(err).ToNot(HaveOccurred()) + + err = ggc.providerAuth(context.TODO()) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(authOpts.Username).To(Equal("")) + g.Expect(authOpts.Password).To(Equal("")) + } else { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(authOpts.Username).To(Equal(tt.wantUsername)) + g.Expect(authOpts.Password).To(Equal(tt.wantPassword)) + } + }) + } +} + // setupGitServer sets up, starts an HTTP Git server. It initialzes // a repo on the server and then returns the server and the URL of the // initialized repository. The auth argument can be set to true to enable diff --git a/git/gogit/go.mod b/git/gogit/go.mod index 0835adf5..b250c6c2 100644 --- a/git/gogit/go.mod +++ b/git/gogit/go.mod @@ -34,14 +34,18 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect github.com/cloudflare/circl v1.4.0 // indirect github.com/cyphar/filepath-securejoin v0.3.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect diff --git a/git/gogit/go.sum b/git/gogit/go.sum index 4d70c55d..1872e526 100644 --- a/git/gogit/go.sum +++ b/git/gogit/go.sum @@ -19,6 +19,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= @@ -53,12 +55,19 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -168,6 +177,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/git/internal/e2e/README.md b/git/internal/e2e/README.md index 9dca9370..aa20ce0a 100644 --- a/git/internal/e2e/README.md +++ b/git/internal/e2e/README.md @@ -33,18 +33,44 @@ GO_TEST_PREFIX='TestGitLabCEE2E' ./run.sh ### GitHub -You need to create a PAT (classic) associated with your account. You can do so by following this +You need to create a PAT (classic) associated with your account. This is used to +create and delete test repositories and to grant permissions to the github app. +You can do so by following this [guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). The token should have the following permission scopes: * `repo`: Full control of private repositories * `admin:public_key`: Full control of user public keys * `delete_repo`: Delete repositories -Specify the token, username and org name as environment variables for the script. Please make sure that the -org already exists as it won't be created by the script itself. +You need to +[register](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) +a new GitHub App and [generate a private +key](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps) +for the app by following the linked guides. The GitHub App is used for +authenticating as an app when cloning and pushing the git repository. The app +should be granted the following repository permissions: +* `Contents`: Read and Write access + +[Install](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app) +the app in the organization/account. Get the following information: +* Get the App ID from the app settings page at + `https://github.com/settings/apps/`. +* Get the App Installation ID from the app installations page at +`https://github.com/settings/installations`. Click the installed app, the URL +will contain the installation ID +`https://github.com/settings/installations/`. For +organizations, the first part of the URL may be different, but it follows the +same pattern. +* The private key that was generated in a previous step. + +Specify the token, username, org name, github app id, github installation id and +private key as environment variables for the script. The private key can be +stored in a file and read into the environment variable. Please make sure that +the org already exists as it won't be created by the script itself. ```shell -GO_TEST_PREFIX='TestGitHubE2E' GITHUB_USER='***' GITHUB_ORG='***' GITHUB_TOKEN='***' ./run.sh +GHAPP_PRIVATE_KEY=`cat private-key-file.pem` +GO_TEST_PREFIX='TestGitHubE2E' GITHUB_USER='***' GITHUB_ORG='***' GITHUB_TOKEN='***' GHAPP_ID='***' GHAPP_INSTALL_ID='***' GHAPP_PRIVATE_KEY='***' ./run.sh ``` ### GitLab diff --git a/git/internal/e2e/github_test.go b/git/internal/e2e/github_test.go index dcb17b16..b7ec0068 100644 --- a/git/internal/e2e/github_test.go +++ b/git/internal/e2e/github_test.go @@ -24,31 +24,41 @@ import ( "fmt" "net/url" "os" + "strconv" "testing" . "github.com/onsi/gomega" "github.com/fluxcd/go-git-providers/github" "github.com/fluxcd/go-git-providers/gitprovider" + authgithub "github.com/fluxcd/pkg/auth/github" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" + gogithub "github.com/google/go-github/v64/github" ) const ( - githubSSHHost = "ssh://" + git.DefaultPublicKeyAuthUser + "@" + github.DefaultDomain - githubHTTPHost = "https://" + github.DefaultDomain - githubUser = "GITHUB_USER" - githubOrg = "GITHUB_ORG" + githubSSHHost = "ssh://" + git.DefaultPublicKeyAuthUser + "@" + github.DefaultDomain + githubHTTPHost = "https://" + github.DefaultDomain + githubUser = "GITHUB_USER" + githubOrg = "GITHUB_ORG" + githubAppIDEnv = "GHAPP_ID" + githubAppInstallIDEnv = "GHAPP_INSTALL_ID" + githubAppPKEnv = "GHAPP_PRIVATE_KEY" ) var ( - githubOAuth2Token string - githubUsername string - githubOrgname string + githubOAuth2Token string + githubUsername string + githubOrgname string + githubAppID int + githubAppInstallationID int + githubAppPrivateKey []byte ) func TestGitHubE2E(t *testing.T) { g := NewWithT(t) + var err error githubOAuth2Token = os.Getenv(github.TokenVariable) if githubOAuth2Token == "" { t.Fatalf("could not read github oauth2 token") @@ -61,12 +71,45 @@ func TestGitHubE2E(t *testing.T) { if githubOrgname == "" { t.Fatalf("could not read github org name") } + githubAppID := os.Getenv(githubAppIDEnv) + if githubAppID == "" { + t.Fatalf("could not read github app id") + } + + githubAppInstallID := os.Getenv(githubAppInstallIDEnv) + if githubAppInstallID == "" { + t.Fatalf("could not read github app installation id") + } + + githubAppPrivateKey := []byte(os.Getenv(githubAppPKEnv)) + if len(githubAppPrivateKey) == 0 { + t.Fatalf("could not read github app private key") + } c, err := github.NewClient(gitprovider.WithDestructiveAPICalls(true), gitprovider.WithOAuth2Token(githubOAuth2Token)) g.Expect(err).ToNot(HaveOccurred()) orgClient := c.OrgRepositories() - repoInfo := func(proto git.TransportType, repo gitprovider.OrgRepository) (*url.URL, *git.AuthOptions, error) { + grantPermissionsToApp := func(repo gitprovider.OrgRepository) error { + ctx := context.Background() + githubClient := c.Raw().(*gogithub.Client) + ghRepo, _, err := githubClient.Repositories.Get(ctx, githubOrgname, repo.Repository().GetRepository()) + if err != nil { + return err + } + installID, err := strconv.Atoi(githubAppInstallID) + if err != nil { + return err + } + _, _, err = githubClient.Apps.AddRepository(ctx, int64(installID), ghRepo.GetID()) + if err != nil { + return err + } + + return nil + } + + repoInfo := func(proto git.TransportType, repo gitprovider.OrgRepository, authMethod string) (*url.URL, *git.AuthOptions, error) { var repoURL *url.URL var authOptions *git.AuthOptions var err error @@ -101,10 +144,24 @@ func TestGitHubE2E(t *testing.T) { if err != nil { return nil, nil, err } - authOptions, err = git.NewAuthOptions(*repoURL, map[string][]byte{ - "username": []byte(githubUsername), - "password": []byte(githubOAuth2Token), - }) + + if authMethod == "app" { + var data map[string][]byte + authOptions, err = git.NewAuthOptions(*repoURL, data) + authOptions.ProviderOpts = &git.ProviderOptions{ + Name: git.ProviderGitHub, + GitHubOpts: []authgithub.OptFunc{ + authgithub.WithAppID(githubAppID), + authgithub.WithInstllationID(githubAppInstallID), + authgithub.WithPrivateKey(githubAppPrivateKey), + }, + } + } else { + authOptions, err = git.NewAuthOptions(*repoURL, map[string][]byte{ + "username": []byte(githubUsername), + "password": []byte(githubOAuth2Token), + }) + } if err != nil { return nil, nil, err } @@ -115,11 +172,11 @@ func TestGitHubE2E(t *testing.T) { protocols := []git.TransportType{git.HTTP, git.SSH} clients := []string{gogit.ClientName} - testFunc := func(t *testing.T, proto git.TransportType, gitClient string) { - t.Run(fmt.Sprintf("repo created using Clone/%s/%s", gitClient, proto), func(t *testing.T) { + testFunc := func(t *testing.T, proto git.TransportType, gitClient string, authMethod string) { + t.Run(fmt.Sprintf("repo created using Clone/%s/%s", authMethod, proto), func(t *testing.T) { g := NewWithT(t) - repoName := fmt.Sprintf("github-e2e-checkout-%s-%s-%s", string(proto), string(gitClient), randStringRunes(5)) + repoName := fmt.Sprintf("github-e2e-checkout-%s-%s-%s", string(proto), string(authMethod), randStringRunes(5)) upstreamRepoURL := githubHTTPHost + "/" + githubOrgname + "/" + repoName ref, err := gitprovider.ParseOrgRepositoryURL(upstreamRepoURL) @@ -129,9 +186,14 @@ func TestGitHubE2E(t *testing.T) { defer repo.Delete(context.TODO()) + if authMethod == "app" { + err := grantPermissionsToApp(repo) + g.Expect(err).ToNot(HaveOccurred()) + } + err = initRepo(t.TempDir(), upstreamRepoURL, "main", "../../testdata/git/repo", githubUsername, githubOAuth2Token) g.Expect(err).ToNot(HaveOccurred()) - repoURL, authOptions, err := repoInfo(proto, repo) + repoURL, authOptions, err := repoInfo(proto, repo, authMethod) g.Expect(err).ToNot(HaveOccurred()) client, err := newClient(gitClient, t.TempDir(), authOptions, false) @@ -145,10 +207,10 @@ func TestGitHubE2E(t *testing.T) { }) }) - t.Run(fmt.Sprintf("repo created using Init/%s/%s", gitClient, proto), func(t *testing.T) { + t.Run(fmt.Sprintf("repo created using Init/%s/%s", authMethod, proto), func(t *testing.T) { g := NewWithT(t) - repoName := fmt.Sprintf("github-e2e-checkout-%s-%s-%s", string(proto), string(gitClient), randStringRunes(5)) + repoName := fmt.Sprintf("github-e2e-checkout-%s-%s-%s", string(proto), string(authMethod), randStringRunes(5)) upstreamRepoURL := githubHTTPHost + "/" + githubOrgname + "/" + repoName ref, err := gitprovider.ParseOrgRepositoryURL(upstreamRepoURL) @@ -158,7 +220,12 @@ func TestGitHubE2E(t *testing.T) { defer repo.Delete(context.TODO()) - repoURL, authOptions, err := repoInfo(proto, repo) + if authMethod == "app" { + err := grantPermissionsToApp(repo) + g.Expect(err).ToNot(HaveOccurred()) + } + + repoURL, authOptions, err := repoInfo(proto, repo, authMethod) g.Expect(err).ToNot(HaveOccurred()) client, err := newClient(gitClient, t.TempDir(), authOptions, false) @@ -175,7 +242,10 @@ func TestGitHubE2E(t *testing.T) { for _, client := range clients { for _, protocol := range protocols { - testFunc(t, protocol, client) + // test client with all protocols without githubApp authentication + testFunc(t, protocol, client, "user") } + // test client with HTTPS protocol with githubApp authentication + testFunc(t, git.HTTP, client, "app") } } diff --git a/git/internal/e2e/go.mod b/git/internal/e2e/go.mod index 66285920..1e66225b 100644 --- a/git/internal/e2e/go.mod +++ b/git/internal/e2e/go.mod @@ -13,12 +13,14 @@ replace ( require ( github.com/fluxcd/go-git-providers v0.21.0 + github.com/fluxcd/pkg/auth v0.0.1 github.com/fluxcd/pkg/git v0.20.0 github.com/fluxcd/pkg/git/gogit v0.20.0 github.com/fluxcd/pkg/gittestserver v0.13.1 github.com/fluxcd/pkg/ssh v0.14.1 github.com/go-git/go-git/v5 v5.12.0 github.com/go-logr/logr v1.4.2 + github.com/google/go-github/v64 v64.0.0 github.com/google/uuid v1.6.0 github.com/onsi/gomega v1.34.2 ) @@ -32,20 +34,21 @@ require ( github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect github.com/cloudflare/circl v1.4.0 // indirect github.com/cyphar/filepath-securejoin v0.3.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fluxcd/gitkit v0.6.0 // indirect - github.com/fluxcd/pkg/auth v0.0.1 // indirect github.com/fluxcd/pkg/version v0.4.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/go-github/v64 v64.0.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/git/internal/e2e/go.sum b/git/internal/e2e/go.sum index 92ca5b1c..75f4d60a 100644 --- a/git/internal/e2e/go.sum +++ b/git/internal/e2e/go.sum @@ -19,6 +19,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= @@ -59,6 +61,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -66,6 +70,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= diff --git a/git/options.go b/git/options.go index 6568caf0..405b7a0b 100644 --- a/git/options.go +++ b/git/options.go @@ -21,6 +21,7 @@ import ( "net/url" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/github" ) const ( @@ -54,8 +55,9 @@ type AuthOptions struct { // ProviderOptions contains options to configure various authentication // providers. type ProviderOptions struct { - Name string - AzureOpts []azure.OptFunc + Name string + AzureOpts []azure.OptFunc + GitHubOpts []github.OptFunc } // KexAlgos hosts the key exchange algorithms to be used for SSH connections. diff --git a/oci/tests/integration/go.mod b/oci/tests/integration/go.mod index 51e6585e..c262b321 100644 --- a/oci/tests/integration/go.mod +++ b/oci/tests/integration/go.mod @@ -55,6 +55,7 @@ require ( github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.4.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect @@ -80,11 +81,14 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect diff --git a/oci/tests/integration/go.sum b/oci/tests/integration/go.sum index af35ed8b..dbcd1838 100644 --- a/oci/tests/integration/go.sum +++ b/oci/tests/integration/go.sum @@ -60,6 +60,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -146,6 +148,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -172,11 +176,16 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=