@@ -62,19 +62,28 @@ var (
6262type 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
7281func (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.
8998func (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
147156func (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.
205236func 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+
238313func 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}
0 commit comments