From 005baa1ac4c3785021f3e7e834d507991e54ad50 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 | 171 +++++++++++++++++++++++ auth/github/client_test.go | 238 ++++++++++++++++++++++++++++++++ auth/github/test_helper.go | 46 ++++++ auth/go.mod | 6 +- auth/go.sum | 10 ++ git/credentials.go | 27 +++- git/credentials_test.go | 77 +++++++++++ git/go.mod | 4 + git/go.sum | 10 ++ git/gogit/client.go | 40 +++++- git/gogit/client_test.go | 148 +++++++++++++++++--- git/gogit/go.mod | 6 +- git/gogit/go.sum | 10 ++ git/internal/e2e/README.md | 25 +++- git/internal/e2e/github_test.go | 106 +++++++++++--- 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 ++ 21 files changed, 909 insertions(+), 50 deletions(-) create mode 100644 auth/github/client.go create mode 100644 auth/github/client_test.go create mode 100644 auth/github/test_helper.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..7b195654 --- /dev/null +++ b/auth/github/client.go @@ -0,0 +1,171 @@ +/* +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" + ApiURLKey = "githubApiURL" +) + +// Client is an authentication provider for GitHub Apps. +type Client struct { + appID int + installationID int + privateKey []byte + apiURL string + proxyConfig *httpproxy.Config + ghTransport *ghinstallation.Transport +} + +// OptFunc enables specifying options for the provider. +type OptFunc func(*Client) error + +// New returns a new authentication provider for GitHub Apps. +func New(opts ...OptFunc) (*Client, error) { + var err error + + p := &Client{} + for _, opt := range opts { + err = opt(p) + if err != nil { + return nil, err + } + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + if p.proxyConfig != nil { + proxyFunc := func(req *http.Request) (*url.URL, error) { + return p.proxyConfig.ProxyFunc()(req.URL) + } + transport.Proxy = proxyFunc + } + p.ghTransport, err = ghinstallation.New(transport, int64(p.appID), int64(p.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 int) OptFunc { + return func(p *Client) error { + p.installationID = installationID + return nil + } +} + +// WithAppID configures the app ID of the GitHub App. +func WithAppID(appID int) OptFunc { + return func(p *Client) error { + p.appID = appID + return nil + } +} + +// WithPrivateKey configures the private key of the GitHub App. +func WithPrivateKey(pk []byte) OptFunc { + return func(p *Client) error { + p.privateKey = pk + return nil + } +} + +// WithApiURL configures the GitHub API endpoint to use to fetch GitHub App +// installation token. +func WithApiURL(apiURL string) OptFunc { + return func(p *Client) error { + p.apiURL = apiURL + return nil + } +} + +// WithAppData configures the client using data from a map +func WithAppData(appData map[string][]byte) OptFunc { + return func(p *Client) error { + var err error + for _, key := range []string{AppIDKey, AppInstallationIDKey, AppPrivateKey} { + if _, exists := appData[key]; !exists { + return fmt.Errorf("github app data must contain key : %s", key) + } + } + p.appID, err = strconv.Atoi(string(appData[AppIDKey])) + if err != nil { + return fmt.Errorf("github app data error for key : %s, err: %v", AppIDKey, err) + } + p.installationID, err = strconv.Atoi(string(appData[AppInstallationIDKey])) + if err != nil { + return fmt.Errorf("github app data error for key : %s, err: %v", AppInstallationIDKey, err) + } + p.privateKey = appData[AppPrivateKey] + p.apiURL = string(appData[ApiURLKey]) + return nil + } +} + +// WithProxyConfig configures the http proxy settings to be used with the +// transport. +func WithProxyConfig(proxyConfig *httpproxy.Config) OptFunc { + return func(p *Client) error { + p.proxyConfig = proxyConfig + return nil + } +} + +// AppToken contains a GitHub App installation token and its expiry. +type AppToken struct { + Token string + ExpiresAt time.Time +} + +// 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..15aa31ad --- /dev/null +++ b/auth/github/client_test.go @@ -0,0 +1,238 @@ +/* +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" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + . "github.com/onsi/gomega" + "golang.org/x/net/http/httpproxy" +) + +func TestClient_Options(t *testing.T) { + appID := 123 + installationID := 456 + pk, _ := CreateTestPrivateKey() + gitHubDefaultURL := "https://api.github.com" + gitHubEnterpriseURL := "https://github.example.com/api/v3" + proxyConfig := httpproxy.Config{ + HTTPProxy: "http://localhost:8080", + HTTPSProxy: "https://localhost:8080", + } + + tests := []struct { + name string + opts []OptFunc + useProxy bool + customApiUrl bool + wantErr error + }{ + { + name: "Create new client", + opts: []OptFunc{WithInstllationID(installationID), WithAppID(appID), WithPrivateKey(pk)}, + }, + { + name: "Create new client with proxy", + opts: []OptFunc{WithInstllationID(installationID), WithAppID(appID), WithPrivateKey(pk)}, + useProxy: true, + }, + { + name: "Create new client with custom api url", + opts: []OptFunc{WithApiURL(gitHubEnterpriseURL), WithInstllationID(installationID), WithAppID(appID), WithPrivateKey(pk)}, + customApiUrl: true, + }, + { + name: "Create new client with app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(fmt.Sprintf("%d", appID)), + AppInstallationIDKey: []byte(fmt.Sprintf("%d", installationID)), + AppPrivateKey: pk, + }, + )}, + }, + { + name: "Create new client with empty data", + opts: []OptFunc{WithAppData(map[string][]byte{})}, + wantErr: errors.New(fmt.Sprintf("github app data must contain key : %s", AppIDKey)), + }, + { + name: "Create new client with app data with missing AppID Key", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppInstallationIDKey: []byte(fmt.Sprintf("%d", installationID)), + AppPrivateKey: pk, + }, + )}, + wantErr: errors.New(fmt.Sprintf("github app data must contain key : %s", AppIDKey)), + }, + { + name: "Create new client with app data with missing AppInstallationIDKey Key", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte("abc"), + AppPrivateKey: pk, + }, + )}, + wantErr: errors.New(fmt.Sprintf("github app data must contain key : %s", AppInstallationIDKey)), + }, + { + name: "Create new client with app data with missing private Key", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(fmt.Sprintf("%d", appID)), + AppInstallationIDKey: []byte(fmt.Sprintf("%d", installationID)), + }, + )}, + wantErr: errors.New(fmt.Sprintf("github app data must contain key : %s", AppPrivateKey)), + }, + { + name: "Create new client with invalid appID in app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte("abc"), + AppInstallationIDKey: []byte(fmt.Sprintf("%d", installationID)), + AppPrivateKey: pk, + }, + )}, + wantErr: errors.New(fmt.Sprintf("github app data error for key : %s, err: strconv.Atoi: parsing \"abc\": invalid syntax", AppIDKey)), + }, + { + name: "Create new client with invalid installationID in app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(fmt.Sprintf("%d", appID)), + AppInstallationIDKey: []byte("abc"), + AppPrivateKey: pk, + }, + )}, + wantErr: errors.New(fmt.Sprintf("github app data error for key : %s, err: strconv.Atoi: parsing \"abc\": invalid syntax", AppInstallationIDKey)), + }, + { + name: "Create new client with invalid private key in app data", + opts: []OptFunc{WithAppData(map[string][]byte{ + AppIDKey: []byte(fmt.Sprintf("%d", appID)), + AppInstallationIDKey: []byte(fmt.Sprintf("%d", 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 + if tt.useProxy { + opts = append(opts, WithProxyConfig(&proxyConfig)) + } + + 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(pk)) + + if tt.customApiUrl { + 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 *FakeAccessToken + statusCode int + wantErr bool + wantAppToken *AppToken + }{ + { + name: "Get valid token", + accessToken: &FakeAccessToken{ + 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() + }) + + pk, err := CreateTestPrivateKey() + g.Expect(err).ToNot(HaveOccurred()) + opts := []OptFunc{ + WithApiURL(srv.URL), WithInstllationID(123), WithAppID(456), WithPrivateKey(pk), + } + + 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/github/test_helper.go b/auth/github/test_helper.go new file mode 100644 index 00000000..e701231b --- /dev/null +++ b/auth/github/test_helper.go @@ -0,0 +1,46 @@ +/* +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 ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "time" +) + +type FakeAccessToken struct { + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` +} + +func CreateTestPrivateKey() ([]byte, error) { + privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + var privateKeyBytes []byte = x509.MarshalPKCS1PrivateKey(privatekey) + privateKeyBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + pk := pem.EncodeToMemory(privateKeyBlock) + return pk, nil +} diff --git a/auth/go.mod b/auth/go.mod index 5d1a1817..f0df9ef1 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -5,21 +5,25 @@ 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/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..86f207ab 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -6,16 +6,25 @@ 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/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= @@ -49,6 +58,7 @@ 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..7f21151f 100644 --- a/git/credentials_test.go +++ b/git/credentials_test.go @@ -18,11 +18,15 @@ 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/onsi/gomega" ) @@ -98,3 +102,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.FakeAccessToken + statusCode int + wantCredentials *Credentials + wantErr bool + }{ + { + name: "get credentials from github success", + statusCode: http.StatusOK, + accessToken: &github.FakeAccessToken{ + 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() + }) + + pk, err := github.CreateTestPrivateKey() + g.Expect(err).ToNot(HaveOccurred()) + + providerOpts := &ProviderOptions{ + Name: ProviderGitHub, + GitHubOpts: []github.OptFunc{github.WithApiURL(srv.URL), github.WithAppID(123), + github.WithInstllationID(456), github.WithPrivateKey(pk)}, + } + + 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..9e159c80 100644 --- a/git/go.mod +++ b/git/go.mod @@ -16,9 +16,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..d38e5cd1 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= @@ -20,10 +22,17 @@ 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= @@ -98,6 +107,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..a93cd4dc 100644 --- a/git/gogit/client.go +++ b/git/gogit/client.go @@ -38,8 +38,10 @@ import ( "github.com/go-git/go-git/v5/storage" "github.com/go-git/go-git/v5/storage/filesystem" "github.com/go-git/go-git/v5/storage/memory" + "golang.org/x/net/http/httpproxy" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/github" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/repository" ) @@ -248,11 +250,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.validateUrl(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 +281,17 @@ func (g *Client) clone(ctx context.Context, url string, cfg repository.CloneConf } } +// validateUrl 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) validateUrl(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 +300,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 { @@ -305,9 +316,10 @@ func (g *Client) validateUrl(u string) error { } if httpOrEmpty && g.authOpts != nil { - if g.authOpts.Username != "" || g.authOpts.Password != "" { + hasGitHubProvider := g.hasProvider(git.ProviderGitHub) + if g.authOpts.Username != "" || g.authOpts.Password != "" || hasGitHubProvider { return errors.New("basic auth cannot be sent over HTTP") - } else if g.authOpts.BearerToken != "" { + } else if g.authOpts.BearerToken != "" || hasAzureProvider { return errors.New("bearer token cannot be sent over HTTP") } } @@ -315,8 +327,13 @@ func (g *Client) validateUrl(u string) error { 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 +342,13 @@ 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: + proxyStr := proxyURL.String() + proxyConfig := &httpproxy.Config{ + HTTPProxy: proxyStr, + HTTPSProxy: proxyStr, + } + g.authOpts.ProviderOpts.GitHubOpts = append(g.authOpts.ProviderOpts.GitHubOpts, github.WithProxyConfig(proxyConfig)) default: return fmt.Errorf("invalid provider") } @@ -335,6 +359,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 +445,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..2ace2125 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,6 +35,7 @@ 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" @@ -741,7 +745,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 +755,8 @@ func TestProviderAuth(t *testing.T) { wantAuthErr error wantValidationErr error wantBearerToken string + wantUsername string + wantPassword string }{ { name: "nil authopts", @@ -821,6 +827,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 +850,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 +867,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 +887,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,9 +905,35 @@ func TestProviderAuth(t *testing.T) { }, Transport: git.HTTP, }, - wantBearerToken: "ado-token", wantValidationErr: errors.New("bearer token cannot be sent over 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("basic auth cannot be sent over HTTP"), + }, } for _, tt := range tests { @@ -914,22 +945,103 @@ 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.validateUrl(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.FakeAccessToken + wantUsername string + wantPassword string + wantErr bool + }{ + { + name: "test git provider auth success", + statusCode: http.StatusOK, + accessToken: &github.FakeAccessToken{ + 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() + }) + + pk, err := github.CreateTestPrivateKey() + g.Expect(err).ToNot(HaveOccurred()) + authOpts := &git.AuthOptions{ + ProviderOpts: &git.ProviderOptions{ + Name: git.ProviderGitHub, + GitHubOpts: []github.OptFunc{github.WithApiURL(srv.URL), github.WithAppID(123), + github.WithInstllationID(456), github.WithPrivateKey(pk)}, + }, + 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)) } }) } diff --git a/git/gogit/go.mod b/git/gogit/go.mod index 0835adf5..5429b496 100644 --- a/git/gogit/go.mod +++ b/git/gogit/go.mod @@ -24,6 +24,7 @@ require ( github.com/go-git/go-git/v5 v5.12.0 github.com/onsi/gomega v1.34.2 golang.org/x/crypto v0.27.0 + golang.org/x/net v0.29.0 ) require ( @@ -34,14 +35,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 @@ -52,7 +57,6 @@ require ( github.com/skeema/knownhosts v1.3.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.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..410e4033 100644 --- a/git/internal/e2e/README.md +++ b/git/internal/e2e/README.md @@ -40,11 +40,32 @@ The token should have the following permission scopes: * `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 +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 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. 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 +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..074850fd 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") } + appID := os.Getenv(githubAppIDEnv) + if appID == "" { + t.Fatalf("could not read github app id") + } + githubAppID, err = strconv.Atoi(appID) + g.Expect(err).ToNot(HaveOccurred()) + + installID := os.Getenv(githubAppInstallIDEnv) + if installID == "" { + t.Fatalf("could not read github app installation id") + } + githubAppInstallationID, err = strconv.Atoi(installID) + g.Expect(err).ToNot(HaveOccurred()) + + 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) { + givePermissionsToApp := 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 + } + _, _, err = githubClient.Apps.AddRepository(ctx, int64(githubAppInstallationID), ghRepo.GetID()) + if err != nil { + return err + } + + return nil + } + + repoInfo := func(proto git.TransportType, repo gitprovider.OrgRepository, githubApp bool) (*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 githubApp { + 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(githubAppInstallationID), + 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) { + testFunc := func(t *testing.T, proto git.TransportType, gitClient string, githubApp bool) { t.Run(fmt.Sprintf("repo created using Clone/%s/%s", gitClient, 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-%s", string(proto), string(gitClient), strconv.FormatBool(githubApp), 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 githubApp { + err := givePermissionsToApp(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, githubApp) g.Expect(err).ToNot(HaveOccurred()) client, err := newClient(gitClient, t.TempDir(), authOptions, false) @@ -148,7 +210,7 @@ func TestGitHubE2E(t *testing.T) { t.Run(fmt.Sprintf("repo created using Init/%s/%s", gitClient, 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-%s", string(proto), string(gitClient), strconv.FormatBool(githubApp), 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 githubApp { + err := givePermissionsToApp(repo) + g.Expect(err).ToNot(HaveOccurred()) + } + + repoURL, authOptions, err := repoInfo(proto, repo, githubApp) 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, false) } + // test client with HTTPS protocol with githubApp authentication + testFunc(t, git.HTTP, client, true) } } 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 659ff987..73e5cddd 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 b5263679..04674358 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=