Skip to content

Expose the token of the installation client #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (h *PRCommentHandler) Handle(ctx context.Context, eventType, deliveryID str
return nil
}

client, err := h.NewInstallationClient(installationID)
client, _, err := h.NewInstallationClient(installationID)
if err != nil {
return err
}
Expand Down
108 changes: 108 additions & 0 deletions example/pr_review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2018 Palantir Technologies, Inc.
//
// 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 main

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/google/go-github/v33/github"
"github.com/palantir/go-githubapp/githubapp"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
transport_http "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)

type PRReviewHandler struct {
githubapp.ClientCreator

preamble string
}

func (h *PRReviewHandler) Handles() []string {
return []string{"pull_request"}
}

func (h *PRReviewHandler) Handle(ctx context.Context, eventType, deliveryID string, payload []byte) error {
var event github.IssueCommentEvent
if err := json.Unmarshal(payload, &event); err != nil {
return errors.Wrap(err, "failed to parse issue comment event payload")
}

if !event.GetIssue().IsPullRequest() {
zerolog.Ctx(ctx).Debug().Msg("Issue comment event is not for a pull request")
return nil
}

repo := event.GetRepo()
prNum := event.GetIssue().GetNumber()
installationID := githubapp.GetInstallationIDFromEvent(&event)

ctx, logger := githubapp.PreparePRContext(ctx, installationID, repo, event.GetIssue().GetNumber())

logger.Debug().Msgf("Event action is %s", event.GetAction())
if event.GetAction() != "created" {
return nil
}

// Get Access Token
client, ts, err := h.NewInstallationClient(installationID)
if err != nil {
return err
}
token, err := ts.Token(context.Background())

// Clone the repository
tokenAuth := &transport_http.BasicAuth{Username: "x-access-token", Password: token}
storer := memory.NewStorage()
gitRepo, err := git.Clone(storer, nil, &git.CloneOptions{
URL: "https://github.com/palantir/go-githubapp.git",
Auth: tokenAuth,
})

// Insert your own advanced Git scenario here:
mainRef, _ := gitRepo.Reference(plumbing.NewBranchReferenceName(event.GetRepo().GetMasterBranch()), true)
commit, _ := gitRepo.CommitObject(mainRef.Hash())
logger.Debug().Msgf("Last commit on master was by %s", commit.Author.Name)

// Remainder of old logic...
repoOwner := repo.GetOwner().GetLogin()
repoName := repo.GetName()
author := event.GetComment().GetUser().GetLogin()
body := event.GetComment().GetBody()

if strings.HasSuffix(author, "[bot]") {
logger.Debug().Msg("Issue comment was created by a bot")
return nil
}

logger.Debug().Msgf("Echoing comment on %s/%s#%d by %s", repoOwner, repoName, prNum, author)
msg := fmt.Sprintf("%s\n%s said\n```\n%s\n```\n", h.preamble, author, body)
prComment := github.IssueComment{
Body: &msg,
}

if _, _, err := client.Issues.CreateComment(ctx, repoOwner, repoName, prNum, &prComment); err != nil {
logger.Error().Err(err).Msg("Failed to comment on pull request")
}

return nil
}
38 changes: 24 additions & 14 deletions githubapp/caching_client_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ const (
DefaultCachingClientCapacity = 64
)

type clientTokenSource struct {
client *github.Client
tokenSource TokenSource
}

type clientV4TokenSource struct {
client *githubv4.Client
tokenSource TokenSource
}

// NewDefaultCachingClientCreator returns a ClientCreator using values from the
// configuration or other defaults.
func NewDefaultCachingClientCreator(c Config, opts ...ClientOption) (ClientCreator, error) {
Expand Down Expand Up @@ -70,42 +80,42 @@ func (c *cachingClientCreator) NewAppV4Client() (*githubv4.Client, error) {
return c.delegate.NewAppV4Client()
}

func (c *cachingClientCreator) NewInstallationClient(installationID int64) (*github.Client, error) {
func (c *cachingClientCreator) NewInstallationClient(installationID int64) (*github.Client, TokenSource, error) {
// if client is in cache, return it
key := c.toCacheKey("v3", installationID)
val, ok := c.cachedClients.Get(key)
if ok {
if client, ok := val.(*github.Client); ok {
return client, nil
if cts, ok := val.(clientTokenSource); ok {
return cts.client, cts.tokenSource, nil
}
}

// otherwise, create and return
client, err := c.delegate.NewInstallationClient(installationID)
client, ts, err := c.delegate.NewInstallationClient(installationID)
if err != nil {
return nil, err
return nil, nil, err
}
c.cachedClients.Add(key, client)
return client, nil
c.cachedClients.Add(key, clientTokenSource{client, ts})
return client, ts, nil
}

func (c *cachingClientCreator) NewInstallationV4Client(installationID int64) (*githubv4.Client, error) {
func (c *cachingClientCreator) NewInstallationV4Client(installationID int64) (*githubv4.Client, TokenSource, error) {
// if client is in cache, return it
key := c.toCacheKey("v4", installationID)
val, ok := c.cachedClients.Get(key)
if ok {
if client, ok := val.(*githubv4.Client); ok {
return client, nil
if cts, ok := val.(clientV4TokenSource); ok {
return cts.client, cts.tokenSource, nil
}
}

// otherwise, create and return
client, err := c.delegate.NewInstallationV4Client(installationID)
client, ts, err := c.delegate.NewInstallationV4Client(installationID)
if err != nil {
return nil, err
return nil, nil, err
}
c.cachedClients.Add(key, client)
return client, nil
c.cachedClients.Add(key, clientV4TokenSource{client, ts})
return client, ts, nil
}

func (c *cachingClientCreator) NewTokenClient(token string) (*github.Client, error) {
Expand Down
37 changes: 22 additions & 15 deletions githubapp/client_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ type ClientCreator interface {
// * the installation ID is the ID that is shown in the URL of https://{githubURL}/settings/installations/{#}
// (navigate to the "installations" page without the # and go to the app's page to see the number)
// * the key bytes must be a PEM-encoded PKCS1 or PKCS8 private key for the application
NewInstallationClient(installationID int64) (*github.Client, error)
NewInstallationClient(installationID int64) (*github.Client, TokenSource, error)

// NewInstallationV4Client returns an installation-authenticated v4 API client, similar to NewInstallationClient.
NewInstallationV4Client(installationID int64) (*githubv4.Client, error)
NewInstallationV4Client(installationID int64) (*githubv4.Client, TokenSource, error)

// NewTokenClient returns a *github.Client that uses the passed in OAuth token for authentication.
NewTokenClient(token string) (*github.Client, error)
Expand Down Expand Up @@ -209,9 +209,9 @@ func (c *clientCreator) NewAppV4Client() (*githubv4.Client, error) {
return client, nil
}

func (c *clientCreator) NewInstallationClient(installationID int64) (*github.Client, error) {
func (c *clientCreator) NewInstallationClient(installationID int64) (*github.Client, TokenSource, error) {
base := c.newHTTPClient()
installation, transportError := newInstallation(c.integrationID, installationID, c.privKeyBytes, c.v3BaseURL)
installation, ghTransport, transportError := newInstallation(c.integrationID, installationID, c.privKeyBytes, c.v3BaseURL)

middleware := []ClientMiddleware{installation}
if c.cacheFunc != nil {
Expand All @@ -220,30 +220,34 @@ func (c *clientCreator) NewInstallationClient(installationID int64) (*github.Cli

client, err := c.newClient(base, middleware, fmt.Sprintf("installation: %d", installationID), installationID)
if err != nil {
return nil, err
return nil, nil, err
}
if *transportError != nil {
return nil, *transportError
return nil, nil, *transportError
}
return client, nil
return client, *ghTransport, nil
}

func (c *clientCreator) NewInstallationV4Client(installationID int64) (*githubv4.Client, error) {
func (c *clientCreator) NewInstallationV4Client(installationID int64) (*githubv4.Client, TokenSource, error) {
base := c.newHTTPClient()
installation, transportError := newInstallation(c.integrationID, installationID, c.privKeyBytes, c.v3BaseURL)
installation, ghTransport, transportError := newInstallation(c.integrationID, installationID, c.privKeyBytes, c.v3BaseURL)

// The v4 API primarily uses POST requests (except for introspection queries)
// which we cannot cache, so don't construct the middleware
middleware := []ClientMiddleware{installation}

client, err := c.newV4Client(base, middleware, fmt.Sprintf("installation: %d", installationID))
if err != nil {
return nil, err
return nil, nil, err
}
if *transportError != nil {
return nil, *transportError
return nil, nil, *transportError
}
return client, nil
return client, *ghTransport, nil
}

type TokenSource interface {
Token(ctx context.Context) (string, error)
}

func (c *clientCreator) NewTokenClient(token string) (*github.Client, error) {
Expand Down Expand Up @@ -334,19 +338,22 @@ func newAppInstallation(integrationID int64, privKeyBytes []byte, v3BaseURL stri
return installation, &transportError
}

func newInstallation(integrationID, installationID int64, privKeyBytes []byte, v3BaseURL string) (ClientMiddleware, *error) {
func newInstallation(integrationID, installationID int64, privKeyBytes []byte, v3BaseURL string) (ClientMiddleware, **ghinstallation.Transport, *error) {
var transportError error
var itr *ghinstallation.Transport
installation := func(next http.RoundTripper) http.RoundTripper {
itr, err := ghinstallation.New(next, integrationID, installationID, privKeyBytes)
var err error
itr, err = ghinstallation.New(next, integrationID, installationID, privKeyBytes)
if err != nil {
transportError = err

return next
}
// leaving the v3 URL since this is used to refresh the token, not make queries
itr.BaseURL = strings.TrimSuffix(v3BaseURL, "/")
return itr
}
return installation, &transportError
return installation, &itr, &transportError
}

func cache(cacheFunc func() httpcache.Cache) ClientMiddleware {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.13
require (
github.com/alexedwards/scs v1.4.1
github.com/bradleyfalzon/ghinstallation v1.1.1
github.com/go-git/go-git/v5 v5.3.0 // indirect
github.com/google/go-github/v29 v29.0.3 // indirect
github.com/google/go-github/v33 v33.0.0
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
Expand All @@ -17,5 +18,5 @@ require (
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
goji.io v2.0.2+incompatible
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3
gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v2 v2.3.0
)
Loading