fix: support gh-installation-id flag and fix #4578 #1229 #3545 (#4579)

Co-authored-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
john-tipper
2024-05-22 19:35:39 +01:00
committed by GitHub
parent 187d9f32a1
commit 82cd0746ee
12 changed files with 254 additions and 34 deletions

View File

@@ -4,11 +4,12 @@
package events
import (
"reflect"
"time"
pegomock "github.com/petergtz/pegomock/v4"
models "github.com/runatlantis/atlantis/server/events/models"
logging "github.com/runatlantis/atlantis/server/logging"
"reflect"
"time"
)
type MockWorkingDir struct {

View File

@@ -71,7 +71,7 @@ type GithubAppCredentials struct {
Key []byte
Hostname string
apiURL *url.URL
installationID int64
InstallationID int64
tr *ghinstallation.Transport
AppSlug string
}
@@ -122,8 +122,8 @@ func (c *GithubAppCredentials) GetToken() (string, error) {
}
func (c *GithubAppCredentials) getInstallationID() (int64, error) {
if c.installationID != 0 {
return c.installationID, nil
if c.InstallationID != 0 {
return c.InstallationID, nil
}
tr := http.DefaultTransport
@@ -148,8 +148,8 @@ func (c *GithubAppCredentials) getInstallationID() (int64, error) {
return 0, fmt.Errorf("wrong number of installations, expected 1, found %d", len(installations))
}
c.installationID = installations[0].GetID()
return c.installationID, nil
c.InstallationID = installations[0].GetID()
return c.InstallationID, nil
}
func (c *GithubAppCredentials) transport() (*ghinstallation.Transport, error) {

View File

@@ -69,3 +69,40 @@ func TestGithubClient_AppAuthentication(t *testing.T) {
t.Errorf("app token was not cached: %q != %q", token, newToken)
}
}
func TestGithubClient_MultipleAppAuthentication(t *testing.T) {
logger := logging.NewNoopLogger(t)
defer disableSSLVerification()()
testServer, err := testdata.GithubMultipleAppTestServer(t)
Ok(t, err)
anonCreds := &vcs.GithubAnonymousCredentials{}
anonClient, err := vcs.NewGithubClient(testServer, anonCreds, vcs.GithubConfig{}, logging.NewNoopLogger(t))
Ok(t, err)
tempSecrets, err := anonClient.ExchangeCode(logger, "good-code")
Ok(t, err)
appCreds := &vcs.GithubAppCredentials{
AppID: tempSecrets.ID,
InstallationID: 1,
Key: []byte(testdata.GithubPrivateKey),
Hostname: testServer,
}
_, err = vcs.NewGithubClient(testServer, appCreds, vcs.GithubConfig{}, logging.NewNoopLogger(t))
Ok(t, err)
token, err := appCreds.GetToken()
Ok(t, err)
newToken, err := appCreds.GetToken()
Ok(t, err)
user, err := appCreds.GetUser()
Ok(t, err)
Assert(t, user == "", "user should be empty")
if token != newToken {
t.Errorf("app token was not cached: %q != %q", token, newToken)
}
}

View File

@@ -496,6 +496,79 @@ var githubAppInstallationJSON = `[
}
]`
var githubAppMultipleInstallationJSON = `[
{
"id": 1,
"account": {
"login": "github",
"id": 1,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE=",
"url": "https://api.github.com/orgs/github",
"repos_url": "https://api.github.com/orgs/github/repos",
"events_url": "https://api.github.com/orgs/github/events",
"hooks_url": "https://api.github.com/orgs/github/hooks",
"issues_url": "https://api.github.com/orgs/github/issues",
"members_url": "https://api.github.com/orgs/github/members{/member}",
"public_members_url": "https://api.github.com/orgs/github/public_members{/member}",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"description": "A great organization"
},
"access_tokens_url": "https://api.github.com/installations/1/access_tokens",
"repositories_url": "https://api.github.com/installation/repositories",
"html_url": "https://github.com/organizations/github/settings/installations/1",
"app_id": 1,
"target_id": 1,
"target_type": "Organization",
"permissions": {
"metadata": "read",
"contents": "read",
"issues": "write",
"single_file": "write"
},
"events": [
"push",
"pull_request"
],
"single_file_name": "config.yml",
"repository_selection": "selected"
},
{
"id": 2,
"account": {
"login": "github",
"id": 1,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE=",
"url": "https://api.github.com/orgs/github",
"repos_url": "https://api.github.com/orgs/github/repos",
"events_url": "https://api.github.com/orgs/github/events",
"hooks_url": "https://api.github.com/orgs/github/hooks",
"issues_url": "https://api.github.com/orgs/github/issues",
"members_url": "https://api.github.com/orgs/github/members{/member}",
"public_members_url": "https://api.github.com/orgs/github/public_members{/member}",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"description": "A great organization"
},
"access_tokens_url": "https://api.github.com/installations/1/access_tokens",
"repositories_url": "https://api.github.com/installation/repositories",
"html_url": "https://github.com/organizations/github/settings/installations/1",
"app_id": 1,
"target_id": 1,
"target_type": "Organization",
"permissions": {
"metadata": "read",
"contents": "read",
"issues": "write",
"single_file": "write"
},
"events": [
"push",
"pull_request"
],
"single_file_name": "config.yml",
"repository_selection": "selected"
}
]`
// nolint: gosec
var githubAppTokenJSON = `{
"token": "some-token",
@@ -741,3 +814,58 @@ func GithubAppTestServer(t *testing.T) (string, error) {
return testServerURL.Host, err
}
func GithubMultipleAppTestServer(t *testing.T) (string, error) {
counter := 0
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/api/v3/app-manifests/good-code/conversions":
encodedKey := strings.Join(strings.Split(GithubPrivateKey, "\n"), "\\n")
appInfo := fmt.Sprintf(githubConversionJSON, encodedKey)
w.Write([]byte(appInfo)) // nolint: errcheck
// https://developer.github.com/v3/apps/#list-installations
case "/api/v3/app/installations":
token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1)
if err := validateGithubToken(token); err != nil {
w.WriteHeader(403)
w.Write([]byte("Invalid token")) // nolint: errcheck
return
}
w.Write([]byte(githubAppMultipleInstallationJSON)) // nolint: errcheck
return
case "/api/v3/apps/some-app":
token := strings.Replace(r.Header.Get("Authorization"), "token ", "", 1)
// token is taken from githubAppTokenJSON
if token != "some-token" {
w.WriteHeader(403)
w.Write([]byte("Invalid installation token")) // nolint: errcheck
return
}
w.Write([]byte(githubAppJSON)) // nolint: errcheck
return
case "/api/v3/app/installations/1/access_tokens":
token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1)
if err := validateGithubToken(token); err != nil {
w.WriteHeader(403)
w.Write([]byte("Invalid token")) // nolint: errcheck
return
}
appToken := fmt.Sprintf(githubAppTokenJSON, counter)
counter++
w.Write([]byte(appToken)) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
testServerURL, err := url.Parse(testServer.URL)
return testServerURL.Host, err
}

View File

@@ -239,18 +239,20 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
return nil, err
}
githubCredentials = &vcs.GithubAppCredentials{
AppID: userConfig.GithubAppID,
Key: privateKey,
Hostname: userConfig.GithubHostname,
AppSlug: userConfig.GithubAppSlug,
AppID: userConfig.GithubAppID,
InstallationID: userConfig.GithubInstallationID,
Key: privateKey,
Hostname: userConfig.GithubHostname,
AppSlug: userConfig.GithubAppSlug,
}
githubAppEnabled = true
} else if userConfig.GithubAppID != 0 && userConfig.GithubAppKey != "" {
githubCredentials = &vcs.GithubAppCredentials{
AppID: userConfig.GithubAppID,
Key: []byte(userConfig.GithubAppKey),
Hostname: userConfig.GithubHostname,
AppSlug: userConfig.GithubAppSlug,
AppID: userConfig.GithubAppID,
InstallationID: userConfig.GithubInstallationID,
Key: []byte(userConfig.GithubAppKey),
Hostname: userConfig.GithubHostname,
AppSlug: userConfig.GithubAppSlug,
}
githubAppEnabled = true
}

View File

@@ -57,6 +57,7 @@ type UserConfig struct {
GithubAppKey string `mapstructure:"gh-app-key"`
GithubAppKeyFile string `mapstructure:"gh-app-key-file"`
GithubAppSlug string `mapstructure:"gh-app-slug"`
GithubInstallationID int64 `mapstructure:"gh-installation-id"`
GithubTeamAllowlist string `mapstructure:"gh-team-allowlist"`
GiteaBaseURL string `mapstructure:"gitea-base-url"`
GiteaToken string `mapstructure:"gitea-token"`