Skip to content
This repository was archived by the owner on Sep 6, 2025. It is now read-only.

Commit 4e07bb9

Browse files
committed
add option to use zappr github app's token for calls to github
1 parent 87d1060 commit 4e07bb9

File tree

9 files changed

+137
-116
lines changed

9 files changed

+137
-116
lines changed

.github.sample.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# Defines the url for Zappr. Comment it out to load it from ZAPPR_URL environment variable
88
URL="https://zappr.domain.com"
99

10-
# Defines the Zappr token. Comment it out to load it from ZAPPR_TOKEN environment variable
11-
Token=""
10+
# Specifies if calls made by Zappr to Github should use its own Client token or yours. Comment it out to load it from ZAPPR_USE_APP_CREDENTIALS environment variable
11+
UseZapprGithubCredentials="true"
1212

1313
[github]
1414
# Defines the github organization

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ You will need to fill in the following values to be able to create a repo:
2929

3030
### Zappr `zappr`
3131

32-
For Zappr you will need a Zappr URL and a Zappr token, the URL is the base URL of your zappr configuration and the token can be found when you login into the UI and copy the value of any request going out the same domain. The cookies that you need are http-only so you can't access them through JS. Just look for a request with the `Cookie:` header and then copy the value.
32+
For Zappr you will need to specify the URL to Zappr, the URL is the base URL of Zappr, its typically `https://zappr.tools-k8s.hellofresh.io`.
3333

3434
### GitHub `github`
3535

cmd/repo_create.go

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@ import (
2020

2121
// CreateRepoOptions are the flags for the create repository command
2222
type CreateRepoOptions struct {
23-
Description string
24-
Private bool
25-
HasPullApprove bool
26-
HasZappr bool
27-
HasTeams bool
28-
HasCollaborators bool
29-
HasLabels bool
30-
HasDefaultLabels bool
31-
HasWebhooks bool
32-
HasBranchProtections bool
33-
HasIssues bool
34-
HasWiki bool
35-
HasPages bool
23+
Description string
24+
Private bool
25+
HasPullApprove bool
26+
HasZappr bool
27+
UseZapprGithubCredentials bool
28+
HasTeams bool
29+
HasCollaborators bool
30+
HasLabels bool
31+
HasDefaultLabels bool
32+
HasWebhooks bool
33+
HasBranchProtections bool
34+
HasIssues bool
35+
HasWiki bool
36+
HasPages bool
3637
}
3738

3839
// NewCreateRepoCmd creates a new create repo command
@@ -68,6 +69,7 @@ func NewCreateRepoCmd(ctx context.Context) *cobra.Command {
6869
cmd.Flags().BoolVar(&opts.HasDefaultLabels, "rm-default-labels", true, "Removes the default github labels")
6970
cmd.Flags().BoolVar(&opts.HasWebhooks, "has-webhooks", false, "Enables webhooks configurations")
7071
cmd.Flags().BoolVar(&opts.HasBranchProtections, "has-branch-protections", true, "Enables branch protections")
72+
cmd.Flags().BoolVar(&opts.UseZapprGithubCredentials, "use-zappr-credentials", true, "Enables authenticating to Github as Zapps App")
7173

7274
return cmd
7375
}
@@ -95,6 +97,7 @@ func RunCreateRepo(ctx context.Context, repoName string, opts *CreateRepoOptions
9597
logger.Debugf("\tAdd labels to repository? %s", strconv.FormatBool(opts.HasLabels))
9698
logger.Debugf("\tAdd webhooks to repository? %s", strconv.FormatBool(opts.HasWebhooks))
9799
logger.Debugf("\tConfigure branch protection? %s", strconv.FormatBool(opts.HasBranchProtections))
100+
logger.Debugf("\tAuthenticate to Github as Zappr? %s", strconv.FormatBool(opts.UseZapprGithubCredentials))
98101

99102
description := opts.Description
100103
githubOpts := &repo.GithubRepoOpts{
@@ -158,17 +161,23 @@ func RunCreateRepo(ctx context.Context, repoName string, opts *CreateRepoOptions
158161
}
159162

160163
var zapprClient zappr.Client
161-
if cfg.Zappr.Token == "" {
162-
logger.Debug("Authenticating to Zappr using Github token")
163-
zapprClient = zappr.NewWithGithubToken(cfg.Zappr.URL, cfg.Github.Token, nil)
164-
} else {
165-
logger.Debug("Authenticating to Zappr using Zappr token")
166-
zapprClient = zappr.NewWithZapprToken(cfg.Zappr.URL, cfg.Zappr.Token, nil)
164+
zapprClient = zappr.New(cfg.Zappr.URL, cfg.Github.Token, nil)
165+
166+
if cfg.Zappr.UseZapprGithubCredentials {
167+
logger.Debug("Retrieving token for zappr github app from zappr")
168+
err = zapprClient.ImpersonateGitHubApp()
169+
if err != nil {
170+
if errwrap.Contains(err, zappr.ErrZapprUnauthorized.Error()) {
171+
return errwrap.Wrapf("could not retrieve token representing github zappr app from zappr. it seems you have not logged in to zappr, if you have, please logout from zappr, log back in and try again: {{err}}", err)
172+
}
173+
174+
return errwrap.Wrapf("could not retrieve token representing github zappr app from zappr: {{err}}", err)
175+
}
167176
}
168177

169178
err = zapprClient.Enable(*ghRepo.ID)
170179
if errwrap.Contains(err, zappr.ErrZapprAlreadyEnabled.Error()) {
171-
logger.Debug("zappr already enabled, moving on...")
180+
logger.Debug("Zappr already enabled, moving on...")
172181
} else if err != nil {
173182
return errwrap.Wrapf("could not enable zappr: {{err}}", err)
174183
}

cmd/repo_delete.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,7 @@ func RunDeleteRepo(ctx context.Context, name string, opts *DeleteRepoOpts) error
5959
}
6060

6161
var zapprClient zappr.Client
62-
if cfg.Zappr.Token == "" {
63-
logger.Debug("Authenticating to Zappr using Github token")
64-
zapprClient = zappr.NewWithGithubToken(cfg.Zappr.URL, cfg.Github.Token, nil)
65-
} else {
66-
logger.Debug("Authenticating to Zappr using Zappr token")
67-
zapprClient = zappr.NewWithZapprToken(cfg.Zappr.URL, cfg.Zappr.Token, nil)
68-
}
62+
zapprClient = zappr.New(cfg.Zappr.URL, cfg.Github.Token, nil)
6963

7064
logger.Debug("Disabling Zappr on repo...")
7165
err = zapprClient.Disable(*ghRepo.ID)

cmd/root.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ func NewRootCmd() *cobra.Command {
4646
}
4747

4848
cfg := config.WithContext(ctx)
49+
4950
if opts.token != "" {
5051
cfg.Github.Token = opts.token
5152
cfg.GithubTestOrg.Token = opts.token
5253
}
5354

55+
if cfg.Github.Token == "" {
56+
log.WithContext(ctx).Fatal("Github token not specified. Please set the GITHUB_TOKEN environment variable, set it in your config file, or provide it with the \"-t\" flag")
57+
}
58+
5459
if opts.org != "" {
5560
cfg.Github.Organization = opts.org
5661
cfg.GithubTestOrg.Organization = opts.org
@@ -59,7 +64,7 @@ func NewRootCmd() *cobra.Command {
5964

6065
ctx, err = github.NewContext(ctx, cfg.Github.Token)
6166
if err != nil {
62-
log.WithContext(ctx).WithError(err).Fatal("Could not create the kube client")
67+
log.WithContext(ctx).WithError(err).Fatal("could not create the kube client")
6368
}
6469

6570
// Aggregates Root commands

pkg/config/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ type (
3131

3232
// Zappr represents Zappr configurations
3333
Zappr struct {
34-
URL string
35-
Token string
34+
URL string
35+
UseZapprGithubCredentials bool
3636
}
3737

3838
// Github represents the github configurations
@@ -105,7 +105,8 @@ func NewContext(ctx context.Context, configFile string) (context.Context, error)
105105
viper.SetDefault("githubtestorg.token", os.Getenv("GITHUB_TOKEN"))
106106

107107
viper.SetDefault("zappr.url", os.Getenv("ZAPPR_URL"))
108-
viper.SetDefault("zappr.token", os.Getenv("ZAPPR_TOKEN"))
108+
viper.SetDefault("zappr.usezapprgithubcredentials", true)
109+
viper.BindEnv("ZAPPR_USE_APP_CREDENTIALS", "zappr.usezapprgithubcredentials")
109110

110111
err := viper.ReadInConfig()
111112
if err != nil {

pkg/zappr/client.go

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import (
1515
type Client interface {
1616
Enable(repoID int) error
1717
Disable(repoID int) error
18+
ImpersonateGitHubApp() error // Retrieves the Github token representing Zappr and uses that for future requests
1819
}
1920

2021
type clientImpl struct {
21-
zapprToken string
22-
githubToken string
23-
slingClient *sling.Sling
22+
githubToken string
23+
githubZapprAppToken string
24+
slingClient *sling.Sling
2425
}
2526

2627
type zapprErrorResponse struct {
@@ -29,6 +30,10 @@ type zapprErrorResponse struct {
2930
Title string `json:"title,omitempty"`
3031
}
3132

33+
type zapprAppTokenResponse struct {
34+
Token string `json:"token"`
35+
}
36+
3237
var (
3338
timeout = 30 * time.Second
3439

@@ -45,22 +50,8 @@ var (
4550
ErrZapprServerError = errors.New("unknown error from zappr")
4651
)
4752

48-
// NewWithZapprToken creates a new Zappr client that uses Zappr Token to make calls to Zappr
49-
func NewWithZapprToken(zapprURL string, zapprToken string, httpClient *http.Client) Client {
50-
if httpClient == nil {
51-
httpClient = &http.Client{Timeout: timeout}
52-
}
53-
54-
slingClient := sling.New().Client(httpClient).Base(zapprURL)
55-
56-
return &clientImpl{
57-
zapprToken: zapprToken,
58-
slingClient: slingClient,
59-
}
60-
}
61-
62-
// NewWithGithubToken creates a new Zappr client that uses Github Token to make calls to Zappr
63-
func NewWithGithubToken(zapprURL string, githubToken string, httpClient *http.Client) Client {
53+
// New creates a new Zappr client that uses Github Token to make calls to Zappr
54+
func New(zapprURL string, githubToken string, httpClient *http.Client) Client {
6455
if httpClient == nil {
6556
httpClient = &http.Client{Timeout: timeout}
6657
}
@@ -75,12 +66,12 @@ func NewWithGithubToken(zapprURL string, githubToken string, httpClient *http.Cl
7566

7667
// Enable turns on Zappr approval check on a Github repo
7768
func (c *clientImpl) Enable(repoID int) error {
78-
req, err := c.slingClient.Get(fmt.Sprintf("api/repos/%d?autoSync=true", repoID)).Request()
69+
req, err := c.slingClient.Get(fmt.Sprintf("/api/repos/%d?autoSync=true", repoID)).Request()
7970
if err != nil {
8071
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
8172
}
8273

83-
status, zapprErrorResponse, err := c.doRequest(req)
74+
status, zapprErrorResponse, err := c.doRequest(req, nil)
8475
if err != nil {
8576
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
8677
}
@@ -90,7 +81,7 @@ func (c *clientImpl) Enable(repoID int) error {
9081
return errwrap.Wrapf("could not Enable Zappr approval checks on repo: {{err}}", err)
9182
}
9283

93-
status, zapprErrorResponse, err = c.doRequest(req)
84+
status, zapprErrorResponse, err = c.doRequest(req, nil)
9485
if status == http.StatusServiceUnavailable && zapprErrorResponse != nil {
9586
// Zappr already active on the repo
9687
if strings.HasPrefix(zapprErrorResponse.Detail, "Check approval already exists for repository") {
@@ -103,12 +94,12 @@ func (c *clientImpl) Enable(repoID int) error {
10394

10495
// Disable turns off Zappr approval check on a Github repo
10596
func (c *clientImpl) Disable(repoID int) error {
106-
req, err := c.slingClient.Get(fmt.Sprintf("api/repos/%d?autoSync=true", repoID)).Request()
97+
req, err := c.slingClient.Get(fmt.Sprintf("/api/repos/%d?autoSync=true", repoID)).Request()
10798
if err != nil {
10899
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
109100
}
110101

111-
status, zapprErrorResponse, err := c.doRequest(req)
102+
status, zapprErrorResponse, err := c.doRequest(req, nil)
112103
if err != nil {
113104
return errwrap.Wrapf("could not fetch repo on zappr to enable approval check: {{err}}", err)
114105
}
@@ -118,7 +109,7 @@ func (c *clientImpl) Disable(repoID int) error {
118109
return errwrap.Wrapf("could not Disable Zappr approval checks on repo: {{err}}", err)
119110
}
120111

121-
status, zapprErrorResponse, err = c.doRequest(req)
112+
status, zapprErrorResponse, err = c.doRequest(req, nil)
122113
if status == http.StatusServiceUnavailable && zapprErrorResponse != nil {
123114
// Zappr active on the repo, but repo has been deleted from github
124115
if strings.HasSuffix(zapprErrorResponse.Detail, "required_status_checks 404 Not Found") {
@@ -134,15 +125,31 @@ func (c *clientImpl) Disable(repoID int) error {
134125
return err
135126
}
136127

137-
func (c *clientImpl) doRequest(req *http.Request) (int, *zapprErrorResponse, error) {
138-
if c.zapprToken == "" {
139-
req.Header.Add("Authorization", fmt.Sprintf("token %s", c.githubToken))
140-
} else {
141-
req.Header.Add("Cookie", c.zapprToken)
128+
func (c *clientImpl) ImpersonateGitHubApp() error {
129+
req, err := c.slingClient.Get("api/apptoken").Request()
130+
if err != nil {
131+
return errwrap.Wrapf("could not fetch github token for zappr github app: {{err}}", err)
132+
}
133+
134+
tokenResponse := &zapprAppTokenResponse{}
135+
_, _, err = c.doRequest(req, tokenResponse)
136+
if err != nil {
137+
return errwrap.Wrapf("could not fetch github token for zappr github app: {{err}}", err)
138+
}
139+
140+
c.githubZapprAppToken = tokenResponse.Token
141+
return nil
142+
}
143+
144+
func (c *clientImpl) doRequest(req *http.Request, response interface{}) (int, *zapprErrorResponse, error) {
145+
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.githubToken))
146+
147+
if c.githubZapprAppToken != "" {
148+
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.githubZapprAppToken))
142149
}
143150

144151
zapprErrorResponse := &zapprErrorResponse{}
145-
resp, err := c.slingClient.Do(req, nil, zapprErrorResponse)
152+
resp, err := c.slingClient.Do(req, response, zapprErrorResponse)
146153

147154
if resp == nil {
148155
// Even though 0 does not seem like a valid http response code

pkg/zappr/client_test.go

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package zappr
22

33
import (
4+
"fmt"
45
"net/http"
56
"testing"
67

@@ -21,38 +22,12 @@ func TestAuthWithGithubToken(t *testing.T) {
2122
defer testServer.Close()
2223

2324
// Should call the "fetch repo" zappr endpoint to find the just created repo
24-
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", mock.Anything, mock.Anything).Return(test.Response{
25-
Status: http.StatusOK,
26-
})
27-
28-
// Add handlers to define expected call(s) to, and response(s) from Zappr
29-
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getGithubAuthHeader(token), mock.Anything).Return(test.Response{
30-
Status: http.StatusCreated,
31-
})
32-
33-
client.Enable(1)
34-
35-
// Assert expected calls were made to Zappr
36-
zapprMock.AssertExpectations(t)
37-
}
38-
39-
func TestAuthWithZapprToken(t *testing.T) {
40-
token := "abcdefgh"
41-
42-
// Get the Zappr Client, Mock Handler and Test Server
43-
client, zapprMock, testServer := NewMockAndHandlerWithZapprToken(token)
44-
45-
// Start the test server and stop it when done
46-
testServer.Start()
47-
defer testServer.Close()
48-
49-
// Should call the "fetch repo" zappr endpoint to find the just created repo
50-
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", mock.Anything, mock.Anything).Return(test.Response{
25+
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", getGithubAuthHeader(token, false), mock.Anything).Return(test.Response{
5126
Status: http.StatusOK,
5227
})
5328

5429
// Add handlers to define expected call(s) to, and response(s) from Zappr
55-
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getZapprAuthHeader(token), mock.Anything).Return(test.Response{
30+
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getGithubAuthHeader(token, true), mock.Anything).Return(test.Response{
5631
Status: http.StatusCreated,
5732
})
5833

@@ -295,3 +270,47 @@ func TestProblematicRequest(t *testing.T) {
295270
// Assert expected calls were made to Zappr
296271
zapprMock.AssertExpectations(t)
297272
}
273+
274+
func TestImpersonateGitHubApp(t *testing.T) {
275+
token := "12345678"
276+
zapprAppToken := "abcdefgh"
277+
278+
// Get the Zappr Client, Mock Handler and Test Server
279+
client, zapprMock, testServer := NewMockAndHandlerWithGithubToken(token)
280+
281+
// Start the test server and stop it when done
282+
testServer.Start()
283+
defer testServer.Close()
284+
285+
// Should call the "apptoken" zappr endpoint to get the github token representing zappr app and use the users github token
286+
zapprMock.On("Handle", "GET", "/api/apptoken", getGithubAuthHeader(token, false), mock.Anything).Return(test.Response{
287+
Status: http.StatusOK,
288+
Body: []byte(fmt.Sprintf(`{ "token": "%s" }`, zapprAppToken)),
289+
})
290+
291+
err := client.ImpersonateGitHubApp()
292+
293+
// Assert no errors were received
294+
assert.Nil(t, err)
295+
296+
// Assert expected calls were made to Zappr
297+
zapprMock.AssertExpectations(t)
298+
299+
// reset expectations, i need to use the same mock object that is using the retrieved zappr app github token
300+
zapprMock.ExpectedCalls = []*mock.Call{}
301+
302+
// Should call the "fetch repo" zappr endpoint to find the just created repo and use zappr app's github token
303+
zapprMock.On("Handle", "GET", "/api/repos/1?autoSync=true", getGithubAuthHeader(zapprAppToken, false), mock.Anything).Return(test.Response{
304+
Status: http.StatusOK,
305+
})
306+
307+
// Add handlers to define expected call(s) to, and response(s) from Zappr and use zappr app's github token
308+
zapprMock.On("Handle", "PUT", "/api/repos/1/approval", getGithubAuthHeader(zapprAppToken, true), mock.Anything).Return(test.Response{
309+
Status: http.StatusCreated,
310+
})
311+
312+
client.Enable(1)
313+
314+
// Assert expected calls were made to Zappr
315+
zapprMock.AssertExpectations(t)
316+
}

0 commit comments

Comments
 (0)