Skip to content

Commit

Permalink
Merge pull request #18 from shogo82148/handle-get-repos-installation-…
Browse files Browse the repository at this point in the history
…not-found

handle installation not found
  • Loading branch information
shogo82148 authored Sep 5, 2021
2 parents 63e242a + 8459adc commit 2f7f9fa
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 0 deletions.
6 changes: 6 additions & 0 deletions provider/github-app-token/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (

type githubClientDummy struct{}

func (c *githubClientDummy) GetApp(ctx context.Context) (*github.GetAppResponse, error) {
return &github.GetAppResponse{
HTMLURL: "https://github.com/shogo82148/actions-github-app-token",
}, nil
}

func (c *githubClientDummy) CreateStatus(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) {
if token != "ghs_dummyGitHubToken" || owner != "shogo82148" || repo != "actions-aws-assume-role" || ref != "e3a45c6c16c1464826b36a598ff39e6cc98c4da4" {
return nil, &github.ErrUnexpectedStatusCode{StatusCode: http.StatusBadRequest}
Expand Down
22 changes: 22 additions & 0 deletions provider/github-app-token/github-app-token.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
)

type githubClient interface {
GetApp(ctx context.Context) (*github.GetAppResponse, error)
CreateStatus(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error)
GetReposInstallation(ctx context.Context, owner, repo string) (*github.GetReposInstallationResponse, error)
CreateAppAccessToken(ctx context.Context, installationID uint64, permissions *github.CreateAppAccessTokenRequest) (*github.CreateAppAccessTokenResponse, error)
Expand All @@ -34,6 +35,7 @@ const (

type Handler struct {
github githubClient
app *github.GetAppResponse
}

func NewHandler() (*Handler, error) {
Expand Down Expand Up @@ -70,8 +72,15 @@ func NewHandler() (*Handler, error) {
if err != nil {
return nil, err
}

app, err := c.GetApp(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get the app information, check your configure: %w", err)
}

return &Handler{
github: c,
app: app,
}, nil
}

Expand Down Expand Up @@ -149,6 +158,19 @@ func (h *Handler) handle(ctx context.Context, req *requestBody) (*responseBody,
}
inst, err := h.github.GetReposInstallation(ctx, owner, repo)
if err != nil {
var ghErr *github.ErrUnexpectedStatusCode
if errors.As(err, &ghErr) && ghErr.StatusCode == http.StatusNotFound {
// installation not found.
// the user may not install the app.
return nil, &validationError{
message: fmt.Sprintf(
"Installation not found. "+
"You need to install the GitHub App to use the action. "+
"See %s for more detail",
h.app.HTMLURL,
),
}
}
return nil, fmt.Errorf("failed to get resp's installation: %w", err)
}
token, err := h.github.CreateAppAccessToken(ctx, inst.ID, &github.CreateAppAccessTokenRequest{
Expand Down
5 changes: 5 additions & 0 deletions provider/github-app-token/github-app-token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import (
)

type githubClientMock struct {
GetAppFunc func(ctx context.Context) (*github.GetAppResponse, error)
CreateStatusFunc func(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error)
GetReposInstallationFunc func(ctx context.Context, owner, repo string) (*github.GetReposInstallationResponse, error)
CreateAppAccessTokenFunc func(ctx context.Context, installationID uint64, permissions *github.CreateAppAccessTokenRequest) (*github.CreateAppAccessTokenResponse, error)
ValidateAPIURLFunc func(url string) error
}

func (c *githubClientMock) GetApp(ctx context.Context) (*github.GetAppResponse, error) {
return c.GetAppFunc(ctx)
}

func (c *githubClientMock) CreateStatus(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) {
return c.CreateStatusFunc(ctx, token, owner, repo, ref, status)
}
Expand Down
51 changes: 51 additions & 0 deletions provider/github-app-token/github/get_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package github

import (
"context"
"encoding/json"
"fmt"
"net/http"
)

type GetAppResponse struct {
HTMLURL string `json:"html_url"`

// omit other fields, we don't use them.
}

// GetApp returns the GitHub App associated with the authentication credentials used.
// https://docs.github.com/en/rest/reference/apps#get-the-authenticated-app
func (c *Client) GetApp(ctx context.Context) (*GetAppResponse, error) {
token, err := c.generateJWT()
if err != nil {
return nil, err
}

// build the request
u := fmt.Sprintf("%s/app", c.baseURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", githubUserAgent)
req.Header.Set("Authorization", "Bearer "+token)

// send the request
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// parse the response
if resp.StatusCode != http.StatusOK {
return nil, newErrUnexpectedStatusCode(resp)
}

var ret *GetAppResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return ret, nil
}
94 changes: 94 additions & 0 deletions provider/github-app-token/github/get_app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package github

import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"

"github.com/golang-jwt/jwt/v4"
)

func TestGetApp(t *testing.T) {
privateKey, err := os.ReadFile("./testdata/id_rsa_for_testing")
if err != nil {
t.Fatal(err)
}
block, _ := pem.Decode(privateKey)
if block == nil {
t.Fatal("no key found")
}
if block.Type != "RSA PRIVATE KEY" {
t.Fatalf("unsupported key type: %q", block.Type)
}

rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}

ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected method: want GET, got %s", r.Method)
}

auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
t.Errorf("unexpected Authorization header: %q", auth)
rw.WriteHeader(http.StatusUnauthorized)
return
}
auth = strings.TrimPrefix(auth, "Bearer ")
token, err := jwt.Parse(auth, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return &rsaPrivateKey.PublicKey, nil
})
if err != nil {
t.Error(err)
rw.WriteHeader(http.StatusUnauthorized)
return
}
claims := token.Claims.(jwt.MapClaims)
iss := claims["iss"].(string)
if iss != "123456" {
t.Errorf("unexpected issuer: want %q, got %q", "123456", iss)
}

path := "/app"
if r.URL.Path != path {
t.Errorf("unexpected path: want %q, got %q", path, r.URL.Path)
}

data, err := os.ReadFile("testdata/app.json")
if err != nil {
panic(err)
}
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
rw.WriteHeader(http.StatusOK)
rw.Write(data)
}))
defer ts.Close()

c, err := NewClient(nil, 123456, privateKey)
if err != nil {
t.Fatal(err)
}
c.baseURL = ts.URL

resp, err := c.GetApp(context.Background())
if err != nil {
t.Fatal(err)
}
if resp.HTMLURL != "https://github.com/apps/octoapp" {
t.Errorf("unexpected html url: want %q, got %q", "https://github.com/apps/octoapp", resp.HTMLURL)
}
}
41 changes: 41 additions & 0 deletions provider/github-app-token/github/testdata/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"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"
]
}

0 comments on commit 2f7f9fa

Please sign in to comment.