Skip to content

Commit

Permalink
Merge pull request #5 from shogo82148/implement-github-token-validater
Browse files Browse the repository at this point in the history
implement GitHub Token validator
  • Loading branch information
shogo82148 authored Aug 31, 2021
2 parents 0f8e530 + 0dea6af commit 2f377f0
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 2 deletions.
114 changes: 112 additions & 2 deletions provider/github-app-token/github-app-token.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,37 @@ import (
"log"
"net/http"
"strconv"
"strings"

"github.com/shogo82148/actions-github-app-token/provider/github-app-token/github"
)

type Handler struct{}
type githubClient interface {
CreateStatus(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error)
}

const (
commitStatusContext = "github-app-token"
creatorLogin = "github-actions[bot]"
creatorID = 41898282
creatorType = "Bot"
)

type Handler struct {
github githubClient
}

func NewHandler() *Handler {
return &Handler{}
return &Handler{
github: github.NewClient(nil),
}
}

type requestBody struct {
GitHubToken string `json:"github_token"`
Repository string `json:"repository"`
SHA string `json:"sha"`
APIURL string `json:"api_url"`
}

type responseBody struct {
Expand Down Expand Up @@ -67,6 +88,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

func (h *Handler) handle(ctx context.Context, req *requestBody) (*responseBody, error) {
if err := h.validateGitHubToken(ctx, req); err != nil {
return nil, err
}

// TODO: implement me

return &responseBody{}, nil
}

Expand Down Expand Up @@ -97,3 +124,86 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
w.WriteHeader(status)
w.Write(data)
}

func (h *Handler) validateGitHubToken(ctx context.Context, req *requestBody) error {
// early check of the token prefix
// ref. https://github.blog/changelog/2021-03-31-authentication-token-format-updates-are-generally-available/
if len(req.GitHubToken) < 4 {
return &validationError{
message: "GITHUB_TOKEN has invalid format",
}
}
switch req.GitHubToken[:4] {
case "ghp_":
// Personal Access Tokens
return &validationError{
message: "GITHUB_TOKEN looks like Personal Access Token. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.",
}
case "gho_":
// OAuth Access tokens
return &validationError{
message: "GITHUB_TOKEN looks like OAuth Access token. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.",
}
case "ghu_":
// GitHub App user-to-server tokens
return &validationError{
message: "GITHUB_TOKEN looks like GitHub App user-to-server token. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.",
}
case "ghs_":
// GitHub App server-to-server tokens
// It's OK
case "ghr_":
// GitHub App refresh tokens
return &validationError{
message: "GITHUB_TOKEN looks like GitHub App refresh token. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.",
}
default:
// Old Format Personal Access Tokens
return &validationError{
message: "GITHUB_TOKEN looks like Personal Access Token. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.",
}
}
resp, err := h.updateCommitStatus(ctx, req, &github.CreateStatusRequest{
State: github.CommitStateSuccess,
Description: "valid github token",
Context: commitStatusContext,
})
if err != nil {
var githubErr *github.UnexpectedStatusCodeError
if errors.As(err, &githubErr) {
if 400 <= githubErr.StatusCode && githubErr.StatusCode < 500 {
return &validationError{
message: "Your GITHUB_TOKEN doesn't have enough permission. Write-Permission is required.",
}
}
}
return err
}
if resp.Creator.Login != creatorLogin || resp.Creator.ID != creatorID || resp.Creator.Type != creatorType {
return &validationError{
message: fmt.Sprintf("`github-token` isn't be generated by @%s. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.", creatorLogin),
}
}
return nil
}

func splitOwnerRepo(fullname string) (owner, repo string, err error) {
idx := strings.IndexByte(fullname, '/')
if idx < 0 {
err = &validationError{
message: fmt.Sprintf("invalid repository name: %s", fullname),
}
return
}
owner = fullname[:idx]
repo = fullname[idx+1:]
return
}

func (h *Handler) updateCommitStatus(ctx context.Context, req *requestBody, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) {
owner, repo, err := splitOwnerRepo(req.Repository)
if err != nil {
return nil, err
}
return h.github.CreateStatus(ctx, req.GitHubToken, owner, repo, req.SHA, status)
}
114 changes: 114 additions & 0 deletions provider/github-app-token/github-app-token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package githubapptoken

import (
"context"
"errors"
"net/http"
"testing"

"github.com/shogo82148/actions-github-app-token/provider/github-app-token/github"
)

type githubClientMock struct {
CreateStatusFunc func(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error)
}

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)
}

func TestValidateGitHubToken(t *testing.T) {
h := &Handler{
github: &githubClientMock{
CreateStatusFunc: func(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) {
if token != "ghs_dummyGitHubToken" {
t.Errorf("unexpected GitHub Token: want %q, got %q", "ghs_dummyGitHubToken", token)
}
if owner != "fuller-inc" {
t.Errorf("unexpected owner: want %q, got %q", "fuller-inc", owner)
}
if repo != "actions-aws-assume-role" {
t.Errorf("unexpected repo: want %q, got %q", "actions-aws-assume-role", repo)
}
if ref != "e3a45c6c16c1464826b36a598ff39e6cc98c4da4" {
t.Errorf("unexpected ref: want %q, got %q", "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", ref)
}
if status.State != github.CommitStateSuccess {
t.Errorf("unexpected commit status state: want %s, got %s", github.CommitStateSuccess, status.State)
}
if status.Context != commitStatusContext {
t.Errorf("unexpected commit status context: want %q, got %q", commitStatusContext, status.Context)
}
return &github.CreateStatusResponse{
Creator: &github.CreateStatusResponseCreator{
Login: creatorLogin,
ID: creatorID,
Type: creatorType,
},
}, nil
},
},
}
err := h.validateGitHubToken(context.Background(), &requestBody{
GitHubToken: "ghs_dummyGitHubToken",
Repository: "fuller-inc/actions-aws-assume-role",
SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4",
})
if err != nil {
t.Error(err)
}
}

func TestValidateGitHubToken_PermissionError(t *testing.T) {
h := &Handler{
github: &githubClientMock{
CreateStatusFunc: func(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) {
return nil, &github.UnexpectedStatusCodeError{
StatusCode: http.StatusBadRequest,
}
},
},
}
err := h.validateGitHubToken(context.Background(), &requestBody{
GitHubToken: "ghs_dummyGitHubToken",
Repository: "fuller-inc/actions-aws-assume-role",
SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4",
})
if err == nil {
t.Error("want error, but not")
}

var validate *validationError
if !errors.As(err, &validate) {
t.Errorf("want validation error, got %T", err)
}
}

func TestValidateGitHubToken_InvalidCreator(t *testing.T) {
h := &Handler{
github: &githubClientMock{
CreateStatusFunc: func(ctx context.Context, token, owner, repo, ref string, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) {
return &github.CreateStatusResponse{
Creator: &github.CreateStatusResponseCreator{
Login: "shogo82148",
ID: 1157344,
Type: "User",
},
}, nil
},
},
}
err := h.validateGitHubToken(context.Background(), &requestBody{
GitHubToken: "ghs_dummyGitHubToken",
Repository: "fuller-inc/actions-aws-assume-role",
SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4",
})
if err == nil {
t.Error("want error, but not")
}

var validate *validationError
if !errors.As(err, &validate) {
t.Errorf("want validation error, got %T", err)
}
}

0 comments on commit 2f377f0

Please sign in to comment.