Skip to content
Closed
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
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 jenkins.util.JenkinsJVM;
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 +139,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 +172,80 @@ 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 DelegatingGitHubAppCredentials(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 DelegatingGitHubAppCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {

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

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

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

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

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

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

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

private final String data;

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

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

}

}

/**
* {@inheritDoc}
Expand Down