Skip to content

Commit

Permalink
Add support for fetching gh app user. (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
nishkrishnan authored Dec 16, 2020
1 parent f9ff225 commit 8f8e7d9
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 18 deletions.
4 changes: 4 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
GHUserFlag = "gh-user"
GHAppIDFlag = "gh-app-id"
GHAppKeyFileFlag = "gh-app-key-file"
GHAppSlugFlag = "gh-app-slug"
GHOrganizationFlag = "gh-org"
GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec
GitlabHostnameFlag = "gitlab-hostname"
Expand Down Expand Up @@ -181,6 +182,9 @@ var stringFlags = map[string]stringFlag{
description: "A path to a file containing the GitHub App's private key",
defaultValue: "",
},
GHAppSlugFlag: {
description: "The Github app slug (ie. the URL-friendly name of your GitHub App)",
},
GHOrganizationFlag: {
description: "The name of the GitHub organization to use during the creation of a Github App for Atlantis",
defaultValue: "",
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ var testFlags = map[string]interface{}{
GHUserFlag: "user",
GHAppIDFlag: int64(0),
GHAppKeyFileFlag: "",
GHAppSlugFlag: "atlantis",
GHOrganizationFlag: "",
GHWebhookSecretFlag: "secret",
GitlabHostnameFlag: "gitlab-hostname",
Expand Down
55 changes: 54 additions & 1 deletion server/events/vcs/fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ var githubAppInstallationJSON = `[

// nolint: gosec
var githubAppTokenJSON = `{
"token": "v1.1f699f1069f60xx%d",
"token": "some-token",
"expires_at": "2050-01-01T00:00:00Z",
"permissions": {
"issues": "write",
Expand Down Expand Up @@ -452,6 +452,48 @@ var githubAppTokenJSON = `{
]
}`

var githubAppJSON = `{
"id": 1,
"slug": "octoapp",
"node_id": "MDExOkludGVncmF0aW9uMQ==",
"owner": {
"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",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": true
},
"name": "Octocat App",
"description": "",
"external_url": "https://example.com",
"html_url": "https://github.com/apps/octoapp",
"created_at": "2017-07-08T16:18:44-04:00",
"updated_at": "2017-07-08T16:18:44-04:00",
"permissions": {
"metadata": "read",
"contents": "read",
"issues": "write",
"single_file": "write"
},
"events": [
"push",
"pull_request"
]
}`

func validateGithubToken(tokenString string) error {
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(GithubPrivateKey))
if err != nil {
Expand Down Expand Up @@ -499,6 +541,17 @@ func GithubAppTestServer(t *testing.T) (string, error) {

w.Write([]byte(githubAppInstallationJSON)) // 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 {
Expand Down
11 changes: 9 additions & 2 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,15 @@ func NewGithubClient(hostname string, credentials GithubCredentials, logger *log
transport,
graphql.WithHeader("Accept", "application/vnd.github.queen-beryl-preview+json"),
)

user, err := credentials.GetUser()
logger.Debug("GH User: %s", user)

if err != nil {
return nil, errors.Wrap(err, "getting user")
}
return &GithubClient{
user: credentials.GetUser(),
user: user,
client: client,
v4MutateClient: v4MutateClient,
ctx: context.Background(),
Expand Down Expand Up @@ -179,7 +186,7 @@ func (g *GithubClient) HidePrevPlanComments(repo models.Repo, pullNum int) error
ListOptions: github.ListOptions{Page: nextPage},
})
if err != nil {
return err
return errors.Wrap(err, "listing comments")
}
allComments = append(allComments, comments...)
if resp.NextPage == 0 {
Expand Down
36 changes: 29 additions & 7 deletions server/events/vcs/github_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
type GithubCredentials interface {
Client() (*http.Client, error)
GetToken() (string, error)
GetUser() string
GetUser() (string, error)
}

// GithubAnonymousCredentials expose no credentials.
Expand All @@ -31,8 +31,8 @@ func (c *GithubAnonymousCredentials) Client() (*http.Client, error) {
}

// GetUser returns the username for these credentials.
func (c *GithubAnonymousCredentials) GetUser() string {
return "anonymous"
func (c *GithubAnonymousCredentials) GetUser() (string, error) {
return "anonymous", nil
}

// GetToken returns an empty token.
Expand All @@ -56,8 +56,8 @@ func (c *GithubUserCredentials) Client() (*http.Client, error) {
}

// GetUser returns the username for these credentials.
func (c *GithubUserCredentials) GetUser() string {
return c.User
func (c *GithubUserCredentials) GetUser() (string, error) {
return c.User, nil
}

// GetToken returns the user token.
Expand All @@ -73,6 +73,7 @@ type GithubAppCredentials struct {
apiURL *url.URL
installationID int64
tr *ghinstallation.Transport
AppSlug string
}

// Client returns a github app installation client.
Expand All @@ -85,8 +86,29 @@ func (c *GithubAppCredentials) Client() (*http.Client, error) {
}

// GetUser returns the username for these credentials.
func (c *GithubAppCredentials) GetUser() string {
return ""
func (c *GithubAppCredentials) GetUser() (string, error) {
// Keeping backwards compatibility since this flag is optional
if c.AppSlug == "" {
return "", nil
}
client, err := c.Client()

if err != nil {
return "", errors.Wrap(err, "initializing client")
}

ghClient := github.NewClient(client)
ghClient.BaseURL = c.getAPIURL()
ctx := context.Background()

app, _, err := ghClient.Apps.Get(ctx, c.AppSlug)

if err != nil {
return "", errors.Wrap(err, "getting app details")
}
// Currently there is no way to get the bot's login info, so this is a
// hack until Github exposes that.
return fmt.Sprintf("%s[bot]", app.GetName()), nil
}

// GetToken returns a fresh installation token.
Expand Down
35 changes: 35 additions & 0 deletions server/events/vcs/github_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ import (
. "github.com/runatlantis/atlantis/testing"
)

func TestGithubClient_GetUser_AppSlug(t *testing.T) {
defer disableSSLVerification()()
testServer, err := fixtures.GithubAppTestServer(t)
Ok(t, err)

anonCreds := &vcs.GithubAnonymousCredentials{}
anonClient, err := vcs.NewGithubClient(testServer, anonCreds, nil)
Ok(t, err)
tempSecrets, err := anonClient.ExchangeCode("good-code")
Ok(t, err)

tmpDir, cleanup := DirStructure(t, map[string]interface{}{
"key.pem": tempSecrets.Key,
})
defer cleanup()
keyPath := fmt.Sprintf("%v/key.pem", tmpDir)

appCreds := &vcs.GithubAppCredentials{
AppID: tempSecrets.ID,
KeyPath: keyPath,
Hostname: testServer,
AppSlug: "some-app",
}

user, err := appCreds.GetUser()
Ok(t, err)

Assert(t, user == "Octocat App[bot]", "user should not empty")
}

func TestGithubClient_AppAuthentication(t *testing.T) {
defer disableSSLVerification()()
testServer, err := fixtures.GithubAppTestServer(t)
Expand Down Expand Up @@ -40,6 +70,11 @@ func TestGithubClient_AppAuthentication(t *testing.T) {
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)
}
Expand Down
15 changes: 14 additions & 1 deletion server/events/vcs/mocks/matchers/ptr_to_http_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 11 additions & 7 deletions server/events/vcs/mocks/mock_github_credentials.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
AppID: userConfig.GithubAppID,
KeyPath: userConfig.GithubAppKey,
Hostname: userConfig.GithubHostname,
AppSlug: userConfig.GithubAppSlug,
}
githubAppEnabled = true
}
Expand Down
1 change: 1 addition & 0 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type UserConfig struct {
GithubOrg string `mapstructure:"gh-org"`
GithubAppID int64 `mapstructure:"gh-app-id"`
GithubAppKey string `mapstructure:"gh-app-key-file"`
GithubAppSlug string `mapstructure:"gh-app-slug"`
GitlabHostname string `mapstructure:"gitlab-hostname"`
GitlabToken string `mapstructure:"gitlab-token"`
GitlabUser string `mapstructure:"gitlab-user"`
Expand Down

0 comments on commit 8f8e7d9

Please sign in to comment.