1
1
package org .jenkinsci .plugins .github_branch_source ;
2
2
3
3
import com .cloudbees .plugins .credentials .CredentialsScope ;
4
+ import com .cloudbees .plugins .credentials .CredentialsSnapshotTaker ;
4
5
import com .cloudbees .plugins .credentials .common .StandardUsernamePasswordCredentials ;
5
6
import com .cloudbees .plugins .credentials .impl .BaseStandardCredentials ;
6
7
import edu .umd .cs .findbugs .annotations .CheckForNull ;
24
25
25
26
import jenkins .security .SlaveToMasterCallable ;
26
27
import jenkins .util .JenkinsJVM ;
27
- import net .sf .json .JSONObject ;
28
28
import org .jenkinsci .plugins .workflow .support .concurrent .Timeout ;
29
29
import org .kohsuke .accmod .Restricted ;
30
30
import org .kohsuke .accmod .restrictions .NoExternalUse ;
@@ -76,6 +76,13 @@ public GitHubAppCredentials(
76
76
this .privateKey = privateKey ;
77
77
}
78
78
79
+
80
+ private GitHubAppCredentials (GitHubAppCredentials base ) {
81
+ this (base .getScope (), base .getId (), base .getDescription (), base .getAppID (), base .getPrivateKey ());
82
+ this .apiUri = base .getApiUri ();
83
+ this .owner = base .getOwner ();
84
+ }
85
+
79
86
public String getApiUri () {
80
87
return apiUri ;
81
88
}
@@ -116,6 +123,7 @@ public void setOwner(String owner) {
116
123
@ SuppressWarnings ("deprecation" ) // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
117
124
static AppInstallationToken generateAppInstallationToken (String appId , String appPrivateKey , String apiUrl , String owner ) {
118
125
// We expect this to be fast but if anything hangs in here we do not want to block indefinitely
126
+ apiUrl = Util .fixEmpty (apiUrl ) == null ? "https://api.github.com" : apiUrl ;
119
127
try (Timeout timeout = Timeout .limit (30 , TimeUnit .SECONDS )) {
120
128
String jwtToken = createJWT (appId , appPrivateKey );
121
129
GitHub gitHubApp = Connector
@@ -175,45 +183,53 @@ private static long getExpirationSeconds(GHAppInstallationToken appInstallationT
175
183
}
176
184
}
177
185
178
- @ NonNull String actualApiUri () {
179
- return Util .fixEmpty (apiUri ) == null ? "https://api.github.com" : apiUri ;
180
- }
181
-
182
186
/**
183
187
* {@inheritDoc}
184
188
*/
185
189
@ NonNull
186
190
@ Override
187
191
public Secret getPassword () {
188
- String appInstallationToken ;
192
+ AppInstallationToken token = getValidToken ();
193
+ Secret password = Secret .fromString (token .getToken ());
194
+
195
+ LOGGER .log (Level .FINEST , "Returned GitHub App Installation Token for app ID {0}" , appID );
196
+
197
+ return password ;
198
+ }
199
+
200
+ @ NonNull
201
+ private AppInstallationToken getValidToken () {
189
202
synchronized (this ) {
190
203
try {
191
204
if (cachedToken == null || cachedToken .isStale ()) {
192
- LOGGER .log (Level .FINE , "Generating App Installation Token for app ID {0}" , appID );
193
- cachedToken = generateAppInstallationToken (appID ,
194
- privateKey .getPlainText (),
195
- actualApiUri (),
196
- owner );
197
- LOGGER .log (Level .FINER , "Retrieved GitHub App Installation Token for app ID {0}" , appID );
205
+ refreshStaleToken ();
206
+ LOGGER .log (Level .FINER ,
207
+ "Retrieved GitHub App Installation Token for app ID {0}" ,
208
+ appID );
198
209
}
199
210
} catch (Exception e ) {
200
211
if (cachedToken != null && !cachedToken .isExpired ()) {
201
212
// Requesting a new token failed. If the cached token is not expired, continue to use it.
202
213
// This minimizes failures due to occasional network instability,
203
214
// while only slightly increasing the chance that tokens will expire while in use.
204
215
LOGGER .log (Level .WARNING ,
205
- "Failed to generate new GitHub App Installation Token for app ID " + appID + ": cached token is stale but has not expired" ,
206
- e );
216
+ "Failed to generate new GitHub App Installation Token for app ID " + getAppID () + ": cached token is stale but has not expired" );
217
+ // Logging the exception here caused a security exception when trying to read the agent logs during testing
218
+ // Added the exception to a secondary log message that can be viewed if it is needed
219
+ LOGGER .log (Level .FINER , () -> Functions .printThrowable (e ));
207
220
} else {
208
221
throw e ;
209
222
}
210
223
}
211
- appInstallationToken = cachedToken . getToken () ;
224
+ return cachedToken ;
212
225
}
226
+ }
213
227
214
- LOGGER .log (Level .FINEST , "Returned GitHub App Installation Token for app ID {0}" , appID );
215
-
216
- return Secret .fromString (appInstallationToken );
228
+ void refreshStaleToken () {
229
+ LOGGER .log (Level .FINE , "Generating App Installation Token for app ID {0}" , appID );
230
+ synchronized (this ) {
231
+ cachedToken = generateAppInstallationToken (appID , privateKey .getPlainText (), apiUri , owner );
232
+ }
217
233
}
218
234
219
235
/**
@@ -225,6 +241,12 @@ public String getUsername() {
225
241
return appID ;
226
242
}
227
243
244
+ void setCachedToken (AppInstallationToken token ) {
245
+ synchronized (this ) {
246
+ this .cachedToken = token ;
247
+ }
248
+ }
249
+
228
250
private AppInstallationToken getCachedToken () {
229
251
synchronized (this ) {
230
252
return cachedToken ;
@@ -342,123 +364,87 @@ long getTokenStaleEpochSeconds() {
342
364
* <li>The agent need not be able to contact GitHub.
343
365
* </ul>
344
366
*/
345
- private Object writeReplace () {
346
- if (/* XStream */ Channel .current () == null ) {
347
- return this ;
367
+ @ Extension
368
+ public static final class SnapshotTaker extends CredentialsSnapshotTaker <GitHubAppCredentials > {
369
+ @ Override
370
+ public Class <GitHubAppCredentials > type () {
371
+ return GitHubAppCredentials .class ;
348
372
}
349
- return new DelegatingGitHubAppCredentials (this );
350
- }
351
-
352
- private static final class DelegatingGitHubAppCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
353
-
354
- private final String appID ;
355
- /**
356
- * An encrypted form of all data needed to refresh the token.
357
- * Used to prevent {@link GetToken} from being abused by compromised build agents.
358
- */
359
- private final String tokenRefreshData ;
360
- private AppInstallationToken cachedToken ;
361
-
362
- private transient Channel ch ;
363
-
364
- DelegatingGitHubAppCredentials (GitHubAppCredentials onMaster ) {
365
- super (onMaster .getScope (), onMaster .getId (), onMaster .getDescription ());
366
- JenkinsJVM .checkJenkinsJVM ();
367
- appID = onMaster .appID ;
368
- JSONObject j = new JSONObject ();
369
- j .put ("appID" , appID );
370
- j .put ("privateKey" , onMaster .privateKey .getPlainText ());
371
- j .put ("apiUri" , onMaster .actualApiUri ());
372
- j .put ("owner" , onMaster .owner );
373
- tokenRefreshData = Secret .fromString (j .toString ()).getEncryptedValue ();
374
-
373
+ @ Override
374
+ public GitHubAppCredentials snapshot (GitHubAppCredentials credentials ) {
375
375
// Check token is valid before sending it to the agent.
376
376
// Ensuring the cached token is not stale before sending it to agents keeps agents from having to
377
377
// immediately refresh the token.
378
378
// This is intentionally only a best-effort attempt.
379
379
// If this fails, the agent will fallback to making the request (which may or may not fail).
380
380
try {
381
- LOGGER .log (Level .FINEST , "Checking App Installation Token for app ID {0} before sending to agent" , onMaster .appID );
382
- onMaster .getPassword ();
381
+ LOGGER .log (Level .FINEST , "Checking App Installation Token for app ID {0} before sending to agent" , credentials .appID );
382
+ credentials .getPassword ();
383
383
} catch (Exception e ) {
384
- LOGGER .log (Level .WARNING , "Failed to update stale GitHub App installation token for app ID " + onMaster . getAppID () + " before sending to agent" , e );
384
+ LOGGER .log (Level .WARNING , "Failed to update stale GitHub App installation token for app ID " + credentials . appID + " before sending to agent" , e );
385
385
}
386
+ return new DelegatingGitHubAppCredentials (credentials );
387
+ }
388
+ }
389
+
390
+ private static final class DelegatingGitHubAppCredentials extends GitHubAppCredentials {
391
+ private AppInstallationToken snapshotToken ;
392
+ private transient Channel ch ;
386
393
387
- cachedToken = onMaster .getCachedToken ();
394
+ DelegatingGitHubAppCredentials (GitHubAppCredentials baseCredentials ) {
395
+ super (baseCredentials );
396
+ this .snapshotToken = baseCredentials .getCachedToken ();
388
397
}
389
398
390
399
private Object readResolve () {
391
- JenkinsJVM .checkNotJenkinsJVM ();
392
- synchronized (this ) {
393
- ch = Channel .currentOrFail ();
400
+ if (!JenkinsJVM .isJenkinsJVM ()) {
401
+ synchronized (this ) {
402
+ ch = Channel .currentOrFail ();
403
+ this .setCachedToken (snapshotToken );
404
+ }
405
+ // snapshot is only needed during transport
406
+ snapshotToken = null ;
407
+ return this ;
408
+ } else {
409
+ GitHubAppCredentials resolved = new GitHubAppCredentials (this );
410
+ resolved .setCachedToken (this .snapshotToken );
411
+ return resolved ;
394
412
}
395
- return this ;
396
- }
397
-
398
- @ NonNull
399
- @ Override
400
- public String getUsername () {
401
- return appID ;
402
413
}
403
414
404
415
@ Override
405
- public Secret getPassword () {
416
+ void refreshStaleToken () {
406
417
JenkinsJVM .checkNotJenkinsJVM ();
407
- try {
408
- String appInstallationToken ;
409
- synchronized (this ) {
410
- try {
411
- if (cachedToken == null || cachedToken .isStale ()) {
412
- LOGGER .log (Level .FINE , "Generating App Installation Token for app ID {0} on agent" , appID );
413
- cachedToken = ch .call (new GetToken (tokenRefreshData ));
414
- LOGGER .log (Level .FINER , "Retrieved GitHub App Installation Token for app ID {0} on agent" , appID );
415
- }
416
- } catch (Exception e ) {
417
- if (cachedToken != null && !cachedToken .isExpired ()) {
418
- // Requesting a new token failed. If the cached token is not expired, continue to use it.
419
- // This minimizes failures due to occasional network instability,
420
- // while only slightly increasing the chance that tokens will expire while in use.
421
- LOGGER .log (Level .WARNING ,
422
- "Failed to generate new GitHub App Installation Token for app ID " + appID + " on agent: cached token is stale but has not expired" );
423
- // Logging the exception here caused a security exeception when trying to read the agent logs during testing
424
- // Added the exception to a secondary log message that can be viewed if it is needed
425
- LOGGER .log (Level .FINER , () -> Functions .printThrowable (e ));
426
- } else {
427
- throw e ;
428
- }
429
- }
430
- appInstallationToken = cachedToken .getToken ();
418
+ LOGGER .log (Level .FINE , "Generating App Installation Token for app ID {0} on agent" , getAppID ());
419
+ synchronized (this ) {
420
+ try {
421
+ setCachedToken (ch .call (new GetToken (this )));
422
+ } catch (IOException | InterruptedException e ) {
423
+ throw new RuntimeException (e );
431
424
}
432
-
433
- LOGGER .log (Level .FINEST , "Returned GitHub App Installation Token for app ID {0} on agent" , appID );
434
-
435
- return Secret .fromString (appInstallationToken );
436
- } catch (IOException | InterruptedException x ) {
437
- throw new RuntimeException (x );
438
425
}
439
426
}
440
427
441
428
private static final class GetToken extends SlaveToMasterCallable <AppInstallationToken , RuntimeException > {
442
429
443
- private final String data ;
430
+ private final GitHubAppCredentials data ;
444
431
445
- GetToken (String data ) {
432
+ GetToken (GitHubAppCredentials data ) {
446
433
this .data = data ;
447
434
}
448
435
449
436
@ Override
450
437
public AppInstallationToken call () throws RuntimeException {
451
438
JenkinsJVM .checkJenkinsJVM ();
452
- JSONObject fields = JSONObject .fromObject (Secret .fromString (data ).getPlainText ());
453
- LOGGER .log (Level .FINE , "Generating App Installation Token for app ID {0} for agent" , fields .get ("appID" ));
439
+ LOGGER .log (Level .FINE , "Generating App Installation Token for app ID {0} for agent" , data .appID );
454
440
AppInstallationToken token = generateAppInstallationToken (
455
- ( String ) fields . get ( " appID" ) ,
456
- ( String ) fields . get ( " privateKey" ),
457
- ( String ) fields . get ( " apiUri" ) ,
458
- ( String ) fields . get ( " owner" ) );
441
+ data . appID ,
442
+ data . privateKey . getPlainText ( ),
443
+ data . apiUri ,
444
+ data . owner );
459
445
LOGGER .log (Level .FINER ,
460
446
"Retrieved GitHub App Installation Token for app ID {0} for agent" ,
461
- fields . get ( " appID" ) );
447
+ data . appID );
462
448
return token ;
463
449
}
464
450
}
0 commit comments