Skip to content

Commit cb5a0e7

Browse files
fatmcgav-depopScott Schulthess
authored andcommitted
Add support for authentication as a GitHub App
This commit adds support for authentication as a GitHub App. This is beneficial in several ways, including: * Increased rate limits * Better separation of access * Finer grained control over access * Removes the need for a bot or service account As part of these changes, have added the `github.com/bradleyfalzon/ghinstallation/v2` module and it's associated dependencies. Added several new configuration fields: * `UseGitHubApp` - Boolean flag signalling if user wants to auth as a GitHub App * `PrivateKeyFile` - Filename for RSA Private Key generated for GitHub App * `AppID` - GitHub App application numerical identifier * `InstallationID` - GitHub App installation numerical identifier Upgrade to golang `1.16` Add some tests for the `Source` model. Add support for both `private_key` and `private_key_file` intputs. This enables either reading the private key from a file, or providing the contents of the private key. Expanded testing to cover additional scenarios. Also rename `AppID` to `ApplicationID`. add manifest yaml bump remove unused travis use real image tags in dockefile, update go.mod update go sum comment xoauth basic comment xoauth basic comment xoauth basic comment xoauth basic add git credentail helper fix dockerfile add credential helper sds sds
1 parent 19746b9 commit cb5a0e7

File tree

9 files changed

+190
-26
lines changed

9 files changed

+190
-26
lines changed

Dockerfile

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
1-
ARG golang
2-
ARG alpine
3-
4-
5-
6-
FROM ${golang} AS builder
7-
1+
FROM public.ecr.aws/docker/library/golang:1.22 as builder
82
ENV DEBIAN_FRONTEND=noninteractive
93
RUN apt-get -y -qq update \
104
&& apt-get -y -qq install "make"
115

126
ADD . /go/src/github.com/telia-oss/github-pr-resource
137
WORKDIR /go/src/github.com/telia-oss/github-pr-resource
148

15-
RUN go version \
16-
&& make all
9+
RUN go version && make all
1710

18-
19-
20-
FROM ${alpine} AS resource
11+
FROM public.ecr.aws/docker/library/alpine:3.21.3 AS resource
2112
RUN apk add --update --no-cache \
2213
git \
2314
git-lfs \
15+
curl \
2416
openssh \
2517
git-crypt
2618
COPY scripts/askpass.sh /usr/local/bin/askpass.sh
19+
ENV GITHUB_APP_CRED_HELPER_VERSION="v0.3.2"
20+
ENV BIN_PATH_TARGET=/usr/local/bin
21+
RUN curl -L https://github.com/bdellegrazie/git-credential-github-app/releases/download/${GITHUB_APP_CRED_HELPER_VERSION}/git-credential-github-app_${GITHUB_APP_CRED_HELPER_VERSION}_Linux_x86_64.tar.gz | tar zxv -C ${BIN_PATH_TARGET}
2722
COPY --from=builder /go/src/github.com/telia-oss/github-pr-resource/build /opt/resource
2823
RUN chmod +x /opt/resource/*
2924

30-
31-
3225
FROM resource
3326
LABEL MAINTAINER=cloudfoundry-community

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,4 @@ Here is the list of changes:
312312
Note that if you are migrating from the original resource on a Concourse version prior to `v5.0.0`, you might
313313
see an error `failed to unmarshal request: json: unknown field "ref"`. The solution is to rename the resource
314314
so that the history is wiped. See telia-oss/github-pr-resource#64 for details.
315+

git.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ func (g *GitClient) Init(branch string) error {
7575
if err := g.command("git", "config", "--global", "user.email", "concourse@local").Run(); err != nil {
7676
return fmt.Errorf("failed to configure git email: %s", err)
7777
}
78-
if err := g.command("git", "config", "--global", "url.https://[email protected]/.insteadOf", "[email protected]:").Run(); err != nil {
78+
fmt.Println("SDS")
79+
if err := g.command("git", "config", "credential.https://github.com.helper","'!git-credential-github-app --appId ((github/concourse-app-id)) -organization ((github/concourse-app-organization-name)) -username x-access-token'").Run(); err != nil {
7980
return fmt.Errorf("failed to configure github url: %s", err)
8081
}
8182
if err := g.command("git", "config", "--global", "url.https://.insteadOf", "git://").Run(); err != nil {
@@ -91,6 +92,7 @@ func (g *GitClient) Pull(uri, branch string, depth int, submodules bool, fetchTa
9192
return err
9293
}
9394

95+
9496
if err := g.command("git", "remote", "add", "origin", endpoint).Run(); err != nil {
9597
return fmt.Errorf("setting 'origin' remote to '%s' failed: %s", endpoint, err)
9698
}
@@ -232,6 +234,6 @@ func (g *GitClient) Endpoint(uri string) (string, error) {
232234
if err != nil {
233235
return "", fmt.Errorf("failed to parse commit url: %s", err)
234236
}
235-
endpoint.User = url.UserPassword("x-oauth-basic", g.AccessToken)
237+
//endpoint.User = url.UserPassword("x-oauth-basic", g.AccessToken)
236238
return endpoint.String(), nil
237239
}

github.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strconv"
1313
"strings"
1414

15+
"github.com/bradleyfalzon/ghinstallation/v2"
1516
"github.com/google/go-github/v61/github"
1617
"github.com/shurcooL/githubv4"
1718
"golang.org/x/oauth2"
@@ -46,23 +47,45 @@ func NewGithubClient(s *Source) (*GithubClient, error) {
4647
return nil, err
4748
}
4849

50+
// We need a transport that we can update if using GitHub App authentication
51+
transport := http.DefaultTransport.(*http.Transport).Clone()
52+
4953
// Skip SSL verification for self-signed certificates
5054
// source: https://github.com/google/go-github/pull/598#issuecomment-333039238
5155
var ctx context.Context
5256
if s.SkipSSLVerification {
53-
insecureClient := &http.Client{
54-
Transport: &http.Transport{
55-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
56-
},
57-
}
57+
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
58+
insecureClient := &http.Client{Transport: transport}
5859
ctx = context.WithValue(context.TODO(), oauth2.HTTPClient, insecureClient)
5960
} else {
6061
ctx = context.TODO()
6162
}
6263

63-
client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(
64-
&oauth2.Token{AccessToken: s.AccessToken},
65-
))
64+
var client *http.Client
65+
if s.UseGitHubApp {
66+
var ghAppInstallationTransport *ghinstallation.Transport
67+
if s.PrivateKeyFile != "" {
68+
ghAppInstallationTransport, err = ghinstallation.NewKeyFromFile(transport, s.ApplicationID, s.InstallationID, s.PrivateKeyFile)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to generate application installation access token using private key file: %s", err)
71+
}
72+
} else {
73+
ghAppInstallationTransport, err = ghinstallation.New(transport, s.ApplicationID, s.InstallationID, []byte(s.PrivateKey))
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to generate application installation access token using private key: %s", err)
76+
}
77+
}
78+
79+
// Client using ghinstallation transport
80+
client = &http.Client{
81+
Transport: ghAppInstallationTransport,
82+
}
83+
} else {
84+
// Client using oauth2 wrapper
85+
client = oauth2.NewClient(ctx, oauth2.StaticTokenSource(
86+
&oauth2.Token{AccessToken: s.AccessToken},
87+
))
88+
}
6689

6790
var v3 *github.Client
6891
if s.V3Endpoint != "" {

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.0
55
toolchain go1.23.7
66

77
require (
8+
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0
89
github.com/google/go-github/v61 v61.0.0
910
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1
1011
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
@@ -14,6 +15,8 @@ require (
1415

1516
require (
1617
github.com/davecgh/go-spew v1.1.1 // indirect
18+
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
19+
github.com/google/go-github/v69 v69.0.0 // indirect
1720
github.com/google/go-querystring v1.1.0 // indirect
1821
github.com/kr/pretty v0.3.1 // indirect
1922
github.com/pmezard/go-difflib v1.0.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0 h1:0D4vKCHOvYrDU8u61TnE2JfNT4VRrBLphmxtqazTO+M=
2+
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0/go.mod h1:LOVmdZYVZ8jqdr4n9wWm1ocDiMz9IfMGfRkaYC1a52A=
13
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
24
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
7+
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
48
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
59
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
610
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
711
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
812
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
13+
github.com/google/go-github/v69 v69.0.0 h1:YnFvZ3pEIZF8KHmI8xyQQe3mYACdkhnaTV2hr7CP2/w=
14+
github.com/google/go-github/v69 v69.0.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
915
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
1016
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
1117
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=

manifest.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name: github-pr-resource
2+
version: 0.0.1

models.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,28 @@ type Source struct {
2929
States []githubv4.PullRequestState `json:"states"`
3030
TrustedTeams []string `json:"trusted_teams"`
3131
TrustedUsers []string `json:"trusted_users"`
32+
UseGitHubApp bool `json:"use_github_app"`
33+
PrivateKey string `json:"private_key"`
34+
PrivateKeyFile string `json:"private_key_file"`
35+
ApplicationID int64 `json:"application_id"`
36+
InstallationID int64 `json:"installation_id"`
3237
}
3338

3439
// Validate the source configuration.
3540
func (s *Source) Validate() error {
36-
if s.AccessToken == "" {
37-
return errors.New("access_token must be set")
41+
if s.AccessToken == "" && !s.UseGitHubApp {
42+
return errors.New("access_token must be set if not using GitHub App authentication")
43+
}
44+
if s.UseGitHubApp {
45+
if s.PrivateKey == "" && s.PrivateKeyFile == "" {
46+
return errors.New("Either private_key or private_key_file should be supplied if using GitHub App authentication")
47+
}
48+
if s.ApplicationID == 0 || s.InstallationID == 0 {
49+
return errors.New("application_id and installation_id must be set if using GitHub App authentication")
50+
}
51+
if s.AccessToken != "" {
52+
return errors.New("access_token is not required when using GitHub App authentication")
53+
}
3854
}
3955
if s.Repository == "" {
4056
return errors.New("repository must be set")

models_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package resource_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
resource "github.com/telia-oss/github-pr-resource"
9+
)
10+
11+
func TestSource(t *testing.T) {
12+
tests := []struct {
13+
description string
14+
source resource.Source
15+
wantErr string
16+
}{
17+
{
18+
description: "validate passes",
19+
source: resource.Source{
20+
AccessToken: "123456",
21+
Repository: "test/test",
22+
},
23+
},
24+
{
25+
description: "should have an access_token",
26+
source: resource.Source{
27+
Repository: "test/test",
28+
},
29+
wantErr: "access_token must be set if not using GitHub App authentication",
30+
},
31+
{
32+
description: "should have a repository",
33+
source: resource.Source{
34+
AccessToken: "123456",
35+
},
36+
wantErr: "repository must be set",
37+
},
38+
{
39+
description: "should support GitHub App authentication",
40+
source: resource.Source{
41+
Repository: "test/test",
42+
UseGitHubApp: true,
43+
PrivateKey: "key.pem",
44+
ApplicationID: 123456,
45+
InstallationID: 1,
46+
},
47+
},
48+
{
49+
description: "requires a private_key or private_key_file GitHub App configuration values",
50+
source: resource.Source{
51+
Repository: "test/test",
52+
UseGitHubApp: true,
53+
ApplicationID: 123456,
54+
InstallationID: 1,
55+
},
56+
wantErr: "Either private_key or private_key_file should be supplied if using GitHub App authentication",
57+
},
58+
{
59+
description: "requires an application_id and installation_id GitHub App configuration values",
60+
source: resource.Source{
61+
Repository: "test/test",
62+
UseGitHubApp: true,
63+
PrivateKey: "key.pem",
64+
ApplicationID: 123456,
65+
},
66+
wantErr: "application_id and installation_id must be set if using GitHub App authentication",
67+
},
68+
{
69+
description: "should not have an access_token when using GitHub App authentication",
70+
source: resource.Source{
71+
Repository: "test/test",
72+
UseGitHubApp: true,
73+
PrivateKey: "key.pem",
74+
ApplicationID: 123456,
75+
InstallationID: 1,
76+
AccessToken: "123456",
77+
},
78+
wantErr: "access_token is not required when using GitHub App authentication",
79+
},
80+
{
81+
description: "requires v3_endpoint when v4_endpoint is set",
82+
source: resource.Source{
83+
AccessToken: "123456",
84+
Repository: "test/test",
85+
V3Endpoint: "https://github.com/v3",
86+
},
87+
wantErr: "v4_endpoint must be set together with v3_endpoint",
88+
},
89+
{
90+
description: "requires v4_endpoint when v3_endpoint is set",
91+
source: resource.Source{
92+
AccessToken: "123456",
93+
Repository: "test/test",
94+
V4Endpoint: "https://github.com/v4",
95+
},
96+
wantErr: "v3_endpoint must be set together with v4_endpoint",
97+
},
98+
}
99+
100+
for _, tc := range tests {
101+
t.Run(tc.description, func(t *testing.T) {
102+
err := tc.source.Validate()
103+
104+
if tc.wantErr != "" {
105+
if err == nil {
106+
t.Logf("Expected error '%s', got nothing", tc.wantErr)
107+
t.Fail()
108+
}
109+
assert.EqualError(t, err, tc.wantErr, fmt.Sprintf("Expected '%s', got '%s'", tc.wantErr, err))
110+
}
111+
112+
if tc.wantErr == "" && err != nil {
113+
t.Logf("Got an error when none expected: %s", err)
114+
t.Fail()
115+
}
116+
})
117+
}
118+
}

0 commit comments

Comments
 (0)