1414import hudson .util .ListBoxModel ;
1515import hudson .util .Secret ;
1616import java .io .IOException ;
17- import java .io .Serializable ;
1817import java .util .List ;
18+ import jenkins .security .SlaveToMasterCallable ;
1919import org .kohsuke .accmod .Restricted ;
2020import org .kohsuke .accmod .restrictions .NoExternalUse ;
2121import org .kohsuke .github .GHApp ;
2222import org .kohsuke .github .GHAppInstallation ;
2323import org .kohsuke .github .GHAppInstallationToken ;
2424import org .kohsuke .github .GitHub ;
25- import org .kohsuke .github .GitHubBuilder ;
2625import org .kohsuke .stapler .DataBoundConstructor ;
2726import org .kohsuke .stapler .DataBoundSetter ;
2827import org .kohsuke .stapler .QueryParameter ;
@@ -139,22 +138,22 @@ static String generateAppInstallationToken(String appId, String appPrivateKey, S
139138
140139 }
141140
141+ @ NonNull String actualApiUri () {
142+ return Util .fixEmpty (apiUri ) == null ? "https://api.github.com" : apiUri ;
143+ }
144+
142145 /**
143146 * {@inheritDoc}
144147 */
145148 @ NonNull
146149 @ Override
147150 public Secret getPassword () {
148- if (Util .fixEmpty (apiUri ) == null ) {
149- apiUri = "https://api.github.com" ;
150- }
151-
152151 long now = System .currentTimeMillis ();
153152 String appInstallationToken ;
154153 if (cachedToken != null && now - tokenCacheTime < JwtHelper .VALIDITY_MS /* extra buffer */ / 2 ) {
155154 appInstallationToken = cachedToken ;
156155 } else {
157- appInstallationToken = generateAppInstallationToken (appID , privateKey .getPlainText (), apiUri , owner );
156+ appInstallationToken = generateAppInstallationToken (appID , privateKey .getPlainText (), actualApiUri () , owner );
158157 cachedToken = appInstallationToken ;
159158 tokenCacheTime = now ;
160159 }
@@ -172,56 +171,93 @@ public String getUsername() {
172171 }
173172
174173 /**
175- * Ensures that the credentials state as serialized via Remoting to an agent includes fields which are {@code transient} for purposes of XStream.
176- * This provides a ~2× performance improvement over reconstructing the object without that state,
177- * in the normal case that {@link #cachedToken} is valid and will remain valid for the brief time that elapses before the agent calls {@link #getPassword}:
174+ * Ensures that the credentials state as serialized via Remoting to an agent calls back to the controller.
175+ * Benefits:
178176 * <ul>
179- * <li>We do not need to make API calls to GitHub to obtain a new token .
177+ * <li>The agent never needs to have access to the plaintext private key .
180178 * <li>We can avoid the considerable amount of class loading associated with the JWT library, Jackson data binding, Bouncy Castle, etc.
179+ * <li>The agent need not be able to contact GitHub.
180+ * </ul>
181+ * Drawbacks:
182+ * <ul>
183+ * <li>There is no caching, so every access requires GitHub API traffic as well as Remoting traffic.
181184 * </ul>
182185 * @see CredentialsSnapshotTaker
183186 */
184187 private Object writeReplace () {
185188 if (/* XStream */ Channel .current () == null ) {
186189 return this ;
187190 }
188- return new Replacer (this );
191+ return new AgentSide (this );
189192 }
190193
191- private static final class Replacer implements Serializable {
192-
193- private final CredentialsScope scope ;
194- private final String id ;
195- private final String description ;
196- private final String appID ;
197- private final Secret privateKey ;
198- private final String apiUri ;
199- private final String owner ;
200- private final String cachedToken ;
201- private final long tokenCacheTime ;
202-
203- Replacer (GitHubAppCredentials onMaster ) {
204- scope = onMaster .getScope ();
205- id = onMaster .getId ();
206- description = onMaster .getDescription ();
207- appID = onMaster .appID ;
208- privateKey = onMaster .privateKey ;
209- apiUri = onMaster .apiUri ;
210- owner = onMaster .owner ;
211- cachedToken = onMaster .cachedToken ;
212- tokenCacheTime = onMaster .tokenCacheTime ;
213- }
214-
215- private Object readResolve () {
216- GitHubAppCredentials clone = new GitHubAppCredentials (scope , id , description , appID , privateKey );
217- clone .apiUri = apiUri ;
218- clone .owner = owner ;
219- clone .cachedToken = cachedToken ;
220- clone .tokenCacheTime = tokenCacheTime ;
221- return clone ;
222- }
194+ private static final class AgentSide extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
223195
224- }
196+ static final String SEP = "%%%" ;
197+
198+ private final String data ;
199+ private Channel ch ;
200+
201+ AgentSide (GitHubAppCredentials onMaster ) {
202+ super (onMaster .getScope (), onMaster .getId (), onMaster .getDescription ());
203+ data = Secret .fromString (onMaster .appID + SEP + onMaster .privateKey .getPlainText () + SEP + onMaster .actualApiUri () + SEP + onMaster .owner ).getEncryptedValue ();
204+ }
205+
206+ private Object readResolve () {
207+ ch = Channel .currentOrFail ();
208+ return this ;
209+ }
210+
211+ @ Override
212+ public String getUsername () {
213+ try {
214+ return ch .call (new GetUsername (data ));
215+ } catch (IOException | InterruptedException x ) {
216+ throw new RuntimeException (x );
217+ }
218+ }
219+
220+ @ Override
221+ public Secret getPassword () {
222+ try {
223+ return Secret .fromString (ch .call (new GetPassword (data )));
224+ } catch (IOException | InterruptedException x ) {
225+ throw new RuntimeException (x );
226+ }
227+ }
228+
229+ private static final class GetUsername extends SlaveToMasterCallable <String , RuntimeException > {
230+
231+ private final String data ;
232+
233+ GetUsername (String data ) {
234+ this .data = data ;
235+ }
236+
237+ @ Override
238+ public String call () throws RuntimeException {
239+ return Secret .fromString (data ).getPlainText ().split (SEP )[0 ];
240+ }
241+
242+ }
243+
244+ private static final class GetPassword extends SlaveToMasterCallable <String , RuntimeException > {
245+
246+ private final String data ;
247+
248+ GetPassword (String data ) {
249+ this .data = data ;
250+ }
251+
252+ @ Override
253+ public String call () throws RuntimeException {
254+ String [] fields = Secret .fromString (data ).getPlainText ().split (SEP );
255+ return generateAppInstallationToken (fields [0 ], fields [1 ], fields [2 ], fields [3 ]);
256+ }
257+
258+ }
259+
260+ }
225261
226262 /**
227263 * {@inheritDoc}
0 commit comments