Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildPlugin(configurations: [
[ platform: "linux", jdk: "8"],
[ platform: "windows", jdk: "8"],
[ platform: "linux", jdk: "11", jenkins: "2.176.4", javaLevel: "8" ]
[ platform: "linux", jdk: "11"]
])
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<changelist>-SNAPSHOT</changelist>
<hpi.compatibleSinceVersion>2.2.0</hpi.compatibleSinceVersion>
<java.level>8</java.level>
<jenkins.version>2.176.4</jenkins.version>
<jenkins.version>2.235.1</jenkins.version>
<useBeta>true</useBeta>
<jjwt.version>0.11.2</jjwt.version>
</properties>
Expand Down Expand Up @@ -114,7 +114,7 @@
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.176.x</artifactId>
<artifactId>bom-2.235.x</artifactId>
<version>11</version>
<scope>import</scope>
<type>pom</type>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@
import hudson.util.ListBoxModel;
import hudson.util.Secret;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import jenkins.security.SlaveToMasterCallable;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GHApp;
import org.kohsuke.github.GHAppInstallation;
import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
Expand Down Expand Up @@ -139,22 +138,22 @@ static String generateAppInstallationToken(String appId, String appPrivateKey, S

}

@NonNull String actualApiUri() {
return Util.fixEmpty(apiUri) == null ? "https://api.github.com" : apiUri;
}

/**
* {@inheritDoc}
*/
@NonNull
@Override
public Secret getPassword() {
if (Util.fixEmpty(apiUri) == null) {
apiUri = "https://api.github.com";
}

long now = System.currentTimeMillis();
String appInstallationToken;
if (cachedToken != null && now - tokenCacheTime < JwtHelper.VALIDITY_MS /* extra buffer */ / 2) {
appInstallationToken = cachedToken;
} else {
appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), apiUri, owner);
appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), actualApiUri(), owner);
cachedToken = appInstallationToken;
tokenCacheTime = now;
}
Expand All @@ -172,56 +171,76 @@ public String getUsername() {
}

/**
* Ensures that the credentials state as serialized via Remoting to an agent includes fields which are {@code transient} for purposes of XStream.
* This provides a ~2× performance improvement over reconstructing the object without that state,
* 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}:
* Ensures that the credentials state as serialized via Remoting to an agent calls back to the controller.
* Benefits:
* <ul>
* <li>We do not need to make API calls to GitHub to obtain a new token.
* <li>The agent never needs to have access to the plaintext private key.
* <li>We can avoid the considerable amount of class loading associated with the JWT library, Jackson data binding, Bouncy Castle, etc.
* <li>The agent need not be able to contact GitHub.
* </ul>
* Drawbacks:
* <ul>
* <li>There is no caching, so every access requires GitHub API traffic as well as Remoting traffic.
* </ul>
* @see CredentialsSnapshotTaker
*/
private Object writeReplace() {
if (/* XStream */Channel.current() == null) {
return this;
}
return new Replacer(this);
return new AgentSide(this);
}

private static final class Replacer implements Serializable {

private final CredentialsScope scope;
private final String id;
private final String description;
private final String appID;
private final Secret privateKey;
private final String apiUri;
private final String owner;
private final String cachedToken;
private final long tokenCacheTime;

Replacer(GitHubAppCredentials onMaster) {
scope = onMaster.getScope();
id = onMaster.getId();
description = onMaster.getDescription();
appID = onMaster.appID;
privateKey = onMaster.privateKey;
apiUri = onMaster.apiUri;
owner = onMaster.owner;
cachedToken = onMaster.cachedToken;
tokenCacheTime = onMaster.tokenCacheTime;
}

private Object readResolve() {
GitHubAppCredentials clone = new GitHubAppCredentials(scope, id, description, appID, privateKey);
clone.apiUri = apiUri;
clone.owner = owner;
clone.cachedToken = cachedToken;
clone.tokenCacheTime = tokenCacheTime;
return clone;
}
private static final class AgentSide extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {

}
static final String SEP = "%%%";

private final String appID;
private final String data;
private transient Channel ch;

AgentSide(GitHubAppCredentials onMaster) {
super(onMaster.getScope(), onMaster.getId(), onMaster.getDescription());
appID = onMaster.appID;
data = Secret.fromString(onMaster.appID + SEP + onMaster.privateKey.getPlainText() + SEP + onMaster.actualApiUri() + SEP + onMaster.owner).getEncryptedValue();
}

private Object readResolve() {
ch = Channel.currentOrFail();
return this;
}

@Override
public String getUsername() {
return appID;
}

@Override
public Secret getPassword() {
try {
return Secret.fromString(ch.call(new GetPassword(data)));
} catch (IOException | InterruptedException x) {
throw new RuntimeException(x);
}
}

private static final class GetPassword extends SlaveToMasterCallable<String, RuntimeException> {

private final String data;

GetPassword(String data) {
this.data = data;
}

@Override
public String call() throws RuntimeException {
String[] fields = Secret.fromString(data).getPlainText().split(SEP);
return generateAppInstallationToken(fields[0], fields[1], fields[2], fields[3]);
}

}

}

/**
* {@inheritDoc}
Expand Down