Skip to content

Commit b5d35fd

Browse files
committed
different clients
1 parent a873b0c commit b5d35fd

File tree

10 files changed

+337
-115
lines changed

10 files changed

+337
-115
lines changed

pkg/connector/connector.go

Lines changed: 131 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,28 @@ var (
6262
type GitHub struct {
6363
orgs []string
6464
client *github.Client
65-
appClient *github.Client
6665
instanceURL string
6766
graphqlClient *githubv4.Client
6867
hasSAMLEnabled *bool
6968
orgCache *orgNameCache
69+
app gitHubApp
70+
}
71+
72+
// gitHubApp has app clients that are used by the GitHub App.
73+
// Each app has a single JWT token shared across all organizations.
74+
// However, each organization has its own unique installation token.
75+
type gitHubApp struct {
76+
appJWTClient *github.Client
77+
appInstallationClient map[int64]*github.Client
78+
graphqlClient map[int64]*githubv4.Client
7079
}
7180

7281
func (gh *GitHub) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncer {
7382
return []connectorbuilder.ResourceSyncer{
74-
orgBuilder(gh.client, gh.appClient, gh.orgCache, gh.orgs),
75-
teamBuilder(gh.client, gh.orgCache),
76-
userBuilder(gh.client, gh.hasSAMLEnabled, gh.graphqlClient, gh.orgCache),
77-
repositoryBuilder(gh.client, gh.orgCache),
83+
orgBuilder(gh.client, gh.app, gh.orgCache, gh.orgs),
84+
teamBuilder(gh.client, gh.app, gh.orgCache),
85+
userBuilder(gh.client, gh.app, gh.hasSAMLEnabled, gh.graphqlClient, gh.orgCache),
86+
repositoryBuilder(gh.client, gh.app, gh.orgCache),
7887
}
7988
}
8089

@@ -87,7 +96,7 @@ func (gh *GitHub) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
8796

8897
// Validate hits the GitHub API to validate that the configured credentials are still valid.
8998
func (gh *GitHub) Validate(ctx context.Context) (annotations.Annotations, error) {
90-
if gh.appClient != nil {
99+
if gh.app.appJWTClient != nil {
91100
return gh.validateAppCredentials(ctx)
92101
}
93102

@@ -145,37 +154,59 @@ func (gh *GitHub) Validate(ctx context.Context) (annotations.Annotations, error)
145154
}
146155

147156
func (gh *GitHub) validateAppCredentials(ctx context.Context) (annotations.Annotations, error) {
148-
page := 0
149-
orgLogins := gh.orgs
157+
iResp, err := getAllInstallations(ctx, gh.app.appJWTClient)
158+
if err != nil {
159+
return nil, err
160+
}
150161

151-
installationsMap := make(map[string]struct{})
162+
for _, o := range gh.orgs {
163+
if _, ok := iResp.names[o]; !ok {
164+
return nil, fmt.Errorf("access token must be an admin on the %s organization", o)
165+
}
166+
}
167+
return nil, nil
168+
}
169+
170+
type getAllInstallationsResp struct {
171+
ids map[int64]struct{}
172+
installationIds map[int64]*github.Installation
173+
names map[string]struct{}
174+
}
175+
176+
func getAllInstallations(ctx context.Context, c *github.Client) (getAllInstallationsResp, error) {
177+
var (
178+
installationsMap = make(map[int64]struct{})
179+
installationsIDMap = make(map[int64]*github.Installation)
180+
installationsNameMap = make(map[string]struct{})
181+
page = 0
182+
)
152183
for {
153-
installations, resp, err := gh.appClient.Apps.ListInstallations(ctx, &github.ListOptions{Page: page})
184+
installations, resp, err := c.Apps.ListInstallations(ctx, &github.ListOptions{Page: page})
154185
if err != nil {
155-
return nil, fmt.Errorf("github-connector: failed to retrieve org: %w", err)
186+
return getAllInstallationsResp{}, fmt.Errorf("github-connector: failed to retrieve org: %w", err)
156187
}
157188
if resp.StatusCode == http.StatusUnauthorized {
158-
return nil, status.Error(codes.Unauthenticated, "github token is not authorized")
189+
return getAllInstallationsResp{}, status.Error(codes.Unauthenticated, "github token is not authorized")
159190
}
160191
for _, installation := range installations {
161-
if installation.Account == nil {
192+
if installation.GetAccount().GetType() != "Organization" {
162193
continue
163194
}
164-
installationsMap[installation.Account.GetLogin()] = struct{}{}
195+
installationsMap[installation.GetAccount().GetID()] = struct{}{}
196+
installationsIDMap[installation.GetID()] = installation
197+
installationsNameMap[installation.GetAccount().GetLogin()] = struct{}{}
165198
}
166199

167200
if resp.NextPage == 0 {
168201
break
169202
}
170203
page = resp.NextPage
171204
}
172-
173-
for _, o := range orgLogins {
174-
if _, ok := installationsMap[o]; !ok {
175-
return nil, fmt.Errorf("access token must be an admin on the %s organization", o)
176-
}
177-
}
178-
return nil, nil
205+
return getAllInstallationsResp{
206+
ids: installationsMap,
207+
installationIds: installationsIDMap,
208+
names: installationsNameMap,
209+
}, nil
179210
}
180211

181212
// newGitHubClient returns a new GitHub API client authenticated with an access token via oauth2.
@@ -203,38 +234,82 @@ func newGitHubClient(ctx context.Context, instanceURL string, accessToken string
203234

204235
// New returns the GitHub connector configured to sync against the instance URL.
205236
func New(ctx context.Context, ghc *cfg.Github) (*GitHub, error) {
206-
jwttoken, token, err := getClientToken(ghc)
207-
if err != nil {
208-
return nil, err
237+
if ghc.Token == "" {
238+
app, err := newAppClient(ctx, ghc)
239+
if err != nil {
240+
return nil, err
241+
}
242+
return &GitHub{
243+
instanceURL: ghc.InstanceUrl,
244+
orgs: ghc.Orgs,
245+
app: app,
246+
orgCache: newOrgNameCache(nil, app.appInstallationClient),
247+
}, nil
209248
}
210249

211-
client, err := newGitHubClient(ctx, ghc.InstanceUrl, token)
250+
client, err := newGitHubClient(ctx, ghc.InstanceUrl, ghc.Token)
212251
if err != nil {
213252
return nil, err
214253
}
215-
graphqlClient, err := newGitHubGraphqlClient(ctx, ghc.InstanceUrl, token)
254+
graphqlClient, err := newGitHubGraphqlClient(ctx, ghc.InstanceUrl, ghc.Token)
216255
if err != nil {
217256
return nil, err
218257
}
219-
220-
var appClient *github.Client
221-
if jwttoken != "" {
222-
appClient, err = newGitHubClient(ctx, ghc.InstanceUrl, jwttoken)
223-
if err != nil {
224-
return nil, err
225-
}
226-
}
227258
gh := &GitHub{
228259
client: client,
229-
appClient: appClient,
230260
instanceURL: ghc.InstanceUrl,
231261
orgs: ghc.Orgs,
232262
graphqlClient: graphqlClient,
233-
orgCache: newOrgNameCache(client),
263+
orgCache: newOrgNameCache(client, nil),
234264
}
265+
235266
return gh, nil
236267
}
237268

269+
func newAppClient(ctx context.Context, ghc *cfg.Github) (gitHubApp, error) {
270+
key, err := loadPrivateKeyFromString(ghc.AppPrivatekey)
271+
if err != nil {
272+
return gitHubApp{}, err
273+
}
274+
now := time.Now()
275+
token, err := jwtv5.NewWithClaims(jwtv5.SigningMethodRS256, jwtv5.MapClaims{
276+
"iat": now.Unix() - 60, // issued at
277+
"exp": now.Add(time.Minute * 10).Unix(), // expires
278+
"iss": ghc.AppId, // GitHub App ID
279+
}).SignedString(key)
280+
if err != nil {
281+
return gitHubApp{}, err
282+
}
283+
284+
client, err := newGitHubClient(ctx, ghc.InstanceUrl, token)
285+
if err != nil {
286+
return gitHubApp{}, err
287+
}
288+
289+
iResp, err := getAllInstallations(ctx, client)
290+
if err != nil {
291+
return gitHubApp{}, err
292+
}
293+
294+
var (
295+
installationsClient = make(map[int64]*github.Client, len(iResp.installationIds))
296+
installationsGLClient = make(map[int64]*githubv4.Client, len(iResp.ids))
297+
)
298+
for id, installation := range iResp.installationIds {
299+
c, glc, err := getInstallationClient(ctx, ghc.InstanceUrl, id, token)
300+
if err != nil {
301+
return gitHubApp{}, err
302+
}
303+
installationsClient[installation.GetAccount().GetID()] = c
304+
installationsGLClient[installation.GetAccount().GetID()] = glc
305+
}
306+
return gitHubApp{
307+
appJWTClient: client,
308+
appInstallationClient: installationsClient,
309+
graphqlClient: installationsGLClient,
310+
}, nil
311+
}
312+
238313
func newGitHubGraphqlClient(ctx context.Context, instanceURL string, accessToken string) (*githubv4.Client, error) {
239314
httpClient, err := uhttp.NewClient(ctx, uhttp.WithLogger(true, ctxzap.Extract(ctx)))
240315
if err != nil {
@@ -286,48 +361,38 @@ func loadPrivateKeyFromString(p string) (*rsa.PrivateKey, error) {
286361
return x509.ParsePKCS1PrivateKey(block.Bytes)
287362
}
288363

289-
// getClientToken returns
290-
// 1. fine-grained personal access tokens if any.
291-
// 2. (JWT token, installation access tokens) if using github app.
292-
func getClientToken(ghc *cfg.Github) (string, string, error) {
293-
if ghc.Token != "" {
294-
return "", ghc.Token, nil
295-
}
296-
297-
key, err := loadPrivateKeyFromString(ghc.AppPrivatekey)
298-
if err != nil {
299-
return "", "", err
300-
}
301-
now := time.Now()
302-
token, err := jwtv5.NewWithClaims(jwtv5.SigningMethodRS256, jwtv5.MapClaims{
303-
"iat": now.Unix() - 60, // issued at
304-
"exp": now.Add(time.Minute * 10).Unix(), // expires
305-
"iss": ghc.AppId, // GitHub App ID
306-
}).SignedString(key)
307-
if err != nil {
308-
return "", "", err
309-
}
310-
311-
url := fmt.Sprintf("https://api.github.com/app/installations/%s/access_tokens", "67110013")
312-
313-
req, _ := http.NewRequest("POST", url, nil)
314-
req.Header.Set("Authorization", "Bearer "+token)
364+
func getInstallationClient(ctx context.Context, instanceURL string, orgId int64, jwtToken string) (*github.Client, *githubv4.Client, error) {
365+
url := fmt.Sprintf("https://api.github.com/app/installations/%d/access_tokens", orgId)
366+
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
367+
req.Header.Set("Authorization", "Bearer "+jwtToken)
315368
req.Header.Set("Accept", "application/vnd.github+json")
316369

317370
resp, err := http.DefaultClient.Do(req)
318371
if err != nil {
319-
return "", "", err
372+
return nil, nil, err
320373
}
321374
defer resp.Body.Close()
322375

323-
if resp.StatusCode != 201 {
376+
if resp.StatusCode != http.StatusCreated {
324377
body, _ := io.ReadAll(resp.Body)
325-
return "", "", fmt.Errorf("GitHub API error: %s", body)
378+
return nil, nil, fmt.Errorf("GitHub API error: %s", body)
326379
}
327380

328381
var result struct {
329382
Token string `json:"token"`
330383
}
331384
err = json.NewDecoder(resp.Body).Decode(&result)
332-
return token, result.Token, err
385+
if err != nil {
386+
return nil, nil, err
387+
}
388+
389+
client, err := newGitHubClient(ctx, instanceURL, result.Token)
390+
if err != nil {
391+
return nil, nil, err
392+
}
393+
gcclient, err := newGitHubGraphqlClient(ctx, instanceURL, result.Token)
394+
if err != nil {
395+
return nil, nil, err
396+
}
397+
return client, gcclient, nil
333398
}

pkg/connector/helpers.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ func titleCase(s string) string {
2626

2727
type orgNameCache struct {
2828
sync.RWMutex
29-
c *github.Client
30-
orgNames map[string]string
29+
c *github.Client
30+
appClients map[int64]*github.Client
31+
orgNames map[string]string
3132
}
3233

3334
func (o *orgNameCache) GetOrgName(ctx context.Context, orgID *v2.ResourceId) (string, error) {
@@ -50,7 +51,16 @@ func (o *orgNameCache) GetOrgName(ctx context.Context, orgID *v2.ResourceId) (st
5051
return "", err
5152
}
5253

53-
org, _, err := o.c.Organizations.GetByID(ctx, oID)
54+
client := o.c
55+
if len(o.appClients) != 0 {
56+
var ok bool
57+
client, ok = o.appClients[oID]
58+
if !ok {
59+
return "", fmt.Errorf("organization: %d doesn't exist", oID)
60+
}
61+
}
62+
63+
org, _, err := client.Organizations.GetByID(ctx, oID)
5464
if err != nil {
5565
return "", err
5666
}
@@ -60,10 +70,11 @@ func (o *orgNameCache) GetOrgName(ctx context.Context, orgID *v2.ResourceId) (st
6070
return org.GetLogin(), nil
6171
}
6272

63-
func newOrgNameCache(c *github.Client) *orgNameCache {
73+
func newOrgNameCache(c *github.Client, appClients map[int64]*github.Client) *orgNameCache {
6474
return &orgNameCache{
65-
c: c,
66-
orgNames: make(map[string]string),
75+
c: c,
76+
appClients: appClients,
77+
orgNames: make(map[string]string),
6778
}
6879
}
6980

0 commit comments

Comments
 (0)