Skip to content

Commit fd58e5e

Browse files
committed
GitHubAppCredentials on remote side to use SlaveToMasterCallable
1 parent 490693b commit fd58e5e

File tree

1 file changed

+81
-45
lines changed

1 file changed

+81
-45
lines changed

src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@
1414
import hudson.util.ListBoxModel;
1515
import hudson.util.Secret;
1616
import java.io.IOException;
17-
import java.io.Serializable;
1817
import java.util.List;
18+
import jenkins.security.SlaveToMasterCallable;
1919
import org.kohsuke.accmod.Restricted;
2020
import org.kohsuke.accmod.restrictions.NoExternalUse;
2121
import org.kohsuke.github.GHApp;
2222
import org.kohsuke.github.GHAppInstallation;
2323
import org.kohsuke.github.GHAppInstallationToken;
2424
import org.kohsuke.github.GitHub;
25-
import org.kohsuke.github.GitHubBuilder;
2625
import org.kohsuke.stapler.DataBoundConstructor;
2726
import org.kohsuke.stapler.DataBoundSetter;
2827
import 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

Comments
 (0)